Java-annotatieverwerking en het maken van een builder

1. Inleiding

Dit artikel is een inleiding tot annotatieverwerking op bronniveau op Java en geeft voorbeelden van het gebruik van deze techniek voor het genereren van extra bronbestanden tijdens het compileren.

2. Toepassingen van annotatieverwerking

De annotatieverwerking op bronniveau verscheen voor het eerst in Java 5. Het is een handige techniek voor het genereren van extra bronbestanden tijdens de compilatiefase.

De bronbestanden hoeven geen Java-bestanden te zijn - u kunt elke soort beschrijving, metagegevens, documentatie, bronnen of elk ander type bestanden genereren op basis van annotaties in uw broncode.

Annotatieverwerking wordt actief gebruikt in veel alomtegenwoordige Java-bibliotheken, bijvoorbeeld om metaklassen te genereren in QueryDSL en JPA, om klassen uit te breiden met standaardcode in de Lombok-bibliotheek.

Een belangrijk ding om op te merken is de beperking van de annotatieverwerkings-API - deze kan alleen worden gebruikt om nieuwe bestanden te genereren, niet om bestaande te wijzigen.

De opmerkelijke uitzondering is de Lombok-bibliotheek die annotatieverwerking gebruikt als een bootstrapping-mechanisme om zichzelf op te nemen in het compilatieproces en de AST aan te passen via enkele interne compiler-API's. Deze hacky-techniek heeft niets te maken met het beoogde doel van annotatieverwerking en wordt daarom in dit artikel niet besproken.

3. API voor het verwerken van annotaties

De annotatieverwerking gebeurt in meerdere rondes. Elke ronde begint met het zoeken door de compiler naar de annotaties in de bronbestanden en het kiezen van de annotatieprocessors die geschikt zijn voor deze annotaties. Elke annotatieprocessor wordt op zijn beurt aangeroepen op de overeenkomstige bronnen.

Als er tijdens dit proces bestanden worden gegenereerd, wordt een nieuwe ronde gestart met de gegenereerde bestanden als invoer. Dit proces gaat door totdat er tijdens de verwerkingsfase geen nieuwe bestanden worden gegenereerd.

Elke annotatieprocessor wordt op zijn beurt aangeroepen op de overeenkomstige bronnen. Als er tijdens dit proces bestanden worden gegenereerd, wordt een nieuwe ronde gestart met de gegenereerde bestanden als invoer. Dit proces gaat door totdat er tijdens de verwerkingsfase geen nieuwe bestanden worden gegenereerd.

De API voor het verwerken van annotaties bevindt zich in het javax.annotation.processing pakket. De belangrijkste interface die u moet implementeren, is de Verwerker interface, die een gedeeltelijke implementatie heeft in de vorm van SamenvattingProcessor klasse. Deze klasse gaan we uitbreiden om onze eigen annotatieprocessor te maken.

4. Het opzetten van het project

Om de mogelijkheden van annotatieverwerking te demonstreren, zullen we een eenvoudige processor ontwikkelen voor het genereren van vloeiende objectbuilders voor geannoteerde klassen.

We gaan ons project opsplitsen in twee Maven-modules. Een van hen, annotatie-processor module, bevat de processor zelf samen met de annotatie, en een ander, de annotatie-gebruiker module, bevat de geannoteerde klasse. Dit is een typisch geval van annotatieverwerking.

De instellingen voor het annotatie-processor module zijn als volgt. We gaan de automatische servicebibliotheek van Google gebruiken om metadatabestanden van de processor te genereren, die later zullen worden besproken, en de maven-compiler-plugin afgestemd op de Java 8-broncode. De versies van deze afhankelijkheden worden geëxtraheerd naar het eigenschappengedeelte.

De nieuwste versies van de auto-service-bibliotheek en de maven-compiler-plug-in zijn te vinden in de Maven Central-repository:

 1.0-rc2 3.5.1 com.google.auto.service auto-service $ {auto-service.version} verstrekt org.apache.maven.plugins maven-compiler-plugin $ {maven-compiler-plugin.version} 1.8 1.8 

De annotatie-gebruiker Maven-module met de geannoteerde bronnen heeft geen speciale afstemming nodig, behalve het toevoegen van een afhankelijkheid van de annotatie-processormodule in de afhankelijkheden-sectie:

 com.baeldung annotatieverwerking 1.0.0-SNAPSHOT 

5. Een aantekening definiëren

Stel dat we een eenvoudige POJO-les hebben in onze annotatie-gebruiker module met verschillende velden:

openbare klasse Persoon {privé int. leeftijd; private String naam; // getters en setters…}

We willen een bouwer-helper-klasse maken om het Persoon les vloeiender:

Persoon person = nieuwe PersonBuilder () .setAge (25) .setName ("John") .build ();

Dit PersonBuilder class is een voor de hand liggende keuze voor een generatie, aangezien de structuur volledig wordt bepaald door de Persoon setter methoden.

Laten we een @BuilderProperty annotatie in het annotatie-processor module voor de setter-methoden. Hiermee kunnen we het Bouwer class voor elke klasse waarvan de setter-methoden zijn geannoteerd:

@Target (ElementType.METHOD) @Retention (RetentionPolicy.SOURCE) openbaar @interface BuilderProperty {}

De @Doelwit annotatie met de ElementType.METHOD parameter zorgt ervoor dat deze annotatie alleen op een methode kan worden geplaatst.

De BRON Bewaarbeleid houdt in dat deze annotatie alleen beschikbaar is tijdens bronverwerking en niet tijdens runtime.

De Persoon klasse met eigenschappen geannoteerd met de @BuilderProperty annotatie ziet er als volgt uit:

openbare klasse Persoon {privé int. leeftijd; private String naam; @BuilderProperty public void setAge (int age) {this.age = age; } @BuilderProperty public void setName (String naam) {this.name = name; } // getters…}

6. Implementeren van een Verwerker

6.1. Een SamenvattingProcessor Subklasse

We beginnen met het uitbreiden van het SamenvattingProcessor klasse binnen de annotatie-processor Maven-module.

Ten eerste moeten we annotaties specificeren die deze processor kan verwerken, en ook de ondersteunde broncodeversie. Dit kan worden gedaan door de methoden te implementeren getSupportedAnnotationTypes en getSupportedSourceVersion van de Verwerker interface of door uw klas te annoteren met @SupportedAnnotationTypes en @SupportedSourceVersion annotaties.

De @AutoService annotatie is een onderdeel van de auto-service bibliotheek en maakt het mogelijk om de metadata van de processor te genereren die in de volgende secties zullen worden uitgelegd.

@SupportedAnnotationTypes ("com.baeldung.annotation.processor.BuilderProperty") @SupportedSourceVersion (SourceVersion.RELEASE_8) @AutoService (Processor.class) public class BuilderProcessor breidt AbstractProcessor uit {@Override public boolean process ; }}

U kunt niet alleen de namen van de concrete annotatieklassen specificeren, maar ook jokertekens, zoals "Com.baeldung.annotation. *" om annotaties in het com.baeldung.annotation pakket en al zijn subpakketten, of zelfs “*” om alle annotaties te verwerken.

De enige methode die we moeten implementeren, is de werkwijze methode die de verwerking zelf doet. Het wordt door de compiler aangeroepen voor elk bronbestand dat de overeenkomende annotaties bevat.

Aantekeningen worden als eerste doorgegeven Stel annotaties in argument, en de informatie over de huidige verwerkingsronde wordt doorgegeven als de RoundEnviroment roundEnv argument.

De terugkeer boolean waarde zou moeten zijn waar als uw annotatieprocessor alle doorgegeven annotaties heeft verwerkt en u niet wilt dat ze worden doorgegeven aan andere annotatieprocessors verderop in de lijst.

6.2. Informatie verzamelen

Onze processor doet nog niet echt iets nuttigs, dus laten we hem vullen met code.

Eerst moeten we alle annotatietypen die in de klas worden gevonden, doorlopen - in ons geval de annotaties set heeft één element dat overeenkomt met de @BuilderProperty annotatie, zelfs als deze annotatie meerdere keren voorkomt in het bronbestand.

Toch is het beter om het werkwijze methode als een iteratiecyclus, voor de volledigheid:

@Override openbaar booleaans proces (Annotaties instellen, RoundEnvironment roundEnv) {voor (TypeElement annotatie: annotaties) {Set annotatedElements = roundEnv.getElementsAnnotatedWith (annotatie); //…} retourneert waar; }

In deze code gebruiken we de Ronde omgeving instantie om alle elementen te ontvangen die zijn geannoteerd met de @BuilderProperty annotatie. In het geval van de Persoon klasse, deze elementen komen overeen met de setName en setAge methoden.

@BuilderProperty De gebruiker van annotatie kan ten onrechte methoden annoteren die niet echt setters zijn. De naam van de setter-methode moet beginnen met set, en de methode zou een enkel argument moeten krijgen. Dus laten we het kaf van het koren scheiden.

In de volgende code gebruiken we de Collectors.partitioningBy () verzamelaar om geannoteerde methoden op te splitsen in twee verzamelingen: correct geannoteerde setters en andere foutief geannoteerde methoden:

Kaart annotatedMethods = annotatedElements.stream (). collect (Collectors.partitioningBy (element -> ((ExecutableType) element.asType ()). getParameterTypes (). size () == 1 && element.getSimpleName (). toString (). startsWith ("set"))); Lijst setters = annotatedMethods.get (true); Lijst otherMethods = annotatedMethods.get (false);

Hier gebruiken we de Element.asType () methode om een ​​exemplaar van de Typ Mirror klasse die ons enige mogelijkheid geeft om typen te onderzoeken, ook al zijn we nog maar in de bronverwerkingsfase.

We moeten de gebruiker waarschuwen voor onjuist geannoteerde methoden, dus laten we de Messager instantie toegankelijk vanaf het AbstractProcessor.processingEnv beschermd veld. De volgende regels geven een foutmelding voor elk foutief geannoteerd element tijdens de bronverwerkingsfase:

otherMethods.forEach (element -> processingEnv.getMessager (). printMessage (Diagnostic.Kind.ERROR, "@BuilderProperty moet worden toegepast op een setXxx-methode" + "met een enkel argument", element));

Als de juiste verzameling setters leeg is, heeft het natuurlijk geen zin om door te gaan met de iteratie van de huidige type-elementenset:

if (setters.isEmpty ()) {ga verder; }

Als de setters-verzameling ten minste één element heeft, gaan we dit gebruiken om de volledig gekwalificeerde klassenaam uit het omsluitende element te halen, wat in het geval van de setter-methode de bronklasse zelf lijkt te zijn:

String className = ((TypeElement) setters.get (0) .getEnclosingElement ()). GetQualifiedName (). ToString ();

Het laatste stukje informatie dat we nodig hebben om een ​​builder-klasse te genereren, is een kaart tussen de namen van de setters en de namen van hun argumenttypen:

Kaart setterMap = setters.stream (). Collect (Collectors.toMap (setter -> setter.getSimpleName (). ToString (), setter -> ((ExecutableType) setter.asType ()) .getParameterTypes (). Get (0) .toString ()));

6.3. Het uitvoerbestand genereren

Nu hebben we alle informatie die we nodig hebben om een ​​builder-klasse te genereren: de naam van de source-klasse, alle setter-namen en hun argumenttypen.

Om het uitvoerbestand te genereren, gebruiken we de Filer instantie die opnieuw wordt geleverd door het object in de AbstractProcessor.processingEnv beschermd eigendom:

JavaFileObject builderFile = processingEnv.getFiler () .createSourceFile (builderClassName); probeer (PrintWriter out = nieuwe PrintWriter (builderFile.openWriter ())) {// geschreven bestand naar out…}

De volledige code van het writeBuilderFile methode wordt hieronder gegeven. We hoeven alleen de pakketnaam, de volledig gekwalificeerde naam van de builder-klasse en eenvoudige klassenamen voor de source-klasse en de builder-klasse te berekenen. De rest van de code is vrij eenvoudig.

private void writeBuilderFile (String className, Map setterMap) gooit IOException {String packageName = null; int lastDot = className.lastIndexOf ('.'); if (lastDot> 0) {pakketnaam = className.substring (0, lastDot); } String simpleClassName = className.substring (lastDot + 1); String builderClassName = className + "Builder"; String builderSimpleClassName = builderClassName .substring (lastDot + 1); JavaFileObject builderFile = processingEnv.getFiler () .createSourceFile (builderClassName); probeer (PrintWriter out = nieuwe PrintWriter (builderFile.openWriter ())) {if (packageName! = null) {out.print ("package"); out.print (pakketnaam); out.println (";"); out.println (); } out.print ("public class"); out.print (builderSimpleClassName); out.println ("{"); out.println (); out.print ("privé"); out.print (simpleClassName); out.print ("object = nieuw"); out.print (simpleClassName); out.println ("();"); out.println (); out.print ("openbaar"); out.print (simpleClassName); out.println ("build () {"); out.println ("return object;"); out.println ("}"); out.println (); setterMap.entrySet (). forEach (setter -> {String methodName = setter.getKey (); String argumentType = setter.getValue (); out.print ("public"); out.print (builderSimpleClassName); out.print ( ""); out.print (methodName); out.print ("("); out.print (argumentType); out.println ("waarde) {"); out.print ("object."); out. print (methodName); out.println ("(waarde);"); out.println ("retourneer dit;"); out.println ("}"); out.println ();}); out.println ("}"); }}

7. Het voorbeeld uitvoeren

Om het genereren van code in actie te zien, moet u ofwel beide modules compileren vanuit de gemeenschappelijke bovenliggende root of eerst het annotatie-processor module en vervolgens het annotatie-gebruiker module.

Het gegenereerde PersonBuilder klasse is te vinden in de annotatie-gebruiker / doel / gegenereerde-bronnen / annotaties / com / baeldung / annotatie / PersonBuilder.java bestand en zou er als volgt uit moeten zien:

pakket com.baeldung.annotation; openbare klasse PersonBuilder {privépersoonobject = nieuwe persoon (); publieke persoon build () {return object; } openbare PersonBuilder setName (waarde java.lang.String) {object.setName (waarde); dit teruggeven; } openbare PersonBuilder setAge (int waarde) {object.setAge (waarde); dit teruggeven; }}

8. Alternatieve manieren om een ​​processor te registreren

Om je annotatieprocessor te gebruiken tijdens de compilatiefase, heb je verschillende andere opties, afhankelijk van je gebruiksscenario en de tools die je gebruikt.

8.1. Met behulp van de Annotation Processor Tool

De apt tool was een speciaal opdrachtregelprogramma voor het verwerken van bronbestanden. Het was een onderdeel van Java 5, maar sinds Java 7 werd het vervangen door andere opties en volledig verwijderd in Java 8. Het zal in dit artikel niet worden besproken.

8.2. Met behulp van de compilersleutel

De -processor compilersleutel is een standaard JDK-voorziening om de bronverwerkingsfase van de compiler uit te breiden met uw eigen annotatieprocessor.

Merk op dat de processor zelf en de annotatie al gecompileerd moeten zijn als klassen in een aparte compilatie en aanwezig moeten zijn op het klassenpad, dus het eerste wat je moet doen is:

javac com / baeldung / annotation / processor / BuilderProcessor javac com / baeldung / annotation / processor / BuilderProperty

Vervolgens doe je de daadwerkelijke compilatie van je bronnen met de -processor sleutel die de klasse van de annotatieprocessor specificeert die u zojuist hebt gecompileerd:

javac -processor com.baeldung.annotation.processor.MyProcessor Persoon.java

Om meerdere annotatieprocessors in één keer te specificeren, kun je hun klassennamen als volgt scheiden met komma's:

javac -processor pakket1.Processor1, pakket2.Processor2 Bronbestand.java

8.3. Maven gebruiken

De maven-compiler-plugin staat het specificeren van annotatieprocessors toe als onderdeel van de configuratie.

Hier is een voorbeeld van het toevoegen van een annotatieprocessor voor de compiler-plug-in. U kunt ook de directory opgeven waarin de gegenereerde bronnen moeten worden geplaatst met behulp van de gegenereerdeSourcesDirectory configuratieparameter.

Merk op dat de BuilderProcessor class zou al gecompileerd moeten zijn, bijvoorbeeld geïmporteerd vanuit een andere jar in de build-afhankelijkheden:

   org.apache.maven.plugins maven-compiler-plugin 3.5.1 1.8 1.8 UTF-8 $ {project.build.directory} / generated-sources / com.baeldung.annotation.processor.BuilderProcessor 

8,4. Een Processor Jar toevoegen aan het Classpath

In plaats van de annotatieprocessor op te geven in de compileropties, kunt u eenvoudig een speciaal gestructureerde jar met de processorklasse toevoegen aan het klassepad van de compiler.

Om het automatisch op te halen, moet de compiler de naam van de processorklasse weten. U moet het dus specificeren in het META-INF / services / javax.annotation.processing.Processor bestand als een volledig gekwalificeerde klassenaam van de processor:

com.baeldung.annotation.processor.BuilderProcessor

U kunt ook meerdere processors uit deze pot specificeren om automatisch op te halen door ze te scheiden met een nieuwe regel:

pakket1.Processor1 pakket2.Processor2 pakket3.Processor3

Als je Maven gebruikt om deze jar te bouwen en dit bestand rechtstreeks in het src / main / resources / META-INF / services directory, krijg je de volgende foutmelding:

[FOUT] Onjuist serviceconfiguratiebestand of er is een uitzondering opgetreden tijdens het maken van Processor-object: javax.annotation.processing.Processor: Provider com.baeldung.annotation.processor.BuilderProcessor niet gevonden

Dit komt doordat de compiler dit bestand probeert te gebruiken tijdens de bronverwerking fase van de module zelf wanneer het BuilderProcessor bestand is nog niet gecompileerd. Het bestand moet ofwel in een andere bronmap worden geplaatst en naar het META-INF / diensten directory tijdens de fase van het kopiëren van bronnen van de Maven-build, of (nog beter) gegenereerd tijdens de build.

De Google auto-service bibliotheek, besproken in de volgende sectie, maakt het mogelijk om dit bestand te genereren met behulp van een eenvoudige annotatie.

8.5. Met behulp van de Google auto-service Bibliotheek

Om het registratiebestand automatisch te genereren, kunt u de @AutoService annotatie van de Google's auto-service bibliotheek, zoals dit:

@AutoService (Processor.class) openbare BuilderProcessor breidt AbstractProcessor uit {//…}

Deze annotatie wordt zelf verwerkt door de annotatieprocessor uit de autoservicebibliotheek. Deze processor genereert de META-INF / services / javax.annotation.processing.Processor bestand met het BuilderProcessor naam van de klasse.

9. Conclusie

In dit artikel hebben we de annotatieverwerking op bronniveau gedemonstreerd aan de hand van een voorbeeld van het genereren van een Builder-klasse voor een POJO. We hebben ook verschillende alternatieve manieren geboden om annotatieprocessors in uw project te registreren.

De broncode voor het artikel is beschikbaar op GitHub.