icancode.de

Eigene Java Annotations – Teil 1

Einleitung

Torsten T. Will

Torsten T. Will

Informatik, Biologie, Programmierung, Fotografie, C++, Python, Java.


Neuste Artikel

Datum in Java 11th November, 2015

Eigene Java Annotations – Teil 0 18th August, 2015

Java

Eigene Java Annotations – Teil 1

Veröffentlicht am .

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.

Torsten T. Will

Torsten T. Will

http://cpp11.generisch.de/

Informatik, Biologie, Programmierung, Fotografie, C++, Python, Java.

Navigation