Die @ToString Java Annotation benötigt Werkzeugunterstützung, damit sie irgend einen Sinn macht. Ich hatte hier schon eine Beispielimplementierung verlinkt. Die Implementierung zeigt nur, wie es geht und das es geht. Sie ist weit von Vollständigkeit entfernt. Beispielsweise behandelt sie nicht alle Eventualitäten und skaliert auch nicht hinsichtlich Werkzeugunterstützung.
Werkzeugunterstützung
Die Werkzeugunterstützung soll sich nicht nur auf ein Frontend beschränken. Ich selbst bevorzuge Maven, kann aber die Vorbehalte dagegen nachvollziehen. Deswegen sollen auch Ant, Gradle oder Buildr User sollen unterstützt werden. Deswegen wird der eigentliche @ToString Prozessor von der @ToString Implementierung und den Frontends getrennt.
Die Trennung zwischen der @ToString Annotation und dem Prozessor wird vorgenommen, da
- der Prozessor nichts in der Anwendung selbst zu suchen hat
- es weitere Prozessorimplementierungen geben kann
Der Prozessor
Der Prozessor muss zwei Dinge erledigen:
- den Bytecode von Klassen untersuchen, um die
@ToStringAnnotationen aufzusammeln - aus den aufgesammelten
@ToStringAnnotationsinformationen gegebenenfalls einetoString()Methode in den untersuchten Bytecode einfügen
Analyse
Der Prozessor analysiert den Bytecode. Dies ist mit Unterstützung von Werkzeugen wie ASM trivial. Anders als bei einer Analyse mittels Reflection muss die Vererbungshierarchie allerdings manuell untersucht werden. Bei Reflection Analysen hat sich in anderen Projekten gezeigt, dass es öfter zu ExceptionInInitializerError kommt als gedacht. Insbesondere bei grösseren Projekten, in denen auch unerfahrene Entwickler mitarbeiten, ist dies ein Phänomen.
Die Analyse der Vererbung muss berücksichtigen, dass private Methoden und Felder in Oberklassen nicht berücksichtigt werden können. Bei package-private Feldern und Methoden muss geprüft werden, ob sich die Ober- bzw. Unterklasse im selben Package befindet. Ist dies der Fall, dürfen sie ausgewertet werden. Ist es nicht der Fall, so dürfen sie – wie bei private – nicht berücksichtigt werden.
protected und public Felder und Methoden sind kein Problem.
Das Analysewerkzeug muss auch statische und/oder finale Methoden und Felder berücksichtigen. Wie diese zu behandeln sind, wird dann später bei der toString() Methodenerzeugung berücksichtigt.
Erzeugung
Der Prozessor muss einfach sein und eine robuste toString() Methode implementieren. Hierbei wird stark auf Java Bordmittel zurückgegriffen. Innerhalb der toString() Implementierung wird ein StringBuilder verwendet. Mittels dessen append Methoden werden die Werte von Feldern und Methoden aneinandergereit.
Zu beachten ist, dass StringBuilder keine Methoden für das Anhängen der primitiven Typen byte und short hat. Hier würde es beim Laden des Bytecodes zu einem verify-Fehler kommen. Mit byte und short muss deswegen die entsprechende int Methode aufgerufen werden. Ebenfalls sollte die append(char[]) des StringBuilders nicht aufgerufen werden. Grund: es sollen die Werte des Arrays dargestellt werden, nicht das Array als String.
Der Rückgriff auf den StringBuilder und die Arrays Hilfsklasse, die im folgenden Abschnitt erläutert wird, befreien auch von null Referenz Prüfung. Diese Aufgabe übernehmen diese beiden Systemklassen mit ihren Methoden.
Arrays
Für die Arrayunterstützung bietet Java mit der Klasse Arrays einige fundamentale Hilfsmethoden an. Uns interessieren dabei aktuell die toString(…) Methoden. Diese Methoden erzeugen aus einem eindimensionalen Array einen definierten String. Für mehrdimensionale Arrays steht die deepToString(Objet[]) Methode bereit. Der erzeugende Code muss also untersuchen, ob es sich um ein eindimensionales Array oder um ein mehrdimensionales Array handelt und entsprechend reagieren.
enum, Interfaces und Annotationen
Grundsätzlich ist es verboten Methoden in Java Interfaces oder Java Annotationen zu erzeugen. Der Classloader würde soetwas mit einem VerifyError quitieren und die entsprechende Klasse nicht laden.
Auf bei Enums sollte keine toString() Methode erzeugt werden, da diese bereits definiert ist. Der Prozessor soll also Enums, Interfaces und Annotationen ignorieren. Im Bytecode ist hinterlegt, um welchen Typen es sich handelt.
toString() Methode vorhanden?
Wird im Bytecode eine toString() Methode mit der entsprechenden Signatur gefunden, dann muss entschieden werden was zu tun ist. Ein konservativer Prozessor soll die bestehende Methode nicht ersetzen. Dennoch soll er so zu konfigurieren sein, dass er die bestehende Methode entfernt und eine eigene Methode erzeugt.
Ist keine toString() Methode vorhanden, dann wird eine toString() Methode erzeugt. In einem solchen Fall kann davon ausgegangen werden, das keine toString() Methode im Quelltext vorhanden ist. Deswegen muss der Prozessor die zu erzeugende Methode als synthetisch kennzeichnen. Dies schreibt Kaptitel 4.7.6 der Java Virtual Machine Specification (Second Edition) vor.
Anders verhält es sich bei einer Ersetzung der toString() Methode. Dabei kann davon ausgegangen werden, dass die toString() Methode im Quellcode vorhanden ist.
Abschluss
Wenn ich nichts übersehen habe, dann kann die Implementierung jetzt starten.
