Java: Programmatische Bestimmung der aktuellen Klasse

von Hubert Schmid vom 2012-04-22

Viele Java-Projekte setzen für das Logging Bibliotheken ein, die mit log4j verwandt sind und hierarchisch aufgebaute Kategorien verwenden. Dabei wird die Struktur in der Regel am Klassen- oder Paketnamen festgemacht. Mit der Bibliothek commons-logging sieht das beispielsweise so aus:

public class Foo { private static final Log log = LogFactory.getLog(Foo.class); // ... }

Erfahrungsgemäß führen solche Redundanzen über die Zeit zu Inkonsistenzen. Ein paar Zeilen werden von einer Datei in eine andere kopiert, ohne die Referenz auf die Klasse anzupassen, so dass die Initialisierung schließlich wie im folgenden Listing aussieht.

public class Bar { // NOTE: Bar.class should be used instead of Foo.class private static final Log log = LogFactory.getLog(Foo.class); // ... }

Da dieser Fehler immer wieder gemacht wird, beschreibe ich im Folgenden ein paar Möglichkeiten, wie er sich technisch vermeiden lässt.

Object.getClass()

Vergleichsweise naheliegend ist die Verwendung der Methode getClass(), die jede Klasse von Object erbt. Diese Variante ist im folgenden Listing zu sehen. Doch leider weist sie gleich zwei Probleme auf.

public class Bar { private final Log log = LogFactory.getLog(getClass()); // ... }

Erstens lässt sich diese Methode nur in nicht-statischem Kontext verwenden, was unter Umständen zu einem Laufzeit-Overhead führen kann. Und zweitens ist der Ausdruck nicht identisch mit Bar.class. Denn getClass() liefert den dynamischen Typ der aktuellen Instanz, der sich von der deklarierenden Klasse unterscheidet, wenn eine Instanz einer Unterklasse erzeugt wird. In vielen Fällen anderen Situationen ist dieses Verhalten erwünscht, doch beim Logging ist es sehr verwirrend.

Thread.getStackTrace()

Die zweite Variante wertet den Stack-Trace des aktuellen Threads aus, um die umgebende Klasse zu bestimmen. Die Initialisierung erfolgt nach dem statischen Import der Hilfsfunktion CurrentClassUtil.getCurrentClass() mit dem Ausdruck LogFactory.getLog(getCurrentClass()). Die Hilfsfunktion sieht dabei schematisch wie folgt aus:

package org.example.util; public final class CurrentClassUtil { public static Class<?> getCurrentClass() { StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); // Do some magic to determine to correct stack trace element // and create the corresponding class object. } }

In der Praxis funktioniert das ordentlich. Allerdings wird dabei aus meiner Sicht einer Funktionalität missbraucht, die eigentlich nur für Debugging-Zwecke gedacht ist. Auch die API-Dokumentation der Methode Thread.getStackTrace weist auf ihre Art darauf hin, dass die Methode nicht für diese Verwendung gedacht ist: Some virtual machines may, under some circumstances, omit one or more stack frames from the stack trace. In the extreme case, a virtual machine that has no stack trace information concerning this thread is permitted to return a zero-length array from this method.

Anonyme Klassen und Class.getEnclosingClass()

Meine bevorzugte Variante zur Bestimmung der aktuellen Klasse ist ein wenig ungewöhnlich, und die Entsprechung zu den vorherigen Beispielen sieht ungefähr so aus:

public class Bar { private final Log log = LogFactory.getLog(new Object() { }.getClass().getEnclosingClass()); // ... }

Bei diesem Ausdruck spielen zwei Dinge zusammen: Ersten wird mit new Object() { } durch die geschweiften Klammern eine anonyme Klasse definiert, die von Object abgeleitet ist – ohne weitere Member zu definieren. Und zweitens wird die Möglichkeit genutzt, zu einer inneren Klasse die umgebende Klasse zu bestimmen. Das passiert beim Aufruf der Methode Class.getEnclosingClass().

Dieser Ausdruck bestimmt also einfach und zuverlässig die aktuelle Klasse. Allerdings hat er den Nachteil, dass er in dieser Form sowohl relativ lang als auch unverständlich ist. Beides lässt sich durch die folgende Hilfsklasse verbessern:

package org.example.util; public abstract class CurrentClass { public Class<?> get() { return getClass().getEnclosingClass(); } }

Damit reduziert sich der Ausdruck für die Bestimmung der aktuellen Klasse auf new CurrentClass() { }.get(). Die Basisklasse CurrentClass ist abstrakt um zu verhindern, dass die geschweiften Klammern versehentlich vergessen werden. Denn sie machen den entscheidenden Unterschied zwischen einer normalen Instantiierung und der Definition einer anonymen Klasse aus.

Richtig sinnvoll wird dieser Ansatz allerdings erst, wenn man ihn mir der eigentlichen Verwendung kombiniert – in diesem Fall mit der Log-Instanz. Statt der allgemeinen Klasse CurrentClass wird dann eine spezifische Klasse für das Logging verwendet.

public class Bar { private static final Log log = new ClassCategoryLog() { }; // ... }

Die Klasse ClassCategoryLog ist grundsätzlich nach dem gleichen Prinzip wie CurrentClass aufgebaut. Der wesentliche Teil der Implementierung ist im folgenden Listing zu sehen:

package org.example.util; abstract class AbstractDelegateLog implements Log { private final Log delegate = getDelegate(); protected abstract Log getDelegate(); @Override public final boolean isDebugEnabled() { return delegate.isDebugEnabled(); } // ... other methods delegating to delegate object } public abstract class ClassCategoryLog extends AbstractDelegateLog { @Override protected Log getDelegate() { return LogFactory.getLog(getClass().getEnclosingClass()); } }

Klassen-Overhead

Es gibt einen Nachteil dieses Verfahrens, auf den ich kurz eingehen möchte. An jeder Code-Stelle, an die umgebende Klasse der auf diese Weise bestimmt, legt der Java-Compiler eine zusätzliche Klasse an. Dadurch kann die Gesamtanzahl der Klassen in einem Projekt signifikant ansteigen, was im Wesentlichen zwei negative Auswirkungen haben kann: Erstens müssen die Klassen irgendwann geladen werden, was relativ viel CPU kostet. Und zweitens verbrauchen sie zur Laufzeit dauerhaft Speicher im sogenannten PermGen Space, der in der Größe beschränkt ist.

Um diese Probleme besser bewerten zu können, habe ich einen kleinen Test mit einer sehr großen Anzahl generierter Klassen gemacht. Das folgende Python-Skript erzeugt ein vollständiges Programm, das abhängig vom Kommandozeilenargument sehr viele Klassen erzeugt, die alle bei der Ausführung geladen werden.

#! /usr/bin/env python3 import sys def make(depth): if depth <= 1: return '1' else: expr = 'new V() { int v() { return ' + make(depth - 1) + '; }; }' return 'r({}, {})'.format(expr, expr) depth, = map(int, sys.argv[1:]) print(''' public final class Main { static abstract class V { abstract int v(); } public static int r(V a, V b) { return 1 + a.v() + b.v(); } public static void main(String... args) { int value = ''' + make(depth) + '''; System.out.println(value); } } '''.strip())

Im Fall des PermGen Space hat sich gezeigt, dass auch hunderttausend Klassen ohne Weiteres möglich sind, was zumindest bei handgeschriebenem Code hinreichend viel Luft lässt.

Auch das Laden der Klassen sollte im Allgemeinen kein Problem sein. In meiner lokalen Testumgebung wurden ungefähr zehntausend Klassen pro Sekunde geladen. Das ist ein durchaus akzeptabler Wert – insbesondere wenn man bedenkt, dass Klassen üblicherweise erst bei Bedarf geladen werden. In meinem Test ist allerdings auch aufgefallen, dass die Ladezeiten sehr stark von der Betriebssystemumgebung abhängen. Bei diesem Programm liegen alle Klassen im gleichen Paket und damit auch im gleichen Verzeichnis. So war bei sehr vielen Klassen zu beobachten, dass die Gesamtlaufzeit des Tests stark durch die Verzeichnisoperationen des Betriebssystems beeinflusst wurde.

Exkurs: Methoden-Kontext

Analog zum Klassen-Kontext lässt sich auf die gleiche Weise auch die aktuelle Methode bestimmen. Das Logging ist auch dafür ein guter Anwendungsfall, denn in vielen Fällen ist es hilfreich, wenn technische Log-Meldungen auch den Methoden-Namen enthalten. Dafür muss in den obigen Beispielen einfach die Methode Class.getEnclosingClass() durch Class.getEnclosingMethode() ersetzt werden – beziehungsweise durch Class.getEnclosingConstructor im Falle eines Konstruktors.

Fazit

Anonyme Klassen wurden vor 15 Jahren in Java eingeführt, doch viele ihrer Möglichkeiten werden auch heute nur selten genutzt. In diesem Artikel habe ich gezeigt, wie man mit ihrer Hilfe einfach und effizient die umgebende Klasse und Methode programmatisch bestimmen kann. In vielen Situationen ist dieses Verfahren den Alternativen deutlich überlegen – zumindest solange man vom massiven Einsatz in generiertem Code absieht. Wichtig ist nur, dass Entwickler die Syntax anonymer Klassen mit den beiden geschweiften Klammern als solche erkennen und verstehen.