Spracovanie anotácií v Jave a vytvorenie Builderu

1. Úvod

Tento článok je úvod do spracovania anotácií na úrovni zdroja Java a poskytuje príklady použitia tejto techniky na generovanie ďalších zdrojových súborov počas kompilácie.

2. Aplikácie spracovania anotácií

Spracovanie anotácií na úrovni zdroja sa prvýkrát objavilo v prostredí Java 5. Jedná sa o praktický postup generovania ďalších zdrojových súborov počas fázy kompilácie.

Zdrojové súbory nemusia byť súbory Java - na základe anotácií vo zdrojovom kóde môžete vygenerovať akýkoľvek druh popisu, metadát, dokumentácie, zdrojov alebo akýkoľvek iný typ súborov.

Spracovanie anotácií sa aktívne používa v mnohých všadeprítomných knižniciach Java, napríklad na generovanie metaclass v QueryDSL a JPA, na rozšírenie tried kódom typového štítku v knižnici Lombok.

Je dôležité si uvedomiť, že obmedzenie API na spracovanie anotácií - dá sa použiť iba na generovanie nových súborov, nie na zmenu existujúcich.

Pozoruhodnou výnimkou je knižnica Lombok, ktorá používa spracovanie anotácií ako mechanizmus bootstrappingu, aby sa zahrnula do procesu kompilácie a upravila AST pomocou niektorých interných API kompilátora. Táto hackerská technika nemá nič spoločné so zamýšľaným účelom spracovania anotácií, a preto sa o nej v tomto článku nehovorí.

3. API na spracovanie anotácií

Spracovanie anotácie prebieha vo viacerých kolách. Každé kolo začína kompilátorom hľadaním anotácií v zdrojových súboroch a výberom anotačných procesorov vhodných pre tieto anotácie. Každý procesor anotácií je zase volaný z príslušných zdrojov.

Ak sa počas tohto procesu vygenerujú nejaké súbory, začne sa ďalšie kolo, v ktorom sa vygenerujú súbory. Tento proces pokračuje, kým sa počas fázy spracovania nevygenerujú žiadne nové súbory.

Každý procesor anotácií je zase volaný z príslušných zdrojov. Ak sa počas tohto procesu vygenerujú nejaké súbory, začne sa ďalšie kolo, v ktorom sa vygenerujú súbory. Tento proces pokračuje, kým sa počas fázy spracovania nevygenerujú žiadne nové súbory.

Rozhranie API na spracovanie anotácií sa nachádza v javax.annotation.processing balíček. Hlavné rozhranie, ktoré budete musieť implementovať, je procesor rozhranie, ktoré má čiastočnú implementáciu v podobe AbstractProcessor trieda. Táto trieda je tá, ktorú rozšírime o vytvorenie vlastného anotačného procesora.

4. Inštalácia projektu

Na demonštráciu možností spracovania anotácií vyvinieme jednoduchý procesor na generovanie plynulých nástrojov na tvorbu objektov pre anotované triedy.

Náš projekt rozdelíme do dvoch modulov Maven. Jeden z nich, anotačný procesor modul bude obsahovať samotný procesor spolu s anotáciou a ďalší, anotácia-užívateľ modul bude obsahovať anotovanú triedu. Toto je typický prípad použitia spracovania anotácií.

Nastavenia pre anotačný procesor modulu sú nasledujúce. Chystáme sa pomocou knižnice automatických služieb spoločnosti Google vygenerovať súbor metadát procesora, o ktorom sa bude diskutovať neskôr, a maven-compiler-plugin vyladený na zdrojový kód Java 8. Verzie týchto závislostí sú extrahované do sekcie vlastností.

Najnovšie verzie knižnice automatických služieb a doplnku maven-compiler-plugin nájdete v úložisku Maven Central:

 1.0-rc2 3.5.1 com.google.auto.service auto-service $ {auto-service.version} poskytované org.apache.maven.plugins maven-compiler-plugin $ {maven-compiler-plugin.version} 1,8 1,8 

The anotácia-užívateľ Modul Maven s anotovanými zdrojmi nevyžaduje žiadne špeciálne ladenie, okrem pridania závislosti na module anotácie a procesora v sekcii závislostí:

 com.baeldung spracovanie anotácií 1.0.0-SNAPSHOT 

5. Definovanie anotácie

Predpokladajme, že máme v našej triede jednoduchú POJO anotácia-užívateľ modul s niekoľkými poľami:

public class Osoba {private int age; súkromné ​​meno reťazca; // zakladatelia a zakladatelia ...}

Chceme vytvoriť triedu pomocných nástrojov na vytvorenie inštancie Osoba trieda plynulejšie:

Osoba person = new PersonBuilder () .setAge (25) .setName ("John") .build ();

Toto PersonBuilder trieda je zrejmou voľbou pre generáciu, pretože jej štruktúra je úplne definovaná symbolom Osoba nastavovacie metódy.

Poďme vytvoriť @BuilderProperty anotácia v anotačný procesor modul pre nastavovacie metódy. Umožní nám to generovať Staviteľ trieda pre každú triedu, ktorá má anotované svoje metódy nastavenia:

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

The @ Cieľ anotácia s ElementType.METHOD parameter zaisťuje, že túto anotáciu je možné vložiť iba na metódu.

The ZDROJ zásada uchovania znamená, že táto anotácia je k dispozícii iba počas spracovania zdroja a nie je k dispozícii za behu programu.

The Osoba trieda s vlastnosťami anotovanými s @BuilderProperty anotácia bude vyzerať nasledovne:

public class Osoba {private int age; súkromné ​​meno reťazca; @BuilderProperty public void setAge (int age) {this.age = age; } @BuilderProperty public void setName (názov reťazca) {this.name = name; } // zakladatelia ...}

6. Implementácia a procesor

6.1. Vytvára sa AbstractProcessor Podtrieda

Začneme s rozširovaním AbstractProcessor trieda vo vnútri anotačný procesor Modul Maven.

Najprv by sme mali určiť anotácie, ktoré je tento procesor schopný spracovať, a tiež podporovanú verziu zdrojového kódu. To sa dá dosiahnuť implementáciou metód getSupportedAnnotationTypes a getSupportedSourceVersion z procesor rozhraním alebo anotáciou triedy pomocou @SupportedAnnotationTypes a @SupportedSourceVersion anotácie.

The @ AutoService anotácia je súčasťou autoservis knižnica a umožňuje generovať metadáta procesora, čo bude vysvetlené v nasledujúcich častiach.

@SupportedAnnotationTypes ("com.baeldung.annotation.processor.BuilderProperty") @SupportedSourceVersion (SourceVersion.RELEASE_8) @AutoService (Processor.class) verejná trieda BuilderProcessor rozširuje AbstractProcessor {@Override verejný boolean proces (Nastaviť verejný boolean proces) ; }}

Môžete určiť nielen konkrétne názvy tried anotácií, ale aj zástupné znaky, napríklad „Com.baeldung.annotation. *“ na spracovanie anotácií vo vnútri com.baeldung.anotácia balíček a všetky jeho čiastkové balíčky alebo dokonca “*” na spracovanie všetkých anotácií.

Jedinou metódou, ktorú budeme musieť implementovať, je procesu metóda, ktorá vykonáva samotné spracovanie. Vyvoláva ho kompilátor pre každý zdrojový súbor obsahujúci zodpovedajúce anotácie.

Anotácie sú odovzdané ako prvé Nastaviť anotácie argument a informácie o aktuálnom kole spracovania sa odovzdajú ako RoundEnviroment roundEnv argument.

Návrat boolovský hodnota by mala byť pravda ak váš anotačný procesor spracoval všetky odovzdané anotácie a nechcete, aby boli odovzdané iným anotačným procesorom v zozname.

6.2. Zhromažďovanie údajov

Náš procesor zatiaľ neurobí nič užitočné, tak ho vyplníme kódom.

Najskôr budeme musieť opakovať všetky typy anotácií, ktoré sa v triede nachádzajú - v našom prípade anotácie sada bude mať jeden prvok zodpovedajúci @BuilderProperty anotácie, aj keď sa táto anotácia v zdrojovom súbore vyskytuje viackrát.

Stále je lepšie implementovať procesu metóda ako iteračný cyklus, pre úplnosť:

@Override public boolean process (Set annotations, RoundEnvironment roundEnv) {for (TypeElement annotation: annotations) {Set annotatedElements = roundEnv.getElementsAnnotatedWith (annotation); //…} návrat true; }

V tomto kóde používame RoundEnvironment napríklad na príjem všetkých prvkov anotovaných pomocou @BuilderProperty anotácia. V prípade Osoba triedy, tieto prvky zodpovedajú setName a setAge metódy.

@BuilderProperty používateľ anotácie môže omylom anotovať metódy, ktoré v skutočnosti nie sú nastavovateľmi. Názov metódy nastavovača by mal začínať nastaviťa metóda by mala dostať jediný argument. Poďme teda oddeliť pšenicu od pliev.

V nasledujúcom kóde používame Collectors.partitioningBy () kolektor na rozdelenie anotovaných metód do dvoch kolekcií: správne anotovaní zadávatelia a ďalšie chybne anotované metódy:

Mapa annotatedMethods = annotatedElements.stream (). collect (Collectors.partitioningBy (element -> ((ExecutableType) element.asType ()). getParameterTypes (). size () == 1 && element.getSimpleName (). toString (). startsWith („súprava“))); Zoznam nastavovateľov = annotatedMethods.get (true); Zoznam otherMethods = annotatedMethods.get (false);

Tu používame Element.asType () metóda na získanie inštancie Typ Zrkadlo trieda, ktorá nám dáva určitú schopnosť nahliadať do typov, aj keď sme iba v štádiu spracovania zdroja.

Mali by sme používateľa upozorniť na nesprávne anotované metódy, takže poďme použiť Messager inštancia prístupná z AbstractProcessor.processingEnv chránené pole. Nasledujúce riadky vygenerujú chybu pre každý chybne anotovaný prvok počas fázy spracovania zdroja:

otherMethods.forEach (element -> processingEnv.getMessager (). printMessage (Diagnostic.Kind.ERROR, "@BuilderProperty musí byť aplikovaný na metódu setXxx" + "s jediným argumentom", element));

Samozrejme, ak je kolekcia správnych nastavovačov prázdna, nemá zmysel pokračovať v opakovaní aktuálnej množiny prvkov aktuálneho typu:

if (setters.isEmpty ()) {pokračovať; }

Ak má kolekcia setterov aspoň jeden prvok, použijeme ju na získanie plne kvalifikovaného názvu triedy z ohraničujúceho prvku, ktorým sa v prípade metódy setter javí byť samotná zdrojová trieda:

Reťazec className = ((TypeElement) setters.get (0) .getEnclosingElement ()). GetQualifiedName (). ToString ();

Posledným bitom informácií, ktoré potrebujeme na vygenerovanie triedy builderov, je mapa medzi menami pôvodcov a názvami ich typov argumentov:

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

6.3. Generovanie výstupného súboru

Teraz máme všetky informácie, ktoré potrebujeme na vygenerovanie triedy staviteľa: názov zdrojovej triedy, všetky jej názvy zakladateľov a ich typy argumentov.

Na vygenerovanie výstupného súboru použijeme Filer inštancia poskytnutá znova objektom v AbstractProcessor.processingEnv chránený majetok:

JavaFileObject builderFile = processingEnv.getFiler () .createSourceFile (builderClassName); try (PrintWriter out = new PrintWriter (builderFile.openWriter ())) {// zápis vygenerovaného súboru na von ...}

Celý kód writeBuilderFile metóda je uvedená nižšie. Potrebujeme iba vypočítať názov balíka, úplný názov triedy staviteľa a jednoduché názvy tried pre zdrojovú triedu a triedu staviteľa. Zvyšok kódu je dosť priamy.

private void writeBuilderFile (String className, Map setterMap) vyvolá IOException {String packageName = null; int lastDot = className.lastIndexOf ('.'); if (lastDot> 0) {packageName = className.substring (0, lastDot); } Reťazec simpleClassName = className.substring (lastDot + 1); Reťazec builderClassName = className + "Builder"; String builderSimpleClassName = builderClassName .substring (lastDot + 1); JavaFileObject builderFile = processingEnv.getFiler () .createSourceFile (builderClassName); try (PrintWriter out = new PrintWriter (builderFile.openWriter ())) {if (packageName! = null) {out.print ("package"); out.print (názov_balíka); out.println (";"); out.println (); } out.print ("verejná trieda"); out.print (builderSimpleClassName); out.println ("{"); out.println (); out.print („súkromný“); out.print (simpleClassName); out.print ("objekt = nový"); out.print (simpleClassName); out.println ("();"); out.println (); out.print („verejný“); out.print (simpleClassName); out.println ("build () {"); out.println ("návratový objekt;"); 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 ("hodnota) {"); out.print ("objekt."); out. print (methodName); out.println ("(value);"); out.println ("return this;"); out.println ("}"); out.println ();}); out.println ("}"); }}

7. Spustenie príkladu

Ak chcete vidieť generovanie kódu v akcii, mali by ste kompilovať oba moduly zo spoločného nadradeného koreňa alebo najskôr skompilovať anotačný procesor modul a potom anotácia-užívateľ modul.

Generované PersonBuilder triedu nájdete vo vnútri anotácia-užívateľ / cieľ / generované-zdroje / anotácie / com / baeldung / anotácia / PersonBuilder.java súbor a mal by vyzerať takto:

balík com.baeldung.annotation; public class PersonBuilder {private Person object = new Person (); public Person build () {návratový objekt; } public PersonBuilder setName (java.lang.String hodnota) {object.setName (hodnota); vráťte to; } public PersonBuilder setAge (int hodnota) {object.setAge (hodnota); vráťte to; }}

8. Alternatívne spôsoby registrácie procesora

Ak chcete použiť anotačný procesor počas fázy kompilácie, máte niekoľko ďalších možností, v závislosti od prípadu použitia a použitých nástrojov.

8.1. Používanie nástroja na spracovanie anotácií

The trefný nástroj bol špeciálny nástroj príkazového riadku na spracovanie zdrojových súborov. Bola to súčasť Java 5, ale od Javy 7 bola zastaraná v prospech iných možností a úplne odstránená v Jave 8. V tomto článku sa o nej nebudem baviť.

8.2. Pomocou kľúča kompilátora

The -procesor kľúč kompilátora je štandardné zariadenie JDK na rozšírenie fázy spracovania zdroja kompilátora o vlastný anotačný procesor.

Samotný procesor a anotácia musia byť už skompilované ako triedy v samostatnej kompilácii a musia byť prítomné na ceste triedy, takže prvá vec, ktorú by ste mali urobiť, je:

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

Potom urobíte skutočnú kompiláciu svojich zdrojov pomocou -procesor kľúč určujúci triedu anotačného procesora, ktorú ste práve zostavili:

javac -procesor com.baeldung.annotation.processor.MyProcessor Person.java

Ak chcete zadať niekoľko procesorov anotácií naraz, môžete ich názvy tried oddeliť čiarkami, napríklad takto:

javac -processor package1.Processor1, package2.Processor2 SourceFile.java

8.3. Používanie Maven

The maven-compiler-plugin umožňuje špecifikovať anotačné procesory ako súčasť svojej konfigurácie.

Tu je príklad pridania anotačného procesora pre doplnok kompilátora. Môžete tiež určiť adresár, do ktorého chcete vygenerované zdroje vložiť, pomocou generatedSourcesDirectory konfiguračný parameter.

Všimnite si, že BuilderProcessor trieda by už mala byť kompilovaná, napríklad importovaná z inej nádoby v závislostiach zostavenia:

   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. Pridanie procesora do triedy Classpath

Namiesto zadania anotačného procesora v možnostiach kompilátora môžete jednoducho pridať do štruktúrovanej cesty kompilátora špeciálne štruktúrovaný jar s triedou procesora.

Aby to kompilátor získal automaticky, musí poznať názov triedy procesora. Musíte to teda určiť v META-INF / services / javax.annotation.processing.Processor súbor ako plne kvalifikovaný názov triedy procesora:

com.baeldung.annotation.processor.BuilderProcessor

Môžete tiež určiť niekoľko procesorov z tohto pohára, ktoré sa majú automaticky vyzdvihnúť, a to tak, že ich oddelíte novým riadkom:

package1.Processor1 package2.Processor2 package3.Processor3

Ak na zostavenie tejto nádoby použijete Maven a pokúsite sa tento súbor vložiť priamo do súboru src / main / resources / META-INF / služby adresári, narazíte na nasledujúcu chybu:

[ERROR] Chybný konfiguračný súbor služby alebo výnimka vyvolaná pri konštrukcii objektu procesora: javax.annotation.processing.Procesor: Poskytovateľ com.baeldung.annotation.processor.BuilderProcessor nenájdený

Je to preto, že kompilátor sa pokúša tento súbor použiť počas spracovanie zdroja štádium samotného modulu, keď BuilderProcessor súbor ešte nie je skompilovaný. Súbor musí byť vložený do iného adresára zdrojov a skopírovaný do priečinka META-INF / služby adresár počas fázy kopírovania prostriedkov zostavenia Maven alebo (ešte lepšie) generovaného počas zostavovania.

Google autoservis Knižnica, o ktorej sa pojednáva v nasledujúcej časti, umožňuje generovanie tohto súboru pomocou jednoduchej anotácie.

8.5. Pomocou Google autoservis Knižnica

Na automatické vygenerovanie registračného súboru môžete použiť @ AutoService anotácia z Googlu autoservis knižnica, napríklad takto:

@AutoService (Processor.class) public BuilderProcessor rozširuje AbstractProcessor {//…}

Táto anotácia je sama spracovaná anotačným procesorom z knižnice auto-služieb. Tento procesor generuje META-INF / services / javax.annotation.processing.Processor súbor obsahujúci BuilderProcessor názov triedy.

9. Záver

V tomto článku sme demonštrovali spracovanie anotácií na úrovni zdroja pomocou príkladu generovania triedy Builder pre POJO. Poskytli sme tiež niekoľko alternatívnych spôsobov registrácie procesorov anotácií vo vašom projekte.

Zdrojový kód článku je k dispozícii na GitHub.