Klassenname für Logger unter Java automatisch ermitteln

Vor einiger Zeit schrieb ich einen Artikel darüber, wie der Klassenname für einen Logger ermittelt werden kann. Im Endergebnis sah die damalige Lösung, unter Nutzung der Simple Logging Facade for Java kurz SLF4J, wie folgt aus:

Logger log = LoggerFactory.getLogger(new Exception().fillInStackTrace().getStackTrace()[0].getClassName());

Damit wird der gesamte Stacktrace zusammengesammelt und entsprechend der Klassenname extrahiert. Das Problem an dieser Variante ist das der gesamte Stack dafür ausgewertet wird und für jede Nutzung eines Logs eine relativ unintuitive und lange Zeile von A nach B kopiert werden muss. Anders sieht es mit folgender Lösung aus:

package net.seeseekey.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utility class to get logger
 */
public final class Logging {

    private Logging() {
    }

    public static Logger getLogger() {

        StackWalker.StackFrame frame = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
                .walk(stream -> stream.skip(1)
                        .findFirst()
                        .orElse(null));

        if (frame == null) {
            return LoggerFactory.getLogger("Common");
        }

        return LoggerFactory.getLogger(frame.getClassName());
    }
}

Bei dieser Utility-Klasse wird der entsprechende Klassenname dynamisch über einen StackWalker ermittelt. Zurückgegeben wird hierbei der Klassenname des Aufrufers. Konnte kein Klassenname ermittelt werden, so wird stattdessen ein Logger mit dem Namen Common zurückgegeben. Damit kann der eigentliche Logger nun wie folgt angelegt werden:

Logger log = Logging.getLogger();

Der StackWalker ist ab Java 9 verfügbar und kann somit in neueren Projekten problemlos genutzt werden. Im Gegensatz zu den bisherigen Methoden Teile des Stacktrace zu erhalten, ist der StackWalker aus Performancesicht zu bevorzugen. Definiert wurde diese API in der JEP 259.

Maps mittels GSON deserialisieren

GSON ist eine von Google entwickelte Java-Bibliothek, welche ursprünglich für den internen Gebrauch bei Google gedacht war. Mit dieser ist, neben vielen anderen Dingen, unter anderem die Serialisierung von Java-Objekten in JSON möglich. Gegeben sei für ein kleines Beispiel die Klasse FooObject:

public class FooObject {

    private int id;
    private String key;
    private String value;

    public FooObject(int id, String key, String value) {
        this.id = id;
        this.key = key;
        this.value = value;
    }

    public int getId() {
        return id;
    }

    public String getKey() {
        return key;
    }

    public String getValue() {
        return value;
    }
}

Die Klasse besteht aus drei internen Variablen, welche eine Id, einen Key und einen Value halten. Daneben existieren ein Konstruktor und drei Getter-Methoden für die Rückgabe der internen Variablen. Mithilfe von GSON kann eine Instanz der Klasse einfach zu JSON serialisiert werden:

Gson gson = new Gson();
FooObject fooObject = new FooObject(1, "keyA", "valueA");
String jsonFooObject = gson.toJson(fooObject);

Heraus kommt bei dieser Serialisierung folgendes JSON-Objekt:

{
   "id":1,
   "key":"keyA",
   "value":"valueA"
}

Auch die Deserialisierung des Objektes mittels GSON ist kein Problem:

fooObject = gson.fromJson(jsonFooObject, FooObject.class);

Interessanter und komplizierter wird es, wenn Maps mittels GSON serialisiert werden sollen:

Map values = new HashMap<>();
values.put("A", new FooObject(1, "keyA", "valueA"));
values.put("B", new FooObject(2, "keyB", "valueB"));
values.put("C", new FooObject(3, "keyC", "valueC"));

Die Serialisierung gestaltet sich noch einfach:

String json = gson.toJson(values);

Bei der Deserialisierung mittels:

Map map = gson.fromJson(json, Map.class);

erhält der Nutzer allerdings eine LinkedTreeMap. Auf die Werte der Map kann durchaus zugegriffen werden:

String value = map.get("A").get("key");

Allerdings schlägt die Umwandlung in ein Objekt vom Typ FooObject fehl:

FooObject fooObjectB = map.get("A");

Stattdessen erhält der Nutzer eine ClassCastException:

Exception in thread "main" java.lang.ClassCastException: com.google.gson.internal.LinkedTreeMap cannot be cast to org.example.console.FooObject

Damit der Cast gelingt muss bei der Deserialisierung der Map anders gearbeitet werden:

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

import java.lang.reflect.Type;

...

Type type = new TypeToken>(){}.getType();
Map map = gson.fromJson(json, type);

Damit kann die Map mit dem FooObject normal genutzt werden. Mithilfe des TypeTokens kann ein generischer Typ repräsentiert und somit für die Deserialisierung mittels GSON eingesetzt werden.

Exception-Hierarchie unter Java

Java nutzt, wie viele andere Sprachen, Exceptions zur Fehlersignalisierung und Fehlerbehandlung. Folgender Code würde hierbei dem Anschein nach alle Exceptions fangen:

try {
  // Do something wrong
} catch(Exception e) {
  // Gotta Catch 'Em All
}

An dieser Stelle trügt der Schein nicht. Allerdings werden nur einige Fehlerklassen gefangen, nämlich nur solche vom Typ Exception. Die Hierarchie der Fehlerklassen ist unter Java ein wenig differenzierter. In Java erbt jede Klasse implizit von der Klasse Object und so erbt auch die Klasse Throwable von dieser und implementiert das Interface Serializable.

Die Hierarchie der Klassen, welche für Fehlersignalisierung zuständig sind

Von der Klasse Throwable wiederum erben die Klassen Error und Exception. Fehler vom Typ Error stellen laut Definition immer Fehler innerhalb der JVM da, während Exceptions gewöhnliche Fehler des Programmes bzw. des Entwicklers sind. Sollen nun alle Fehlerklassen gefangen werden, so müsste der Quellcode wie folgt aussehen:

try {
  // Do something wrong
} catch(Throwable t) {
  // Gotta Catch 'Em All
}

Das Beispiel sollte nur als solches betrachtet werden, da es sich immer empfiehlt spezielle Fehler zu fangen und zu behandeln. Ein solch allgemeiner Fehlerhandler eignet sich nur für Spezialfälle wie z.B. das Logging nicht behandelter Fehler. Die Hierarchie verästelt sich anschließend noch weiter, so erben unterschiedlichste Klassen von der Klasse Error. Bei der Klasse Exception sieht dies ähnlich aus, allerdings existiert hier eine Besonderheit, die Klasse RuntimeException. Normalerweise muss eine Methode Exceptions, die sie wirft im Methodenkopf bekannt geben, wenn sie nicht in der Methode behandelt werden:

public static void example() throws Exception {
  throw new Exception();
}

Bei Klassen die von der Klasse RuntimeException erben muss diese Bekanntmachung im Methodenkopf nicht erfolgen. Sie werden trotzdem nach oben durchgereicht bis sie gefangen werden oder sich das Programm beendet, wenn die Behandlung der Exception nicht durchgeführt wurde.

Java Exceptions in Strings verpacken

Wenn in einem Java-Programm etwas schiefläuft so, wird in den meisten Fällen eine Exception erzeugt und diese nach oben im Stack durchgereicht, bis sie behandelt wird. Manchmal ist es notwendig die Exception bzw. Teile davon in einen String umzuwandeln. Natürlich kann die Exception nun mühsam von Hand auseinander genommen und in einen String konvertiert werden. Sinnvoller ist es die Hilfsmethoden aus der Utility-Bibliothek Guava zu nutzen:

String stacktrace = Throwables.getStackTraceAsString(e);

Bei der Methode getStackTraceAsString wird der Stacktrace der betreffenden Exception in einen String umgewandelt. Neben dieser Methode existieren weitere Methoden rund um das Exception-Handling in der Klasse Throwables. Einfacher wird das Ganze, wenn ein Logger genutzt wird und in diesem die Exception auftauchen soll:

Logger logger = LoggerFactory.getLogger(new Exception().fillInStackTrace().getStackTrace()[0].getClassName());
logger.error("Exception: ", e);

In diesem Fall bieten die meisten Logger die Möglichkeit an, das Throwable direkt als Parameter zu übergeben. Die weitere Verarbeitung der Exception wird anschließend vom Logger erledigt.

Ausnahmen in der MediaWiki mit Details anzeigen

Unter Umständen kann es vorkommen, das die MediaWiki Software im Betrieb eine Ausnahme (Exception) wirft. Dann bekommt man eine Meldung nach dem Schema:

[6030c238] 2015-02-16 8:48:30: Fatal exception of type MWException

Diese Aussage ist natürlich zur Fehlerfindung nicht sehr aussagekräftig. Damit man eine ausführliche Ausgabe zur Exception bekommt, muss in der LocalSettings.php Datei folgende Option hinzugefügt werden:

$wgShowExceptionDetails=true;

Anschließend erhält man im Falle eine Exception einen sauberen Callstack, mit welchen man sich auf Fehlersuche begeben kann.