Obrada Java anotacija i stvaranje graditelja

1. Uvod

Ovaj je članak uvod u obradu napomena na izvornom nivou Java i daje primjere korištenja ove tehnike za generiranje dodatnih izvornih datoteka tijekom kompilacije.

2. Primjene obrade bilješki

Obrada bilješki na izvornoj razini prvi se put pojavila u Javi 5. To je zgodna tehnika za generiranje dodatnih izvornih datoteka tijekom faze kompilacije.

Izvorne datoteke ne moraju biti Java datoteke - možete generirati bilo koju vrstu opisa, metapodataka, dokumentacije, resursa ili bilo koje druge vrste datoteka, na temelju bilješki u izvornom kodu.

Obrada bilješki aktivno se koristi u mnogim sveprisutnim Java knjižnicama, na primjer, za generiranje metaklasa u QueryDSL i JPA, za uvećavanje klasa s uzorkom koda u biblioteci Lombok.

Važna stvar koju treba napomenuti je ograničenje API-ja za obradu napomena - može se koristiti samo za generiranje novih datoteka, a ne za promjenu postojećih.

Izuzetna iznimka je knjižnica Lombok koja koristi obradu napomena kao mehanizam za pokretanje sustava da bi se uključila u proces kompilacije i izmijenila AST putem nekih internih API-ja kompajlera. Ova hacky tehnika nema nikakve veze s namjerom obrade bilješki i stoga se u ovom članku ne raspravlja.

3. API za obradu bilješki

Obrada bilješki vrši se u više krugova. Svaka runda započinje prevođivačem koji traži napomene u izvornim datotekama i bira procesore napomena koji odgovaraju tim napomenama. Zauzvrat se svaki procesor napomena poziva na odgovarajuće izvore.

Ako se tijekom ovog postupka generiraju datoteke, započinje se druga runda s generiranim datotekama kao ulaznim podacima. Taj se postupak nastavlja sve dok se u fazi obrade ne generiraju nove datoteke.

Zauzvrat se svaki procesor napomena poziva na odgovarajuće izvore. Ako se tijekom ovog postupka generiraju datoteke, započinje se druga runda s generiranim datotekama kao ulaznim podacima. Taj se postupak nastavlja sve dok se u fazi obrade ne generiraju nove datoteke.

API za obradu napomena nalazi se u javax.anotacija.obrada paket. Glavno sučelje koje ćete morati implementirati je Procesor sučelje, koje ima djelomičnu implementaciju u obliku SažetakProcesor razred. Ova klasa je ona koju ćemo proširiti kako bismo stvorili vlastiti procesor napomena.

4. Postavljanje projekta

Da bismo demonstrirali mogućnosti obrade bilješki, razvit ćemo jednostavan procesor za generiranje tečnih graditelja objekata za označene klase.

Podijelit ćemo naš projekt na dva Mavenova modula. Jedan od njih, napomena-obrađivač modul, sadržavat će sam procesor zajedno s napomenom i još jedan, bilješka-korisnik modul, sadržavat će označenu klasu. Ovo je tipičan slučaj upotrebe obrade bilješki.

Postavke za napomena-obrađivač modula su kako slijedi. Upotrijebit ćemo Googleovu knjižnicu automatskih usluga za generiranje datoteke metapodataka procesora o kojoj ćemo kasnije razgovarati i maven-compiler-plugin podešen za izvorni kod Java 8. Verzije ovih ovisnosti izdvajaju se u odjeljak svojstava.

Najnovije verzije knjižnice automatskih usluga i dodatka za maven-compiler mogu se naći u spremištu Maven Central:

 1.0-rc2 3.5.1 com.google.auto.service auto-usluga $ {auto-service.version} pod uvjetom da org.apache.maven.plugins maven-compiler-plugin $ {maven-compiler-plugin.version} 1,8 1,8 

The bilješka-korisnik Modul Maven s označenim izvorima ne treba posebno podešavanje, osim dodavanja ovisnosti o modulu procesora napomena u odjeljku ovisnosti:

 com.baeldung obrada bilješki 1.0.0-SNAPSHOT 

5. Utvrđivanje bilješke

Pretpostavimo da imamo jednostavnu POJO klasu u svojoj bilješka-korisnik modul s nekoliko polja:

javni razred Osoba {private int age; privatni naziv niza; // geteri i postavljači ...}

Želimo stvoriti pomoćnu klasu graditelja za instanciju Osoba tečaj tečajnije:

Osoba osoba = novi PersonBuilder () .setAge (25) .setName ("John") .build ();

Ovaj PersonBuilder Razred je očit izbor za generaciju, jer je njegova struktura u potpunosti definirana Osoba metode postavljača.

Stvorimo a @BuilderProperty bilješka u napomena-obrađivač modul za metode postavljanja. Omogućit će nam generiranje Graditelj razred za svaku klasu koja ima označene metode postavljača:

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

The @Cilj napomena uz ElementType.METHOD parametar osigurava da se ova napomena može staviti samo na metodu.

The IZVOR politika zadržavanja znači da je ova napomena dostupna samo tijekom obrade izvora i nije dostupna tijekom izvođenja.

The Osoba klasa sa svojstvima označenim s @BuilderProperty napomena će izgledati kako slijedi:

javni razred Osoba {private int age; privatni naziv niza; @BuilderProperty javna praznina setAge (int age) {this.age = age; } @BuilderProperty javna praznina setName (ime niza) {this.name = name; } // dobivači…}

6. Provedba a Procesor

6.1. Stvaranje SažetakProcesor Podrazred

Počet ćemo s proširivanjem SažetakProcesor klasa unutar napomena-obrađivač Maven modul.

Prvo bismo trebali navesti napomene koje ovaj procesor može obraditi, kao i podržanu verziju izvornog koda. To se može učiniti primjenom metoda getSupportedAnnotationTypes i getSupportedSourceVersion od Procesor sučelje ili označavanjem predavanja s @SupportedAnnotationTypes i @SupportedSourceVersion bilješke.

The @AutoService bilješka je dio auto-usluga knjižnica i omogućuje generiranje metapodataka procesora što će biti objašnjeno u sljedećim odjeljcima.

@SupportedAnnotationTypes ("com.baeldung.annotation.processor.BuilderProperty") @SupportedSourceVersion (SourceVersion.RELEASE_8) @AutoService (Processor.class) javna klasa BuilderProcessor proširuje AbstractProcessor {@Overridn Postavljanje povratnih informacija {RoundOverriden Povratak na javni unos {RoundOverriden ; }}

Možete odrediti ne samo konkretna imena klasa napomena, već i zamjenske znakove, poput "Com.baeldung.annotation. *" za obradu napomena unutar com.baeldung.notacija paket i svi njegovi podpaketi, ili čak “*” za obradu svih bilješki.

Jedina metoda koju ćemo morati primijeniti je postupak metoda koja vrši samu obradu. Poziva ga prevoditelj za svaku izvornu datoteku koja sadrži odgovarajuće napomene.

Bilješke se donose kao prve Postavi napomene argument, a podaci o trenutnom krugu obrade prosljeđuju se kao RoundEnviroment roundEnv argument.

Povratak boolean vrijednost bi trebala biti pravi ako je vaš procesor bilješki obradio sve proslijeđene bilješke i ne želite da se prosljeđuju drugim procesorima bilješki s popisa.

6.2. Prikupljanje podataka

Naš procesor još uvijek ne čini ništa korisno, pa napunimo ga kodom.

Prvo ćemo morati prelistati sve vrste bilješki koje se nalaze u klasi - u našem slučaju, bilješke set će imati jedan element koji odgovara @BuilderProperty napomena, čak i ako se ta napomena dogodi više puta u izvornoj datoteci.

Ipak, bolje je implementirati postupak metoda kao iteracijski ciklus, radi cjelovitosti:

@Override javni logički postupak (Postavi napomene, RoundEnvironment roundEnv) {za (Napomena TypeElement: napomene) {Postavi annotatedElements = roundEnv.getElementsAnnotatedWith (napomena); // ...} return true; }

U ovom kodu koristimo Okruglo okruženje instancu za primanje svih elemenata označenih s @BuilderProperty bilješka. U slučaju Osoba klase, ti elementi odgovaraju setName i setAge metode.

@BuilderProperty Korisnik anotacije mogao bi pogrešno bilježiti metode koje zapravo nisu postavljači. Naziv metode postavljača trebao bi započeti s postavljen, a metoda treba primiti jedan argument. Pa odvojimo pšenicu od kukolja.

U sljedećem kodu koristimo Collectors.partitioningBy () collector za dijeljenje anotiranih metoda u dvije zbirke: ispravno označeni postavljači i druge pogrešno označene metode:

Karta annotatedMethods = annotatedElements.stream (). collect (Collectors.partitioningBy (element -> ((ExecutableType) element.asType ()). getParameterTypes (). size () == 1 && element.getSimpleName (). toString (). startWith ("postavljeno"))); Postavljači popisa = annotatedMethods.get (true); Navedi otherMethods = annotatedMethods.get (false);

Ovdje koristimo Element.asType () metoda za primanje instance TypeMirror klasa koja nam daje određenu sposobnost introspekcije vrsta iako smo tek u fazi obrade izvora.

Trebali bismo upozoriti korisnika na pogrešno označene metode, pa upotrijebimo Messager instanci dostupnoj s AbstractProcessor.processingEnv zaštićeno polje. Sljedeći će retci prikazati pogrešku za svaki pogrešno označeni element tijekom faze obrade izvora:

otherMethods.forEach (element -> processingEnv.getMessager (). printMessage (Diagnostic.Kind.ERROR, "@BuilderProperty mora biti primijenjen na setXxx metodu" + "s jednim argumentom", element));

Naravno, ako je ispravna zbirka postavljača prazna, nema svrhe nastavljati trenutnu iteraciju skupa elemenata tipa:

if (setters.isEmpty ()) {nastaviti; }

Ako zbirka postavljača ima barem jedan element, koristit ćemo ga za dobivanje potpuno kvalificiranog naziva klase iz priloženog elementa, što se u slučaju metode postavljača čini izvornom klasom:

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

Posljednji bit podataka koji su nam potrebni za generiranje graditeljske klase je karta između imena postavljača i imena njihovih vrsta argumenata:

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

6.3. Generiranje izlazne datoteke

Sada imamo sve informacije potrebne za generiranje graditeljske klase: ime izvorne klase, sva imena postavljača i njihove vrste argumenata.

Da bismo generirali izlaznu datoteku, upotrijebit ćemo Filer instancu ponovno pruža objekt u AbstractProcessor.processingEnv zaštićeno dobro:

JavaFileObject builderFile = processingEnv.getFiler () .createSourceFile (builderClassName); probajte (PrintWriter out = novi PrintWriter (builderFile.openWriter ())) {// pisanje generirane datoteke u out ...}

Kompletni kod writeBuilderFile metoda je navedena u nastavku. Trebamo izračunati samo naziv paketa, potpuno kvalificirano ime klase graditelja i jednostavna imena klase za izvornu klasu i klasu graditelja. Ostatak koda prilično je jednostavan.

private void writeBuilderFile (StringName klase, Map setterMap) baca IOException {StringName paketa = null; int lastDot = className.lastIndexOf ('.'); if (lastDot> 0) {packageName = className.substring (0, lastDot); } Niz simpleClassName = className.substring (lastDot + 1); String builderClassName = className + "Builder"; Niz builderSimpleClassName = builderClassName .substring (lastDot + 1); JavaFileObject builderFile = processingEnv.getFiler () .createSourceFile (builderClassName); isprobajte (PrintWriter out = novi PrintWriter (builderFile.openWriter ())) {if (ime paketa! = null) {out.print ("paket"); out.print (ime paketa); out.println (";"); out.println (); } out.print ("javna klasa"); out.print (builderSimpleClassName); out.println ("{"); out.println (); out.print ("private"); out.print (simpleClassName); out.print ("objekt = novi"); out.print (simpleClassName); out.println ("();"); out.println (); out.print ("javno"); out.print (simpleClassName); out.println ("build () {"); out.println ("return object;"); out.println ("}"); out.println (); setterMap.entrySet (). forEach (setter -> {StringNaziv metode = setter.getKey (); String argumentType = setter.getValue (); out.print ("public"); out.print (builderSimpleClassName); out.print ( ""); out.print (methodName); out.print ("("); out.print (argumentType); out.println ("value) {"); out.print ("object."); out. print (methodName); out.println ("(value);"); out.println ("return this;"); out.println ("}"); out.println ();}); out.println ("}"); }}

7. Pokretanje primjera

Da biste vidjeli kako generiranje koda djeluje, trebali biste kompajlirati oba modula iz zajedničkog roditeljskog korijena ili prvo kompajlirati napomena-obrađivač modul, a zatim bilješka-korisnik modul.

Stvoreno PersonBuilder klasa može se naći unutar annotation-user / target / generated-sources / annotations / com / baeldung / annotation / PersonBuilder.java datoteka i trebala bi izgledati ovako:

paket com.baeldung.annotation; javna klasa PersonBuilder {objekt privatne osobe = nova osoba (); javni Person build () {return objekt; } javni PersonBuilder setName (java.lang.String vrijednost) {object.setName (value); vrati ovo; } javni PersonBuilder setAge (int vrijednost) {object.setAge (vrijednost); vrati ovo; }}

8. Alternativni načini registracije procesora

Da biste koristili procesor napomena tijekom faze kompilacije, imate nekoliko drugih mogućnosti, ovisno o vašem slučaju korištenja i alatima koje koristite.

8.1. Korištenje alata za obradu bilješki

The prikladan Alat je bio poseban uslužni program naredbenog retka za obradu izvornih datoteka. Bio je dio Jave 5, ali od Jave 7 zastario je u korist drugih opcija i potpuno uklonjen u Javi 8. O njemu se neće raspravljati u ovom članku.

8.2. Korištenje ključa kompajlera

The -procesor ključ kompajlera je standardni JDK uređaj za nadogradnju faze obrade izvora kompajlera vlastitim procesorom bilješki.

Imajte na umu da sam procesor i napomena moraju biti već kompilirani kao klase u zasebnoj kompilaciji i prisutni na putu predavanja, pa prvo što biste trebali učiniti je:

javac com / baeldung / anotacija / procesor / BuilderProcessor javac com / baeldung / anotacija / procesor / BuilderProperty

Tada radite stvarnu kompilaciju svojih izvora s -procesor tipka koja određuje klasu procesora napomena koju ste upravo kompilirali:

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

Da biste odredili nekoliko procesora napomena u jednom potezu, nazive njihovih razreda možete odvojiti zarezima, ovako:

javac -procesorski paket1.Procesor1, paket2.Procesor2 SourceFile.java

8.3. Koristeći Maven

The maven-compiler-plugin omogućuje specificiranje procesora za napomene kao dio njegove konfiguracije.

Evo primjera dodavanja procesora napomena za dodatak kompajlera. Također možete odrediti direktorij u koji ćete staviti generirane izvore, koristeći generatedSourcesDirectory parametar konfiguracije.

Imajte na umu da BuilderProcessor klasa bi već trebala biti kompajlirana, na primjer, uvezena iz druge jar u ovisnostima o gradnji:

   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. Dodavanje staklenke procesora u Classpath

Umjesto da navedete procesor napomena u opcijama kompajlera, možete jednostavno dodati posebno strukturirani jar s klasom procesora na put klase kompajlera.

Da bi ga automatski podigao, kompajler mora znati naziv klase procesora. Dakle, morate to navesti u META-INF / services / javax.annotation.processing.Processor datoteka kao potpuno kvalificirano ime klase procesora:

com.baeldung.annotation.processor.BuilderProcessor

Također možete odrediti nekoliko procesora iz ove staklenke koji će se automatski pokupiti razdvajanjem nove linije:

paket1.Procesor1 paket2.Procesor2 paket3.Procesor3

Ako koristite Maven za izgradnju ove jar i pokušate staviti ovu datoteku izravno u src / main / resources / META-INF / usluge direktorija, naići ćete na sljedeću pogrešku:

[POGREŠKA] Loša datoteka konfiguracije usluge ili izuzetak izbačen tijekom izrade objekta procesora: javax.annotation.processing.Processor: Provider com.baeldung.annotation.processor.BuilderProcessor nije pronađen

To je zato što prevodilac pokušava koristiti ovu datoteku tijekom obrada izvora faza samog modula kada se BuilderProcessor datoteka još nije sastavljena. Datoteka se mora staviti u drugi direktorij resursa i kopirati u META-INF / usluge direktorij tijekom faze kopiranja resursa gradnje Maven, ili (još bolje) generirane tijekom gradnje.

Google auto-usluga knjižnica, o kojoj se govori u sljedećem odjeljku, omogućuje generiranje ove datoteke pomoću jednostavne bilješke.

8.5. Korištenje Googlea auto-usluga Knjižnica

Da biste automatski generirali registracijsku datoteku, možete koristiti @AutoService Googleova napomena auto-usluga knjižnica, ovako:

@AutoService (Processor.class) javni BuilderProcessor proširuje AbstractProcessor {// ...}

Ovu napomenu sam obrađuje procesor bilješki iz knjižnice automatskih usluga. Ovaj procesor generira META-INF / services / javax.annotation.processing.Processor datoteka koja sadrži BuilderProcessor naziv klase.

9. Zaključak

U ovom smo članku demonstrirali obradu bilješki na razini izvora primjerom generiranja klase Builder za POJO. Osigurali smo i nekoliko alternativnih načina registracije procesora za napomene u vašem projektu.

Izvorni kôd članka dostupan je na GitHubu.