1. Reflection, Annotationen und JavaBeans
Reflection bietet uns die Möglichkeit, in ein laufendes Java-Programm hineinzuschauen. Wir können eine Klasse fragen, welche Eigenschaften sie hat, und später auf beliebigen Objekten Methoden aufrufen und Objekt- bzw. Klassenvariablen auslesen und modifizieren. Viele Frameworks greifen auf die Reflection-API zurück, etwa JPA für das objektrelationale Mapping oder JAXB für die Abbildung von Java-Objekten auf XML-Bäume. Wir werden einige Beispiele selbst programmieren, die ohne Reflection nicht möglich wären.
Annotationen sind eine Art selbst programmierte Modifizierer. Mit ihnen können wir den Quellcode Metadaten anreichern, die später über Reflection oder ein anderes Werkzeug ausgelesen werden können. Oftmals sind wir nur Nutzer fremder Annotationen, in diesem Kapitel soll allerdings auch geübt werden, wie man selbst eigene Annotationstypen schreibt.
Voraussetzungen
Class
-Typ kennenTypbeziehungen zur Laufzeit auslesen können
Eigenschaften von Objekten zur Laufzeit ansprechen können
Annotationen auslesen können
neue Annotationstypen deklarieren können
Verwendete Datentypen in diesem Kapitel:
Noch mehr Aufgaben findest du im Buch: ›Captain CiaoCiao erobert Java: Das Trainingsbuch für besseres Java. 300 Java-Workshops, Aufgaben und Übungen mit kommentierten Lösungen‹
1.1. Reflection-API
Mit der Reflection-API lassen sich beliebige Objekte untersuchen und die folgenden Aufgaben nutzen das, um UML-Diagramme von beliebigen Datentypen zu erzeugen. Die Aufgaben konzentrieren sich auf realistische Einsatzfelder; man kann mit der Reflection-API auch viel Unsinn anstellen, etwa von immutablen Strings die Zeichen ändern, aber das ist dumm, und so etwas wollen wir nicht machen.
1.1.1. UML-Klassendiagramm mit Vererbungsbeziehungen erstellen ⭐
UML-Diagramme sind in der Dokumentation von Systemen sehr praktisch. Einige UML-Diagramme lassen sich auch durch Werkzeuge automatisch generieren. Wir wollen so ein Werkzeug selber schreiben. Ausgangspunkt ist dabei eine beliebige Klasse, die per Reflection untersucht wird. Wir können alle Eigenschaften dieser Klasse auslesen und ein UML-Diagramm generieren.
Da UML-Diagramme grafisch sind, stellt sich natürlich die Frage, wie wir in Java Grafiken zeichnen können. Dieses Problem wollen wir nicht lösen, sondern die Beschreibungssprache PlantUML (https://plantuml.com/) nutzen. PlantUML ist quasi das für UML-Diagramme, was HTML für Webseiten und SVG für Vektorgrafiken ist. Ein Beispiel:
interface Serializable <<interface>> Serializable <|-- ElectronicDevice ElectronicDevice <|.. Radio
Die Pfeile --|>
oder <|--
sind regulär dargestellt, ..|>
oder <|..
gestichelt.
PlantUML ist quelloffen und ermöglicht es, ein Kommandozeilenprogramm zu installieren, das textuelle Beschreibungen in grafische UML-Diagramme umwandelt. Alternativ können Websites wie https://www.planttext.com genutzt werden, um UML-Diagramme live darzustellen.
Aufgabe:
Generiere für eine beliebige Klasse, von der nur der voll qualifizierte Name gegeben ist, einen PlantUML-Diagramm-Text, und gib den Text auf der Konsole aus.
Das Diagramm soll den Typ und seine Basistypen (Oberklassen und implementierte Schnittstellen) zeigen.
Das Diagramm soll rekursiv auch die Typen der Oberklassen mit aufführen.
Beispiel:
Für
Class.forName("java.awt.Point")
könnte die Ausgabe so aussehen:Point2D <|-- Point Object <|-- Point2D interface Cloneable <<interface>> Cloneable <|.. Point2D interface Serializable <<interface>> Serializable <|.. Point hide members
1.1.2. UML-Klassendiagramm mit Eigenschaften erstellen ⭐
In PlantUML können nicht nur die Typbeziehungen — wie Vererbung, Implementierung von Schnittstellen und Assoziationen — beschrieben werden, sondern auch Objekt-/Klassenvariablen und Methoden:
class Radio { isOn: boolean isOn() : boolean {static} format(number: int): String }
Das Ergebnis wird etwa so aussehen:
Aufgabe:
Schreibe eine Methode, die ein beliebiges
Class
-Objekt bekommt und als Ergebnis einen mehrzeiligen String in der PlantUML-Syntax liefert.Es reicht, nur die Objekt-/Klassenvariablen, Konstruktoren und Methoden aufzunehmen, nicht die Typbeziehungen.
Beispiel:
Für den Typ
java.awt.Dimension
könnte die Ausgabe so aussehen:@startuml class Dimension { + width: int + height: int - serialVersionUID: long + Dimension(arg0: Dimension) + Dimension() + Dimension(arg0: int, arg1: int) + equals(arg0: Object): boolean + toString(): String + hashCode(): int + getSize(): Dimension - initIDs(): void + setSize(arg0: Dimension): void + setSize(arg0: double, arg1: double): void + setSize(arg0: int, arg1: int): void + getWidth(): double + getHeight(): double } @enduml
1.1.3. CSV-Dateien aus Listeneinträgen generieren ⭐⭐
In einer CSV-Datei werden die Einträge durch Komma oder Semikolon getrennt, das sieht etwa so aus:
1;2;3 4;5;6
Aufgabe:
Schreibe eine statische Methode
writeAsCsv(List<?> objects, Appendable out)
, die alle Objekte der Liste abläuft, per Reflection alle Informationen extrahiert und dann die Ergebnisse im CSV-Format in den übergebenen Ausgabestrom schreibt.Zum Extrahieren können wir entweder die öffentlichen Bean-Getter aufrufen (wenn wir über Properties gehen möchten) oder auf die (internen) Objektvariablen zugreifen — die Lösung kann eine der beiden Varianten nutzen.
Beispielnutzung:
Point p = new Point( 1, 2 );
Point q = new Point( 3, 4 );
List<?> list = Arrays.asList( p, q );
Writer out = new StringWriter();
writeAsCsv( list, out );
System.out.println( out );
Bonus: Nutzt man Zugriffe auf Objektvariablen, sollen die mit dem Modifizierer transient
markierten Objektvariablen nicht geschrieben werden.
1.2. Annotationen
Mit Annotationen können wir Metadaten in den Java-Code einbringen, die später — in der Regel über Reflection — ausgelesen werden können. Annotationen sind sehr wichtig geworden, denn vieles drücken Entwickler heute deklarativ aus und überlassen dem Framework die eigentliche Ausführung.
1.2.1. CSV-Dokumente aus annotierten Objektvariablen erzeugen ⭐⭐
Gegeben ist eine Klasse mit Annotationen:
@Csv
class Pirate {
@CsvColumn String name;
@CsvColumn String profession;
@CsvColumn int height;
@CsvColumn( format = "### €" ) double income;
@CsvColumn( format = "###.00" ) Object weight;
String secrets;
}
Aufgabe:
Deklariere die Annotation
@Csv
, die man nur an Typendeklarationen setzen kann.Deklariere die Annotation
@CsvColumn
, die man nur an Objektvariablen setzen kann.Erlaube bei
@CsvColumn
ein String-Attributformat
, für ein Pattern, das die Formatierung der Zahl über einDecimalFormat
-Muster steuert.Lege eine Klasse
CsvWriter
mit einem Konstruktor an, der sich einClass
-Objekt als Typ-Token merkt und auch einenWriter
, in den später die CSV-Zeilen geschrieben werden. Die KlasseCsvWriter
kannAutoCloseable
sein.Lege
CsvWriter
als generischen TypCsvWriter<T>
an.Schreibe zwei neue Methoden
void writeObject( T object )
: Schreibt ein Objektvoid write( Iterable<? extends T> iterable )
: Schreibt mehrere Objekte
Das Trennzeichen für die CSV-Spalten ist standardmäßig
';'
, soll aber über eine Methodedelimiter(char)
geändert werden können.Überlege, welche Fehlerfälle auftreten können und melde sie als ungeprüfte Ausnahme.
Beispielnutzung:
Pirate p1 = new Pirate();
p1.name = "Hotzenplotz";
p1.profession = null;
p1.height = 192;
p1.income = 124234.3234;
p1.weight = 89.10;
p1.secrets = "kinky";
StringWriter writer = new StringWriter();
try ( CsvWriter<Pirate> csvWriter =
new CsvWriter<>( Pirate.class, writer ).delimiter( ',' ) ) {
csvWriter.writeObject( p1 );
csvWriter.writeObject( p1 );
}
System.out.println( writer );
Lösung Csv, CsvColumn, ReflectionCsvExporter, CsvPirateWriter
1.3. Proxy-Objekte
Proxy Objekte spielen im Jahre Universum eine große Rolle, und die nächste Aufgabe soll zeige, wie Proxies nützlich eingesetzt werden können.
1.3.1. Webressourcen erfragen
Gegeben ist folgende Schnittstelle mit annotierten Methoden:
interface Wikipedia {
@Url( "https://www.wikipedia.org/" )
String homepage();
@Url( "https://en.wikipedia.org/wiki/Portal:Current_events" )
String news();
}
Aufgabe:
Nutze
java.lang.reflect.Proxy
und denjava.lang.reflect.InvocationHandler
, um von einer gegebenen Schnittstelle (wieWikipedia
) zur Laufzeit ein Proxy-Objekt zu generieren. Wenn eine Methode der Schnittstelle aufgerufen wird, soll die URL der Annotation ausgewertet, der Inhalt der Webseite geladen und zurückgegeben werden.Achte darauf, dass alle aufgerufen Methoden mit
Url
annotiert sein müssen.Wird
@Url
eingesetzt, sind Default-Methoden nicht zulässig, auch müssen die Methoden den RückgabetypString
besitzen.Es ist nicht verboten, wenn es statische oder
default
-Methoden in der Schnittstelle gibt, nur können sie dann über den Proxy nicht aufgerufen werden.
Beispiel:
Der Proxy-Generator ist hinter der statischen
UrlRessourceLoader
-Methodeget(…)
versteckt:System.out.println( UrlRessourceLoader.get( Wikipedia.class ).homepage() ); System.out.println( UrlRessourceLoader.get( Wikipedia.class ).news() );
Die Ausgabe könnte beginnen mit
<!DOCTYPE html> <html lang="en" class="no-js"> <head> <meta charset="utf-8"> <title>Wikipedia</title> …
Noch mehr Aufgaben findest du im Buch: ›Captain CiaoCiao erobert Java: Das Trainingsbuch für besseres Java. 300 Java-Workshops, Aufgaben und Übungen mit kommentierten Lösungen‹