Nachdem wir in Teil 0 das Warum geklärt haben, kommen wir nun zum ersten Wie.
Ich habe mir dafür die Annotation StepInfos
ausgedacht. Sie ist dazu gedacht, vor eine Methode geschrieben zu werden und Meta-Informationen zu der Methode bereitzustellen.
@StepInfos(stepid=10, description="Initialisiere Daten")
public void step_10() {
....
}
Als einfaches Beispiel möchte ich sicherstellen, dass der Programmierer hier wirklich stepid=10
passend zu step_10
schreibt, und nicht etwa 12, 666 oder etwas anders. Das möchte ich schon zur Übersetzungszeit sicherstellen, um Fehler so früh wie möglich auszuschließen. Hierzu bedienen wir uns eines Annotation Prozessors.
Der Hintergrund dazu ist, dass die Meta-Information stepid=10
auch zur Laufzeit benötigt werden könnte. Und damit die Information zur Laufzeit stimmt (mit dem Methodennamen übereinstimmt) benötigen wir diesen Check.
Die Annotationsklasse
Um überhaupt @StepInfos
im Code verwenden zu können, benötigen wir eine Klasse mit dem Namen „Stepinfos“ — wobei „Klasse“ nicht ganz richtig ist, denn anstatt class StepInfos
in Stepinfos.java
zu schreiben ist es public @interface StepInfos
:
package sample;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.COMPILE)
public @interface StepInfos {
int stepid();
String description();
}
Mit @interface
sagen wir, dass wir einen Annotation definieren. Die beiden vorstehenden Elemente sagen, dass diese an Methoden stehen darf und nur zur Übersetzungszeit benötigt wird. Dadurch kann der Compiler alle Informationen über die Annotation aus dem Compilat entfernen und es wird kein Speicherplatz zur Laufzeit verschwendet. Später werden wir hier allerdings RetentionPolicy.RUNTIME
schreiben, was die Annotation als Meta-Information im erzeugten Classfile behält.
Mit diesem File in unserem Projekt können wir nun die Annotation wie oben beschrieben verwenden.
Anntation Prozessor
Dem Compiler zu sagen, dass er diese Annotation überprüfen soll, ist in Java geschickt gelöst. Früher musste man einen extra-compile-Schritt dafür definieren, heute passiert alles auf einmal.
In Maven kann man zum Beispiel sagen, dass ein solcher Schritt ausgeführt werden soll. Das erledigt das allgegenwärtige Plugin maven-compiler-plugin
:
...
<build>
<finalName>sample</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessors>
<annotationProcessor>sample.StepInfosProcessor</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
...
Damit wird während der Compilerung die Klasse sample.StepInfosProcessor
ausgeführt. Und die kann alles mögliche machen — in unserem einfachen Beispiel einen Compilerfehler ausgeben:
package sample;
import java.util.Set;
import javax.annotation.processing.*;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
/** Dummy. Currently only as a demonstration how to create new classes during compilation. */
@SupportedAnnotationTypes({"sample.StepInfos"})
public class StepInfosProcessor extends AbstractProcessor {
// utils
Types types;
Elements elems;
@Override
public boolean process(Set elements, RoundEnvironment env) {
types = processingEnv.getTypeUtils();
elems = processingEnv.getElementUtils();
//processingEnv is a predefined member in AbstractProcessor class
//Messager allows the processor to output messages to the environment
Messager messager = processingEnv.getMessager();
for (TypeElement te : elements) {
//Get the members that are annotated with Option
for (Element e : env.getElementsAnnotatedWith(te)) { //Process the members. processAnnotation is our own method
processAnnotation(e, messager);
}
}
return true;
}
private static boolean not(boolean val) { return !val; }
private void processAnnotation(Element method, Messager msg) {
final StepInfos ann = method.getAnnotation(StepInfos.class);
// check basic properties
if (method.getKind() != ElementKind.METHOD) {
error("annotation only for methods", method);
}
Set modifiers = method.getModifiers();
if(not(modifiers.contains(Modifier.PUBLIC))) {
error("annotated Element must be public", method);
}
if(modifiers.contains(Modifier.STATIC)) {
error("annotated Element must not be static", method);
}
// check types
final ExecutableType emeth = (ExecutableType)method.asType();
if(not(emeth.getReturnType().getKind().equals(TypeKind.BOOLEAN))) {
error("annotated Element must have return type boolean", method);
}
if(emeth.getParameterTypes().size() != 1) {
error("annotated Element must have exactly one parameter", method);
} else {
final TypeMirror param0 = emeth.getParameterTypes().get(0);
final TypeMirror string = elems.getTypeElement(String.class.getCanonicalName()).asType();
final boolean isSame = types.isSameType(param0, string);
if(not(isSame)) {
error("annotated Element must have exactly one String parameter", method);
}
}
// check name
final String name = method.getSimpleName().toString();
if(not(name.matches("step_\\d{1,5}"))) {
error("annotated Element must have name of the form 'step_'", method);
return;
}
final int stepidFromName = Integer.parseInt(name.split("_")[1]);
if(stepidFromName != ann.stepid()) {
error("annotated Element must have same from 'step_' as from @StepInfos(stepid=)", method);
}
}
/** @param where will be used to present a position hint in the compiler message, if null its a position-less message */
void error(String msg, Element where) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, msg, where);
}
}
Entscheidend ist hier wiederum eine Annotation: @SupportedAnnotationTypes({"sample.StepInfos"})
sorgt dafür, dass diese Klasse zum passenden Zeitpunkt aufgerufen wird. Der Einstiegspunkt ist dann process(...)
.
Mit env.getElementsAnnotatedWith(te)
suchen wir alle interessanten Annotationen heraus. Die private Handlermethode schaut sich dann d das annotierte Element genau an. Hier prüfen wir die Anzahl Parameter der annotierten Methode und vieles andere.
Reflection in Schokolade
Das geht allerdings nicht mit der Standard-Reflection, die man sonst aus Java kennt. Der zu übersetzende Code hat noch keine wirklichen Klassen oder Instanzen erzeugt. Wir befinden uns ja noch in der Übersetzungsphase. Deswegen stehen uns ganz andere Informationen zur Verfügung — ähnliche, aber nicht identisch. Jetzt kommen sie nicht aus java.lang.reflect
, sondern aus javax.lang.model
. Ansonsten sind darin viele Dinge ähnlich; manchmal muss man es aber auch ganz anders machen, wie der Umgang mit TypeMirror
zeigt.
Fehlermeldungen
Habt ihr processingEnv.getMessager()
bemerkt? Der ist spannend. Denn der erlaubt uns nun, den Compiler zur Ausgabe einer Fehlermeldung zu bewegen. Mit printMessage(Diagnostic.Kind.ERROR, ...)
wird dies bewerkstelligt — inklusive der Information, wo das passiert ist.
Sowohl auf der Kommandozeile aus auch in Netbeans wird nun ein Fehler ausgegeben, wenn man zum Beispiel schreibt:
@StepInfos(stepid=666, description="bla")
public void step_10() {
....
}
Und zur Laufzeit?
Wenn wir nun wissen, dass die Information „stepid=“ zuverlässig mit dem Namen der Methode übereinstimmt, können wir auch zur Laufzeit die Information auswerten. Das sehen wir uns im [Teil 2] an.
Der Artikel Eigene Java Annotations – Teil 1 erschien zuerst auf icancode.de.