Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Das Annotation Processing API - Use Cases und Best Practices

Das Annotation Processing API - Use Cases und Best Practices

Bestandteil von Java seit Version 6, ist das Annotation Processing API immer noch eines der eher unbekannteren APIs der Plattform. Zu Unrecht, eröffnet die Einbindung von Annotationsprozessoren in den Java-Compiler doch eine Vielzahl interessanter Möglichkeiten, z.B. die Generierung von Value Objects und Builder-Klassen, Dependency Injection ohne Reflection zur Laufzeit oder die Erstellung typsicherer Bean-zu-Bean-Mapper.

Auch die Erstellung eigener, projektspezifischer Prozessoren ist nicht schwer; der Vortrag gibt einen grundlegenden Überblick über das API, vergleicht Ansätze zur Code-Generierung (APIs wie JavaPoet vs. Templates) und diskutiert Best Practices für Implementierung, Test und Verwendung von Annotationsprozessoren.

Abgerundet wird die Session mit einer Vorstellung verschiedener populärer Annotationsprozessoren wie Immutables, Dagger oder MapStruct.

Slides einer Session vom JavaLand 2019 (https://programm.javaland.eu/2019/#/scheduledEvent/575382)

Gunnar Morling

March 19, 2019
Tweet

More Decks by Gunnar Morling

Other Decks in Programming

Transcript

  1. Das Annotation Processing API - Use Das Annotation Processing API

    - Use Cases und Best Practices Cases und Best Practices Gunnar Morling Gunnar Morling @gunnarmorling @gunnarmorling
  2. Gunnar Morling Gunnar Morling Opensource-Softwareentwickler bei Red Hat Debezium (Talk

    am Mittwoch, 20.03., 15:00) Hibernate Spec Lead für Bean Validation 2.0 Gründer von MapStruct und Deptective [email protected] @gunnarmorling http://in.relation.to/gunnar-morling/ @gunnarmorling #JavaLand #AnnotationProcessing
  3. Annotation Processing Annotation Processing Überblick Überblick Plug-ins für den Compiler

    zur Verarbeitung von Annotationen Standardisiert in JSR 269 Prozessoren können Klassen inspizieren und neue Ressourcen erzeugen @gunnarmorling #JavaLand #AnnotationProcessing
  4. Annotation Processing Annotation Processing Use Cases Use Cases Korrektheitsprüfung bestehender

    Klassen (Ausgabe von "Diagnostics") Neue Klassen (oder Ressourcen) erzeugen Boilerplate vermeiden DRY Reflection vermeiden @gunnarmorling #JavaLand #AnnotationProcessing
  5. Immutables Immutables Unveränderliche Datentypen Unveränderliche Datentypen @gunnarmorling #JavaLand #AnnotationProcessing @Value.Immutable

    public interface ValueObject { String name(); List<Integer> counts(); Optional<String> description(); } ValueObject valueObject = ImmutableValueObject.builder() .name("My value") .addCounts(1) .addCounts(2) .build(); https://immutables.github.io/
  6. Hibernate Hibernate Statisches JPA-Metamodell Statisches JPA-Metamodell @gunnarmorling #JavaLand #AnnotationProcessing @Entity

    public class Order { @Id @GeneratedValue Integer id; @ManyToOne Customer customer; @OneToMany Set<Item> items; BigDecimal cost; ... } @StaticMetamodel(Order.class) public class Order_ { public static volatile SingularAttribute<Order, Integer> id; public static volatile SingularAttribute<Order, Customer> customer; public static volatile SetAttribute<Order, Item> items; public static volatile SingularAttribute<Order, BigDecimal> cost; } https://hibernate.org/
  7. MapStruct MapStruct Compile-time Bean Mappings Compile-time Bean Mappings @gunnarmorling #JavaLand

    #AnnotationProcessing @Mapper public interface CarMapper { @Mapping(source = "make", target = "manufacturer") @Mapping(source = "numberOfSeats", target = "seatCount") CarDto carToCarDto(Car car); } public class CarMapperImpl implements CarMapper { @Override public CarDto carToCarDto(Car car) { if ( car == null ) { return null; } CarDto carDto = new CarDto(); if ( car.getFeatures() != null ) { carDto.setFeatures( new ArrayList<String>( car.getFeatu } carDto.setManufacturer( car.getMake() ); carDto.setSeatCount( car.getNumberOfSeats() ); carDto.setDriver( personToPersonDto( car.getDriver() ) ); carDto.setPrice( String.valueOf( car.getPrice() ) ); if ( car.getCategory() != null ) { carDto.setCategory( car.getCategory().toString() ); } carDto.setEngine( engineToEngineDto( car.getEngine() ) ); return carDto; } } http://mapstruct.org/
  8. ap4k ap4k K8s und OpenShift Manifeste K8s und OpenShift Manifeste

    @gunnarmorling #JavaLand #AnnotationProcessing @KubernetesApplication public class Main { public static void main(String[] args) { //Your application code goes here. } } apiVersion: "apps/v1" kind: "Deployment" metadata: name: "kubernetes­example" spec: replicas: 1 selector: matchLabels: app: "my­gradle­app" version: "1.0­SNAPSHOT" group: "default" template: metadata: labels: app: "my­gradle­app" version: "1.0­SNAPSHOT" group: "default" spec: containers: https://github.com/ap4k/ap4k
  9. Annotation Processing Annotation Processing Was geht nicht? Was geht nicht?

    Methodenimplementierungen inspizieren Bestehende Klassen verändern @gunnarmorling #JavaLand #AnnotationProcessing
  10. Annotation Processing Annotation Processing Konfiguration - Maven Konfiguration - Maven

    @gunnarmorling <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven­compiler­plugin</artifactId> <configuration> <annotationProcessorPaths> <annotationProcessorPath> <groupId>org.mapstruct</groupId> <artifactId>mapstruct­processor</artifactId> <version>1.3.0.Final</version> </annotationProcessorPath> </annotationProcessorPaths> </configuration> </plugin> #JavaLand #AnnotationProcessing
  11. Annotation Processing Annotation Processing Konfiguration - Maven + Eclipse Konfiguration

    - Maven + Eclipse Automatische Ausführung von Prozessoren direkt in der IDE Konfiguration über m2e-apt ( ) https://marketplace.eclipse.org/content/m2e-apt @gunnarmorling <properties> <m2e.apt.activation>jdt_apt</m2e.apt.activation> </properties> #JavaLand #AnnotationProcessing
  12. Annotation Processing Annotation Processing Konfiguration - Gradle Konfiguration - Gradle

    @gunnarmorling plugins { id 'java' } ... dependencies { annotationProcessor 'org.mapstruct:mapstruct­processor:1.3.0.Final' } #JavaLand #AnnotationProcessing
  13. Annotation Processor Implementierung Annotation Processor Implementierung Drei Schritte Drei Schritte

    Annotationen definieren Prozessor implementieren Servicefile anlegen @gunnarmorling #JavaLand #AnnotationProcessing
  14. Annotation Processor Implementierung Annotation Processor Implementierung Schritt 1 - Schritt

    1 - Annotationen definieren Annotationen definieren @gunnarmorling #JavaLand #AnnotationProcessing @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface GenBuilder { String nameSuffix() default "Builder"; }
  15. Annotation Processor Implementierung Annotation Processor Implementierung Schritt 1 - Schritt

    1 - Annotationen definieren Annotationen definieren @gunnarmorling @Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.CLASS) public @interface GenBuilder { String nameSuffix() default "Builder"; } @GenBuilder(nameSuffix="Creator") public class Customer { // ... } #JavaLand #AnnotationProcessing
  16. Annotation Processor Implementierung Annotation Processor Implementierung Schritt 2 - Schritt

    2 - Prozessor implementieren Prozessor implementieren @gunnarmorling @SupportedOptions({ "com.example.some­option", "com.example.another­option" }) @SupportedAnnotationTypes("com.example.GenBuilder") public class BuilderGenerationProcessor extends AbstractProcessor { // ... } #JavaLand #AnnotationProcessing
  17. Annotation Processor Implementierung Annotation Processor Implementierung Schritt 2 - Schritt

    2 - Prozessor implementieren Prozessor implementieren @gunnarmorling @SupportedOptions({ "com.example.some­option", "com.example.another­option" }) @SupportedAnnotationTypes("com.example.GenBuilder") public class BuilderGenerationProcessor extends AbstractProcessor { // ... } #JavaLand #AnnotationProcessing
  18. Annotation Processor Implementierung Annotation Processor Implementierung Schritt 2 - Schritt

    2 - Prozessor implementieren Prozessor implementieren @gunnarmorling @SupportedOptions({ "com.example.some­option", "com.example.another­option" }) @SupportedAnnotationTypes("com.example.GenBuilder") public class BuilderGenerationProcessor extends AbstractProcessor { // ... } #JavaLand #AnnotationProcessing
  19. Annotation Processor Implementierung Annotation Processor Implementierung Schritt 2 - Schritt

    2 - Prozessor implementieren Prozessor implementieren @gunnarmorling public class BuilderGenerationProcessor extends AbstractProcessor { private Filer filer; private Message messager; public synchronized void init(ProcessingEnvironment pe) { super.init(processingEnv); this.filer = filer; this.messager = messager; } // ... } #JavaLand #AnnotationProcessing Dateien erzeugen Diagnostics ausgeben
  20. Annotation Processor Implementierung Annotation Processor Implementierung Schritt 2 - Schritt

    2 - Prozessor implementieren Prozessor implementieren @gunnarmorling public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (TypeElement annotation : annotations) { Set<? extends Element> annotated = roundEnv.getElementsAnnotatedWith( annotation ); // processing ... } return false; } #JavaLand #AnnotationProcessing
  21. Annotation Processor Implementierung Annotation Processor Implementierung Schritt 3 - Servicefile

    anlegen Schritt 3 - Servicefile anlegen @gunnarmorling builder­processor ├── pom.xml ├── src │ └── main │ ├── java │ │ └── com │ │ └── example │ │ └── builder │ │ └── generator │ │ └── BuilderGenerationProcessor.java │ └── resources │ └── META­INF │ └── services │ └── javax.annotation.processing.Processor #JavaLand #AnnotationProcessing
  22. Annotation Processor Implementierung Annotation Processor Implementierung Schritt 3 - Servicefile

    anlegen Schritt 3 - Servicefile anlegen @gunnarmorling builder­processor ├── pom.xml ├── src │ └── main │ ├── java │ │ └── com │ │ └── example │ │ └── builder │ │ └── generator │ │ └── BuilderGenerationProcessor.java │ └── resources │ └── META­INF │ └── services │ └── javax.annotation.processing.Processor com.example.builder.generator.BuilderGenerationProcessor #JavaLand #AnnotationProcessing
  23. Annotation Processor Implementierung Annotation Processor Implementierung Schritt 3 - Servicefile

    anlegen (Alternative) Schritt 3 - Servicefile anlegen (Alternative) Via Annotation Processor :-) Google's AutoService ( ) https://github.com/google/auto @gunnarmorling @AutoService(Processor.class) public class BuilderGenerationProcessor extends AbstractProcessor { // ... } #JavaLand #AnnotationProcessing
  24. Annotation Processor Implementierung Annotation Processor Implementierung Processing Rounds Processing Rounds

    Verarbeitung geschieht in Runden: Teilmenge von annotierten Elementen Neue Source-Dateien Neue Runde Letzte Runde: Generierung aggregierter Ressourcen Hilfreich zur Analyse: ­XprintRounds , ­XprintProcessorInfo @gunnarmorling #JavaLand #AnnotationProcessing Quelle: https://openjdk.java.net/groups/compiler/doc/compilation-overview/index.html
  25. Annotation Processor Implementierung Annotation Processor Implementierung Processing Rounds Processing Rounds

    @GenBuilder public class Customer { // ... } @GenBuilder public class PurchaseOrder { // ... } @gunnarmorling #JavaLand #AnnotationProcessing
  26. Annotation Processor Implementierung Annotation Processor Implementierung Processing Rounds Processing Rounds

    @gunnarmorling @GenBuilder public class Customer { // ... } BuilderGenerationProcessor (@GenBuilder) GenStatisticsProcessor (@Generated) #JavaLand #AnnotationProcessing @GenBuilder public class PurchaseOrder { // ... }
  27. Annotation Processor Implementierung Annotation Processor Implementierung Processing Rounds Processing Rounds

    @gunnarmorling @GenBuilder public class Customer { // ... } BuilderGenerationProcessor (@GenBuilder) GenStatisticsProcessor (@Generated) Round 1: input files: {d.m.a.usage.Customer, d.m.a.usage.PurchaseOrder} annotations: [d.m.a.annotations.GenBuilder] last round: false Processor d.m.a.builder.BuilderGenerationProcessor matches [/d.m.a.annotations.GenBuilder] and returns false. #JavaLand #AnnotationProcessing @GenBuilder public class PurchaseOrder { // ... }
  28. Annotation Processor Implementierung Annotation Processor Implementierung Processing Rounds Processing Rounds

    @gunnarmorling @GenBuilder public class Customer { // ... } @GenBuilder public class PurchaseOrder { // ... } BuilderGenerationProcessor (@GenBuilder) GenStatisticsProcessor (@Generated) Round 1: input files: {d.m.a.usage.Customer, d.m.a.usage.PurchaseOrder} annotations: [d.m.a.annotations.GenBuilder] last round: false Processor d.m.a.builder.BuilderGenerationProcessor matches [/d.m.a.annotations.GenBuilder] and returns false. @Generated("BuilderGenerationProcessor") public class CustomerBuilder { // ... } @Generated("BuilderGenerationProcessor") public class PurchaseOrderBuilder { // ... } #JavaLand #AnnotationProcessing
  29. Annotation Processor Implementierung Annotation Processor Implementierung Processing Rounds Processing Rounds

    @gunnarmorling @GenBuilder public class Customer { // ... } @GenBuilder public class PurchaseOrder { // ... } BuilderGenerationProcessor (@GenBuilder) GenStatisticsProcessor (@Generated) Round 2: input files: {d.m.a.usage.CustomerBuilder, d.m.a.usage.PurchaseOrderB annotations: [j.a.p.Generated] last round: false BuilderGenerationProcessor#process() Processor d.m.a.builder.BuilderGenerationProcessor matches [] and returns false. StatsGenerationProcessor#process() Processor d.m.a.stats.StatsGenerationProcessor matches [java.compiler/j.a.p.Generated] and returns false. @Generated("BuilderGenerationProcessor") public class CustomerBuilder { // ... } @Generated("BuilderGenerationProcessor") public class PurchaseOrderBuilder { // ... } #JavaLand #AnnotationProcessing
  30. Annotation Processor Implementierung Annotation Processor Implementierung Processing Rounds Processing Rounds

    @gunnarmorling @GenBuilder public class Customer { // ... } @GenBuilder public class PurchaseOrder { // ... } BuilderGenerationProcessor (@GenBuilder) GenStatisticsProcessor (@Generated) Round 3: input files: {} annotations: [] last round: true @Generated("BuilderGenerationProcessor") public class CustomerBuilder { // ... } @Generated("BuilderGenerationProcessor") public class PurchaseOrderBuilder { // ... } #JavaLand #AnnotationProcessing
  31. Annotation Processor Implementierung Annotation Processor Implementierung Processing Rounds Processing Rounds

    @gunnarmorling @GenBuilder public class Customer { // ... } @GenBuilder public class PurchaseOrder { // ... } BuilderGenerationProcessor (@GenBuilder) GenStatisticsProcessor (@Generated) Round 3: input files: {} annotations: [] last round: true @Generated("BuilderGenerationProcessor") public class CustomerBuilder { // ... } @Generated("BuilderGenerationProcessor") public class PurchaseOrderBuilder { // ... } public class GeneratedStats { public static final int generatedCount = 2; } #JavaLand #AnnotationProcessing
  32. Annotation Processor Implementierung Annotation Processor Implementierung Klassen vs. Types vs.

    Elements Klassen vs. Types vs. Elements Kein Zugriff auf kompilierte Klassen per Reflection Elements: Elemente eines Java-Programms Package: PackageElement Klasse: TypeElement Methode: ExecutableElement Types: Java-Typen z.B. java.util.Set, java.util.Set<?>, java.util.Set<String> @gunnarmorling #JavaLand #AnnotationProcessing
  33. Code-Generierung Code-Generierung Writer Writer @gunnarmorling PackageElement packageElement = procEnv.getElementUtils().getPackageOf( element

    ); Name name = element.getSimpleName(); String builderName = name.toString() + "Builder"; JavaFileObject builderFile = processingEnv.getFiler().createSourceFile( packageElement.getQualifiedName() + "." + builderName ); Writer writer = builderFile.openWriter(); writer.append( "package " + packageElement.getQualifiedName() + ";\n" ); writer.append( "public class " + builderName + " {\n" ); // ... writer.append( "}" ); #JavaLand #AnnotationProcessing
  34. Code-Generierung Code-Generierung Writer Writer @gunnarmorling PackageElement packageElement = procEnv.getElementUtils().getPackageOf( element

    ); Name name = element.getSimpleName(); String builderName = name.toString() + "Builder"; JavaFileObject builderFile = processingEnv.getFiler().createSourceFile( packageElement.getQualifiedName() + "." + builderName ); Writer writer = builderFile.openWriter(); writer.append( "package " + packageElement.getQualifiedName() + ";\n" ); writer.append( "public class " + builderName + " {\n" ); // ... writer.append( "}" ); #JavaLand #AnnotationProcessing
  35. Code-Generierung Code-Generierung APIs (z.B. JavaPoet, JavaParser) APIs (z.B. JavaPoet, JavaParser)

    @gunnarmorling MethodSpec main = MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") .build(); public static void main(String[] args) { System.out.println("Hello, JavaPoet!"); } #JavaLand #AnnotationProcessing https://github.com/square/javapoet
  36. Code-Generierung Code-Generierung APIs (z.B. JavaPoet, JavaParser) APIs (z.B. JavaPoet, JavaParser)

    @gunnarmorling MethodSpec main = MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); public final class HelloWorld { public static void main(String[] args) { System.out.println("Hello, JavaPoet!"); } } #JavaLand #AnnotationProcessing https://github.com/square/javapoet
  37. Code-Generierung Code-Generierung APIs (z.B. JavaPoet, JavaParser) APIs (z.B. JavaPoet, JavaParser)

    @gunnarmorling MethodSpec main = MethodSpec.methodBuilder("main") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(String[].class, "args") .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!") .build(); TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld") .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(main) .build(); JavaFile javaFile = JavaFile.builder( "com.example.helloworld", helloWorld).build(); package com.example.helloworld; public final class HelloWorld { public static void main(String[] args) { System.out.println("Hello, JavaPoet!"); } } #JavaLand #AnnotationProcessing https://github.com/square/javapoet
  38. Code-Generierung Code-Generierung Empfehlungen Empfehlungen APIs gut geeignet für bedingte Logiken

    (Methodenimplementierungen) Templates gut geeignet für statische Strukturen (Klassenrümpfe) Modell aufbauen, anreichern und ausgeben z.B. MapStruct: "Mapper", "MappingMethod" etc. @gunnarmorling #JavaLand #AnnotationProcessing
  39. Annotation Processor Implementierung Annotation Processor Implementierung Best Practices 1/2 Best

    Practices 1/2 Separate JARs für Annotationen und Prozessor SupportedAnnotationTypes(*) vermeiden ERROR-Diagnostics statt Exceptions @gunnarmorling #JavaLand #AnnotationProcessing
  40. Annotation Processor Implementierung Annotation Processor Implementierung Best Practices 2/2 Best

    Practices 2/2 " Clean Code" generieren Korrekte Einrückung Imports Java 9+: @javax.annotation.processing.Generated statt @javax.annotation.Generated @gunnarmorling #JavaLand #AnnotationProcessing
  41. Annotation Processor Tests Annotation Processor Tests Verschiedene Ansätze Verschiedene Ansätze

    Integrationstests Separates Projekt Maven Verifier JSR 199 Google Compile Testing @gunnarmorling #JavaLand #AnnotationProcessing
  42. Annotation Processor Tests Annotation Processor Tests Google Compile Testing Google

    Compile Testing @gunnarmorling // compile Compilation compilation = javac() .withProcessors(new MyAnnotationProcessor()) .compile(JavaFileObjects.forResource("HelloWorld.java")); #JavaLand #AnnotationProcessing
  43. Annotation Processor Tests Annotation Processor Tests Google Compile Testing Google

    Compile Testing @gunnarmorling // compile Compilation compilation = javac() .withProcessors(new MyAnnotationProcessor()) .compile(JavaFileObjects.forResource("HelloWorld.java")); // assert diagnostic assertThat(compilation).hadErrorContaining("No types named HelloWorld!") .inFile(helloWorld).onLine(23).atColumn(5); #JavaLand #AnnotationProcessing
  44. Annotation Processor Tests Annotation Processor Tests Google Compile Testing Google

    Compile Testing @gunnarmorling // compile Compilation compilation = javac() .withProcessors(new MyAnnotationProcessor()) .compile(JavaFileObjects.forResource("HelloWorld.java")); // assert generated code assertThat(compilation).succeeded(); assertThat(compilation).generatedSourceFile("GeneratedHelloWorld") .hasSourceEquivalentTo(JavaFileObjects.forResource("GeneratedHelloWorld.java")); #JavaLand #AnnotationProcessing
  45. Annotation Processor Tests Annotation Processor Tests Best Practices Best Practices

    javac und ecj testen Input-Sources aus dem Projekt laden Generierten Code ausführen @gunnarmorling URL classesDir = getClass().getProtectionDomain().getCodeSource().getLocation(); Path projectDir = Paths.get(classesDir.toURI()).getParent().getParent(); URL resource = projectDir.resolve("src/test/java") .resolve(clazz.getName().replace(".", File.separator) + ".java") .toUri().toURL(); #JavaLand #AnnotationProcessing
  46. Exkurs: javac Plug-in-API Exkurs: javac Plug-in-API Zugriff auf den kompletten

    AST Zugriff auf den kompletten AST Inklusive Methodenimplementierungen Beispiele: Google ErrorProne Deptective @gunnarmorling #JavaLand #AnnotationProcessing
  47. Exkurs: javac Plug-in-API Exkurs: javac Plug-in-API Deptective - Architekturvalidierung zur

    Compiler-Zeit Deptective - Architekturvalidierung zur Compiler-Zeit @gunnarmorling #JavaLand #AnnotationProcessing https://github.com/moditect/deptective
  48. Exkurs: Quarkus Exkurs: Quarkus "Compile-Time Boot" und Native Images "Compile-Time

    Boot" und Native Images @gunnarmorling #JavaLand #AnnotationProcessing
  49. Annotation Processing Annotation Processing Zusammenfassung Zusammenfassung Annotationsprozessoren: Plug-ins für den

    Java-Compiler Vielfältige Anwendungsfälle Validierung Code-Generierung Demnächst auch in Eurem Projekt? @gunnarmorling #JavaLand #AnnotationProcessing