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)

8e25c0ca4bf25113bd9c0ccc5d118164?s=128

Gunnar Morling

March 19, 2019
Tweet

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. #JavaLand #AnnotationProcessing @gunnarmorling

  3. Agenda Agenda @gunnarmorling Use Cases Implementierung Best Practices Testen Beispiele

    #JavaLand #AnnotationProcessing
  4. 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 gunnar@hibernate.org @gunnarmorling http://in.relation.to/gunnar-morling/ @gunnarmorling #JavaLand #AnnotationProcessing
  5. 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
  6. 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
  7. 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/
  8. 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/
  9. Hibernate Hibernate Bean Validation Bean Validation @gunnarmorling #JavaLand #AnnotationProcessing

  10. 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/
  11. 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
  12. @gunnarmorling #JavaLand #AnnotationProcessing https://github.com/gradle/gradle/issues/5277

  13. Annotation Processing Annotation Processing Was geht nicht? Was geht nicht?

    Methodenimplementierungen inspizieren Bestehende Klassen verändern @gunnarmorling #JavaLand #AnnotationProcessing
  14. 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
  15. 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
  16. Annotation Processing Annotation Processing Konfiguration - Gradle Konfiguration - Gradle

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

  18. Annotation Processor Implementierung Annotation Processor Implementierung Drei Schritte Drei Schritte

    Annotationen definieren Prozessor implementieren Servicefile anlegen @gunnarmorling #JavaLand #AnnotationProcessing
  19. 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"; }
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. Annotation Processor Implementierung Annotation Processor Implementierung Processing Rounds Processing Rounds

    @GenBuilder public class Customer { // ... } @GenBuilder public class PurchaseOrder { // ... } @gunnarmorling #JavaLand #AnnotationProcessing
  31. 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 { // ... }
  32. 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 { // ... }
  33. 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
  34. 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
  35. 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
  36. 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
  37. 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
  38. Code-Generierung Code-Generierung

  39. 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
  40. 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
  41. Code-Generierung Code-Generierung Templates (z.B. FreeMarker) Templates (z.B. FreeMarker) @gunnarmorling #JavaLand

    #AnnotationProcessing
  42. 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
  43. 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
  44. 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
  45. 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
  46. 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
  47. 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
  48. Tests Tests

  49. 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
  50. 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
  51. 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
  52. 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
  53. 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
  54. Exkurs: Exkurs: Verwandte Konzepte Verwandte Konzepte

  55. 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
  56. 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
  57. Exkurs: Quarkus Exkurs: Quarkus "Compile-Time Boot" und Native Images "Compile-Time

    Boot" und Native Images @gunnarmorling #JavaLand #AnnotationProcessing
  58. Zusammenfassung Zusammenfassung

  59. 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
  60. Ressourcen Ressourcen Slides: JSR: Quarkus: @gunnarmorling http://speakerdeck.com/gunnarmorling https://jcp.org/en/jsr/detail?id=269 https://quarkus.io/ @gunnarmorling

    #JavaLand #AnnotationProcessing
  61. None