Inhaltsübersicht:
- AOP und Logging - Teil I
- Einführung in das Thema Logging und AOP. Was ist Logging? Was ist Tracing? Motivation für einen neuen Ansatz.
- AOP und Logging - Teil II
- Eine Lösungsansatz ohne AspectJ.
- AOP und Logging - Teil III
- Logging vs. Debugging und eine erste einfache Lösung.
Im letzten Beitrag habe ich versprochen eine Lösung für das Logging mittels AOP bzw. AspectJ zu präsentieren. In einem ersten Schritt werde ich erst einmal eine Lösung ohne AspectJ präsentieren und dann werde ich diese Lösung in späteren Beiträgen weiter verfeinern.
Zunächst werden alle identifizierten Tracing-Einträge des Beispiels entfernt, da diese relativ einfach mit einem Aspect behandelt werden können. Dann sieht die Klasse wie folgt aus:
Zunächst werden alle identifizierten Tracing-Einträge des Beispiels entfernt, da diese relativ einfach mit einem Aspect behandelt werden können. Dann sieht die Klasse wie folgt aus:
package ajlog.example;
// #1#
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyClass {
// #2#
private static Logger logger = LoggerFactory.getLogger(MyClass.class);
public Object doSomethingWith2Objects(Object a, Object b) {
try {
Number ai = (Number) a;
Number bi = (Number) b;
if (ai.intValue() > 100)
logger.warn("a is not a tiny number"); // #3#
if (bi.intValue() > 100)
logger.warn("b is not a tiny number"); // #3#
return new Long(ai.intValue() + bi.longValue());
}
catch (RuntimeException ex) {
}
return null;
}
}
Nachdem die Tracing-Einträge entfernt wurden, sieht das ganze schon deutlich übersichtlicher und wartbarer aus. Unschön ist, dass es nach wie vor eine explizite Abhängigkeit zur Logging-Library und der Erzeugung einer Logging-Instanz habe (#1# und #2#). Zudem ist die Bedeutung, die hinter dem Loggingaufruf (#3#) steht, nicht repräsentiert. Der nächste Schritt ist also: Den Logging-Einträgen eine Bedeutung geben. Das könnte dann wie folgt aussehen:
package ajlog.example; public class MyClass { public Object doSomethingWith2Objects(Object a, Object b) { try { Number ai = (Number) a; Number bi = (Number) b; if (ai.intValue() > 100) MyLog.isNotATinyNumber("a", ai, 100); if (bi.intValue() > 100) MyLog.isNotATinyNumber("b", bi, 100); return new Long(ai.intValue() + bi.longValue()); } catch (RuntimeException ex) { } return null; } }
und die dazugehörige Implementation von MyLog ...
package ajlog.example; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyLog { // #1# Only drawback : It's MyLog not MyClass the logger belongs to. private static Logger logger = LoggerFactory.getLogger(MyLog.class); public static void isNotATinyNumber( String varName, Number currValue, int compValue ) { if (logger.isWarnEnabled()) { logger.warn("{} is not a tiny number ({} > {})", new Object[]{ varName, currValue, new Integer(compValue) } ); } } }
Was haben wir dadurch dieses refactoring erreicht?
Zunächst mal das Offensichtliche:
- Es wird keine Logging-Instanz mehr zur Klasse MyClass hinzugefügt und auch die imports fehlen. Die direkte Abhängigkeit ist aufgelöst worden.
- Es gibt nur noch eine (Klassen-)Methode isNotATinyNumber(). D.h. es gibt nur noch eine (Logging-)Methode, egal ob dies Variable "a" oder "b" betrifft.
- Aus Logging-Eintrag ist eine bedeutungstragende Einheit, eine Logging-Methode geworden, die einer Logging-Klasse zusammengefasst werden können.
- Die erzeugte Logging-Methode ist wiederverwendbar.
- Das Logging kann systematisiert werden.
- Die Logging Library ist (relativ schnell) austauschbar.
- Es kann eine Cross Referenz Liste erstellt werden.
- Mittels JavaDoc kann das gesamte Logging dokumentiert werden.
- Der Level lässt sich ändern.
- Der Text lässt sich ändern.
- Diese Art von Logging kann von einer IDE besser unterstützt werden: Die Logging-Klasse (MyLog) und die Logging-Methode (isNotATinyNumber) mit all ihren Parametern wird mehr oder weniger auf Knopfdruck erzeugt, bzw. bestehende Klassen und Methoden werden aufgelistet.
- Eine einfache Art der Systematik: Man kann zwischen globalen (relevanten) und lokalen (weniger relevanten) Logging unterscheiden. Man nutzt einfach public static und (package) static zur Markierung der Logging-Methoden, also eine Klassenmethode ohne jeden Zugriffsqualifizierer (public, protected, private). Leztere wären dann Debug-Ausgaben. Erstere wären Info-, Warning-, Error- und Fatal-Ausgaben.
Ein "Nachteil" (#1#) will nicht verschweigen: In der Logging-Ausgabe wird nicht protokoliert, wer die Quelle des Log-Eintrags war, also der Aufrufer von MyLog.isNotATinyNumber(). Aber dies gehört meiner Meinung nach nur in den Bereich eines Log-Eintrages auf dem Level des Debugging und ist ein zu verkraftender Nachteil (und lässt sich mit einem etwas einmalig erhöhten Aufwand sogar eliminieren).
Natürlich bin ich noch am Ziel meiner Träume, aber der erste grosse Schritt ist vollzogen.
Was fehlt mir zu meinem Glück?
- Ich würde gerne das ganze auch ausführen können, ohne das ich eine Logging Library im classpath habe.
- Ich würde gerne Texte und Level anpassen können ohne neu zu builden.
- Die versprochene Internationalisierung fehlt auch noch.
- Ich will die Logging Library schmerzlos austauschen können. D.h. indem ich ein JAR im classpath austausche.
Ich mache es spannend und werde ein ersten naiven Aspekt im Teil III entwickeln ;-)