Java Annotation Processors – Konfigurationsdateien generieren
Im 1.Teil der Blogartikel-Serie zu Java Annotation Prozessoren haben wir erfahren, wie man einen einfachen Annotation Prozessor schreibt, registriert und nutzt. Nun widmen wir uns der Frage, wie man Konfigurationsdateien generieren kann.
Konfigurationsdateien generieren
Im zweiten Abschnitt wollen wir uns dem Erzeugen von Konfigurationsdateien für eine einfache Plugin Bibliothek widmen. Dafür werden wir einen Annotation Prozessor schreiben, der alle Klassen, die mit einer @Extension
versehen wurden, in eine XML-Datei schreibt. Zudem vollständigen Namen der Klasse soll außerdem noch das Javadoc der Klasse mit in die XML-Datei geschrieben werden. Zusätzlich werden wir eine Klasse schreiben die es uns erlaubt diese Dateien aus dem Classpath auszulesen.
Es ist auch möglich alle Klassen mit einer @Extension
Annotation zu finden, ohne einen Annotation Prozessor zu verwenden. Dafür müsste man aber alle Elemente des Classpath (Ordner und Jar-Dateien) öffnen, jede Klasse laden und mit Reflection
überprüfen, ob die Klasse die gesuchte Annotation hat. Dieses Vorgehen ist sehr viel aufwändiger, anfälliger für Fehler und deutlich langsamer.
Die Extension Annotation
@Documented
@Target(ElementType.TYPE)
public @interface Extension {
}
Die Extension Annotation ähnelt sehr der Log-Annotation aus dem ersten Abschnitt, mit Ausnahme der Documented
Annotation. @Documented
sorgt dafür, dass unsere Annotation im Javadoc der annotierten Klasse auftaucht.
Der Extension Annotation Prozessor
Der ExtensionProcessor
sammelt zuerst alle Klassen die mit unserer Extension Annotation versehen wurden in einem Set:
Set<ExtensionDescriptor> descriptors = new LinkedHashSet<>();
for ( TypeElement annotation : annotations ) {
for ( Element extension : roundEnv.getElementsAnnotatedWith(annotation) ) {
ExtensionDescriptor descriptor = createDescriptor(extension);
descriptors.add(descriptor);
}
}
Die createDescriptor
Methode speichert dabei den Namen und das Javadoc der annotierten Klasse in einer eigenen Klasse namens ExtensionDescriptor
. Den Namen kann man über den Typ des Elementes erfragen:
extension.asType().toString()
Das JavaDoc der Klasse kann man über Elements
des ProcessingEnvironments
erfragen:
processingEnv.getElementUtils().getDocComment(extension).trim()
Nachdem wir alle Extensions gesammelt haben, können wir unsere XML-Datei schreiben. Damit unsere XML-Datei im Classpath verfügbar ist, muss sie in das richtige Verzeichnis geschrieben werden. Das Verzeichnis lässt sich über die Filer
Klasse des ProcessingEnvironment
herausfinden:
Filer filer = processingEnv.getFiler();
FileObject fileObject = filer.getResource(StandardLocation.CLASS_OUTPUT, "", "extensions.xml");
File extensionsFile = new File(fileObject.toUri());
Jetzt müssen wir die Extensions Datei nur noch mit Inhalt füllen. Dafür erstellen wir eine Wrapper
Klasse für unsere ExtensionDescriptor
Klasse und annotieren beide mit JAXB Annotationen. Anschließend können wir die Extensions-Datei mit Hilfe von JAXB schreiben:
JAXB.marshal(new ExtensionDescriptorWrapper(descriptors), file);
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<spl-extensions>
<extensions>
<className>com.cloudogu.blog.AhoiService</className>
<description>Says ahoi to someone.</description>
</extensions>
<extensions>
<className>com.cloudogu.blog.HelloService</className>
<description>Says hello to someone.</description>
</extensions>
</spl-extensions>
Diese Datei sollte sich im selben Verzeichnis wie die kompilierten Klassen befinden (bei Maven target/classes
).
Extensions Util
Um die Extensions zur Laufzeit wieder auszulesen, können wir uns leicht eine Hilfsklasse schreiben:
public static List<ExtensionDescriptor> getExtensions() throws IOException {
List<ExtensionDescriptor> descriptors = new ArrayList<>();
Enumeration<URL> extensionFiles = Thread.currentThread().getContextClassLoader().getResources(LOCATION);
while (extensionFiles.hasMoreElements()) {
URL extensionFile = extensionFiles.nextElement();
ExtensionDescriptorWrapper extensionDescriptorWrapper = JAXB.unmarshal(extensionFile, ExtensionDescriptorWrapper.class);
descriptors.addAll(extensionDescriptorWrapper.getExtensions());
}
return descriptors;
}
Mit dieser Methode werden alle Extension XML-Dateien im Classpath gefunden. Außerdem werden in einer Liste alle Klassen gespeichert, die mit einer Extension Annotation annotiert wurden. Da wir den ContextClassLoader
des Threads verwenden, können sich unsere Extensions XML-Dateien sogar in verschiedenen JAR-Dateien befinden.
Wenn wir jetzt alle Extension Klassen unserer Anwendung ausgeben wollen, können wir folgenden Code verwenden:
for (ExtensionDescriptor descriptor : Extensions.getExtensions()) {
System.out.println(descriptor);
}
Das gesamte Beispiel kann unter part-2 des GitHub Repositories gefunden werden.
Beispiele aus der Open Source Welt
Ein prominentes Beispiel für einen Annotation Prozessor der Konfigurationsdateien generiert, ist der META-INF/services generator von Kohsuke Kawaguchi, der aus einer MetaInfServices
Annotation die Konfiguration für den Java 6 ServiceLoader
erzeugen kann.
Ein weiteres Beispiel ist das Plugin Framework von SCM-Manager 2.0.0. SCM-Manager hat in Version 1 noch Classpath Scanning verwendet um die Erweiterungen zu finden. Durch den Umstieg auf Annotation Prozessoren konnte die Startzeit von SCM-Manager 2 drastisch verkürzt werden.
Besuchen Sie unsere Community-Plattform, um Ihre Ideen zu teilen, Ressourcen herunterzuladen und auf unsere Schulungen zuzugreifen.
Jetzt mitmachenFazit
In diesem zweiten Blogbeitrag haben wir uns auf das Erstellen von Konfigurationsdateien sowie die Erweiterung des Annotationsprozessors konzentriert. Im dritten und letzten Teil zeigen wir, wie Code mit Annotation Prozessoren generiert werden kann.