Java Annotation Processing and Create a Builder
1. Introduksjon
Denne artikkelen er en intro til Java-kildenivåbehandling av merknader og gir eksempler på bruk av denne teknikken for å generere flere kildefiler under kompilering.
2. Anvendelser av merknader
Merknadsbehandlingen på kildenivå dukket først opp i Java 5. Det er en praktisk teknikk for å generere flere kildefiler under kompileringsfasen.
Kildefilene trenger ikke å være Java-filer - du kan generere noen form for beskrivelse, metadata, dokumentasjon, ressurser eller andre typer filer, basert på merknader i kildekoden.
Annoteringsbehandling brukes aktivt i mange allestedsnærværende Java-biblioteker, for eksempel for å generere metaklasser i QueryDSL og JPA, for å utvide klasser med kjelekode i Lombok-biblioteket.
En viktig ting å merke seg er begrensningen av kommentarbehandlings-API-et - det kan bare brukes til å generere nye filer, ikke for å endre eksisterende.
Det bemerkelsesverdige unntaket er Lombok-biblioteket som bruker prosessering av merknader som en bootstrapping-mekanisme for å inkludere seg selv i kompileringsprosessen og endre AST via noen interne kompilator-APIer. Denne hacky-teknikken har ingenting å gjøre med det tiltenkte formålet med kommentarbehandling og er derfor ikke diskutert i denne artikkelen.
3. Kommentar-behandlings-API
Merknadsbehandlingen gjøres i flere runder. Hver runde starter med at kompilatoren søker etter kommentarene i kildefilene og velger merkeprosessorer som passer for disse kommentarene. Hver merkeprosessor kalles på sin side til de tilsvarende kildene.
Hvis noen filer genereres under denne prosessen, startes en ny runde med de genererte filene som input. Denne prosessen fortsetter til ingen nye filer genereres under behandlingsfasen.
Hver merkeprosessor kalles på sin side til de tilsvarende kildene. Hvis noen filer genereres under denne prosessen, startes en ny runde med de genererte filene som input. Denne prosessen fortsetter til ingen nye filer genereres under behandlingsfasen.
Merknadsbehandlings-API-et ligger i javax.annotation.processing pakke. Hovedgrensesnittet du må implementere er Prosessor grensesnitt, som har en delvis implementering i form av AbstraktProsessor klasse. Denne klassen er den vi skal utvide for å lage vår egen kommentarprosessor.
4. Sette opp prosjektet
For å demonstrere mulighetene for merkebehandling, vil vi utvikle en enkel prosessor for å generere flytende objektbyggere for merkede klasser.
Vi skal dele prosjektet vårt i to Maven-moduler. En av dem, kommentarprosessor modul, inneholder selve prosessoren sammen med merknaden, og en annen, kommentar-bruker modul, inneholder den merkede klassen. Dette er et typisk brukstilfelle av kommentarbehandling.
Innstillingene for merkeprosessor modulen er som følger. Vi skal bruke Googles autotjenestebibliotek til å generere metadata-fil for prosessorer som vil bli diskutert senere, og maven-compiler-plugin innstilt for Java 8 kildekoden. Versjonene av disse avhengighetene er hentet til eiendomsdelen.
De nyeste versjonene av autotjenestebiblioteket og maven-compiler-plugin finner du i Maven Central repository:
1.0-rc2 3.5.1 com.google.auto.service auto-service $ {auto-service.version} gitt org.apache.maven.plugins maven-compiler-plugin $ {maven-compiler-plugin.version} 1.8 1.8
De kommentar-bruker Maven-modul med merkede kilder trenger ingen spesiell innstilling, bortsett fra å legge til en avhengighet av merkeprosessormodulen i avhengighetsdelen:
com.baeldung merknadsbehandling 1.0.0-SNAPSHOT
5. Definere en kommentar
Anta at vi har en enkel POJO-klasse i vår kommentar-bruker modul med flere felt:
offentlig klasse Person {privat int alder; privat strengnavn; // getters og setters ...}
Vi ønsker å lage en byggherreklasse for å sette i gang Person klasse mer flytende:
Personperson = ny PersonBuilder () .setAge (25) .setName ("John") .build ();
Dette PersonBuilder klasse er et opplagt valg for en generasjon, da strukturen er fullstendig definert av Person settermetoder.
La oss lage en @BuilderProperty kommentar i merkeprosessor modul for settermetodene. Det vil tillate oss å generere Bygger klasse for hver klasse som har sine setermetoder kommentert:
@Target (ElementType.METHOD) @Retention (RetentionPolicy.SOURCE) offentlig @interface BuilderProperty {}
De @Mål kommentar med ElementType.METHOD parameteren sikrer at denne merknaden bare kan settes på en metode.
De KILDE opprettholdelsespolicy betyr at denne merknaden bare er tilgjengelig under kildebehandlingen og ikke er tilgjengelig på kjøretid.
De Person klasse med eiendommer merket med @BuilderProperty merknader vil se slik ut:
offentlig klasse Person {privat int alder; privat strengnavn; @BuilderProperty ugyldig setAge (int age) {this.age = age; } @BuilderProperty public void setName (strengnavn) {this.name = name; } // getters ...}
6. Implementering a Prosessor
6.1. Opprette en AbstraktProsessor Underklasse
Vi begynner med å utvide AbstraktProsessor klasse inne i merkeprosessor Maven-modul.
Først bør vi spesifisere merknader om at denne prosessoren er i stand til å behandle, og også den støttede kildekodeversjonen. Dette kan gjøres enten ved å implementere metodene getSupportedAnnotationTypes og getSupportedSourceVersion av Prosessor grensesnitt eller ved å kommentere klassen din med @SupportedAnnotationTypes og @SupportedSourceVersion kommentarer.
De @AutoService kommentar er en del av autotjeneste biblioteket og tillater å generere prosessorens metadata som vil bli forklart i de følgende avsnittene.
@SupportedAnnotationTypes ("com.baeldung.annotation.processor.BuilderProperty") @SupportedSourceVersion (SourceVersion.RELEASE_8) @AutoService (Processor.class) public class BuilderProcessor utvider AbstractProcessor {@Override offentlig miljøavtale ; }}
Du kan ikke bare spesifisere navnene på de konkrete merknadsklassene, men også jokertegn “Com.baeldung.annotation. *” å behandle merknader inne i com.baeldung.annotation pakken og alle dens underpakker, eller til og med “*” for å behandle alle merknader.
Den eneste metoden vi må implementere er prosess metode som gjør selve behandlingen. Det kalles av kompilatoren for hver kildefil som inneholder de matchende kommentarene.
Kommentarer sendes som den første Angi merknader argumentet, og informasjonen om den aktuelle behandlingsrunden blir sendt som RoundEnviroment roundEnv argument.
Tilbakekomsten boolsk verdien skal være ekte hvis merkeprosessoren din har behandlet alle passerte kommentarene, og du ikke vil at de skal sendes til andre merkeprosessorer nedover listen.
6.2. Samler data
Prosessoren vår gjør egentlig ikke noe nyttig ennå, så la oss fylle den med kode.
Først må vi gjenta alle merknader som finnes i klassen - i vårt tilfelle kommentarer settet vil ha et enkelt element som tilsvarer @BuilderProperty kommentar, selv om denne kommentaren forekommer flere ganger i kildefilen.
Likevel er det bedre å implementere prosess metode som en iterasjonssyklus, for fullstendighets skyld:
@Override offentlig boolsk prosess (Sett annotasjoner, RoundEnvironment roundEnv) {for (TypeElement-kommentar: annotasjoner) {Set annotatedElements = roundEnv.getElementsAnnotatedWith (annotation); // ...} returner sant; }
I denne koden bruker vi Rundt miljø forekomst for å motta alle elementene som er merket med @BuilderProperty kommentar. I tilfelle av Person klasse, samsvarer disse elementene med setName og sett alder metoder.
@BuilderProperty kommentarens bruker kan feilaktig kommentere metoder som ikke er settere. Settermetodenavnet skal begynne med sett, og metoden skal motta ett enkelt argument. Så la oss skille hveten fra agnet.
I den følgende koden bruker vi Collectors.partitioningBy () samler for å dele merkede metoder i to samlinger: korrekt merkede settere og andre feilkommenterte metoder:
Kart annotatedMethods = annotatedElements.stream (). collect (Collectors.partitioningBy (element -> ((ExecutableType) element.asType ()). getParameterTypes (). size () == 1 && element.getSimpleName (). toString (). startsMed ("sett"))); List setters = annotatedMethods.get (true); List otherMethods = annotatedMethods.get (false);
Her bruker vi Element.asType () metoden for å motta en forekomst av Type Speil klasse som gir oss noen evner til å introspisere typer selv om vi bare er på kildebehandlingsstadiet.
Vi bør advare brukeren om feilkommenterte metoder, så la oss bruke Messager eksempel tilgjengelig fra AbstractProcessor.processingEnv beskyttet felt. Følgende linjer vil sende en feil for hvert feilkommentert element under kildebehandlingsfasen:
otherMethods.forEach (element -> processingEnv.getMessager (). printMessage (Diagnostic.Kind.ERROR, "@BuilderProperty må brukes på en setXxx-metode" + "med et enkelt argument", element));
Selvfølgelig, hvis den riktige settersamlingen er tom, er det ingen vits å fortsette gjeldende iterasjonssett iterasjon:
if (setters.isEmpty ()) {fortsett; }
Hvis settersamlingen har minst ett element, skal vi bruke det til å hente det fullstendige klassenavnet fra det vedlagte elementet, som i tilfelle settermetoden ser ut til å være selve kildeklassen:
String className = ((TypeElement) setters.get (0) .getEnclosingElement ()). GetQualifiedName (). ToString ();
Den siste informasjonen vi trenger for å generere en byggeklasse, er et kart mellom navnene på setterne og navnene på deres argumenttyper:
Kart setterMap = setters.stream (). Samle (Collectors.toMap (setter -> setter.getSimpleName (). ToString (), setter -> ((ExecutableType) setter.asType ()). GetParameterTypes (). Get (0) .toString ()));
6.3. Genererer utdatafilen
Nå har vi all informasjonen vi trenger for å generere en byggeklasse: navnet på kildeklassen, alle setternavnene og argumenttypene deres.
For å generere utdatafilen bruker vi Filer forekomst igjen av objektet i AbstractProcessor.processingEnv beskyttet eiendom:
JavaFileObject builderFile = processingEnv.getFiler () .createSourceFile (builderClassName); prøv (PrintWriter out = new PrintWriter (builderFile.openWriter ())) {// skriver generert fil for å ut ...}
Den komplette koden til writeBuilderFile metoden er gitt nedenfor. Vi trenger bare å beregne pakkenavnet, fullt kvalifisert navn på byggeklassen og enkle klassenavn for kildeklassen og byggeklassen. Resten av koden er ganske grei.
private void writeBuilderFile (String className, Map setterMap) kaster IOException {String packageName = null; int lastDot = className.lastIndexOf ('.'); hvis (lastDot> 0) {packageName = className.substring (0, lastDot); } Streng simpleClassName = className.substring (lastDot + 1); String builderClassName = className + "Builder"; String builderSimpleClassName = builderClassName. Substring (lastDot + 1); JavaFileObject builderFile = processingEnv.getFiler () .createSourceFile (builderClassName); prøv (PrintWriter out = new PrintWriter (builderFile.openWriter ())) {if (packageName! = null) {out.print ("package"); out.print (packageName); out.println (";"); out.println (); } out.print ("offentlig klasse"); out.print (builderSimpleClassName); out.println ("{"); out.println (); out.print ("privat"); out.print (simpleClassName); out.print ("object = new"); out.print (simpleClassName); out.println ("();"); out.println (); out.print ("offentlig"); out.print (simpleClassName); out.println ("build () {"); out.println ("returobjekt;"); 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 ("value) {"); out.print ("object."); out. print (methodName); out.println ("(verdi);"); out.println ("returner dette;"); out.println ("}"); out.println ();}); out.println ("}"); }}
7. Kjører eksemplet
For å se kodegenerering i aksjon, bør du enten kompilere begge modulene fra den vanlige foreldreroten eller først kompilere merkeprosessor modulen og deretter kommentar-bruker modul.
Den genererte PersonBuilder klassen finner du inne i merknadsbruker / mål / genererte kilder / merknader / com / baeldung / merknad / PersonBuilder.java filen og skal se slik ut:
pakke com.baeldung.annotation; offentlig klasse PersonBuilder {privatpersonobjekt = ny person (); public Person build () {returobjekt; } offentlig PersonBuilder-settnavn (java.lang.String-verdi) {object.setName (verdi); returner dette; } offentlig PersonBuilder setAge (int-verdi) {object.setAge (verdi); returner dette; }}
8. Alternative måter å registrere en prosessor på
For å bruke kommentarprosessoren din under kompileringsfasen, har du flere andre alternativer, avhengig av brukssaken og verktøyene du bruker.
8.1. Ved hjelp av kommentarprosessorverktøyet
De apt verktøy var et spesielt kommandolinjeprogram for behandling av kildefiler. Det var en del av Java 5, men siden Java 7 ble det avviklet til fordel for andre alternativer og fjernet fullstendig i Java 8. Det vil ikke bli diskutert i denne artikkelen.
8.2. Bruke kompilatortasten
De -prosessor kompilatornøkkel er et standard JDK-anlegg for å utvide kildebehandlingsfasen til kompilatoren med din egen kommentarprosessor.
Merk at selve prosessoren og merknaden allerede skal kompileres som klasser i en egen samling og til stede på klassestien, så det første du bør gjøre er:
javac com / baeldung / kommentar / prosessor / BuilderProsessor javac com / baeldung / kommentar / prosessor / BuilderProperty
Så gjør du selve samlingen av kildene dine med -prosessor nøkkel som spesifiserer merkeprosessorklassen du nettopp har samlet:
javac -prosessor com.baeldung.annotation.processor.MyProcessor Person.java
For å spesifisere flere merkeprosessorer på en gang, kan du skille klassenavnene med komma, slik:
javac-prosessorpakke1.Prosessor1, pakke2.Prosessor2 SourceFile.java
8.3. Bruke Maven
De maven-compiler-plugin tillater spesifisering av merkeprosessorer som en del av konfigurasjonen.
Her er et eksempel på å legge til kommentarprosessor for kompilator-pluginet. Du kan også spesifisere katalogen du vil generere kilder i, ved hjelp av generatedSourcesDirectory konfigurasjonsparameter.
Merk at BuilderProsessor klasse skal allerede være kompilert, for eksempel importert fra en annen krukke i byggeavhengighetene:
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. Legge til en prosessorkrukke til Classpath
I stedet for å spesifisere merkeprosessoren i kompilatoralternativene, kan du bare legge til en spesielt strukturert krukke med prosessorklassen til kompilatorens klassebane.
For å plukke den opp automatisk, må kompilatoren vite navnet på prosessorklassen. Så du må spesifisere det i META-INF / services / javax.annotation.processing.Processor filen som et fullt kvalifisert klassenavn til prosessoren:
com.baeldung.annotation.processor.BuilderProcessor
Du kan også spesifisere flere prosessorer fra denne krukken for å plukke opp automatisk ved å skille dem med en ny linje:
pakke1.Prosessor1 pakke2.Prosessor2 pakke3.Prosessor3
Hvis du bruker Maven til å bygge denne krukken og prøver å legge denne filen direkte i src / main / resources / META-INF / services katalogen, vil du støte på følgende feil:
[FEIL] Dårlig tjenestekonfigurasjonsfil, eller unntak kastet mens du konstruerte prosessorobjekt: javax.annotation.processing.Processor: Provider com.baeldung.annotation.processor.BuilderProcessor ikke funnet
Dette er fordi kompilatoren prøver å bruke denne filen under kildebehandling fasen av selve modulen når BuilderProsessor filen er ennå ikke kompilert. Filen må enten plasseres i en annen ressurskatalog og kopieres til META-INF / tjenester katalog under ressurskopieringsfasen av Maven-bygningen, eller (enda bedre) generert under byggingen.
Google autotjeneste biblioteket, diskutert i det følgende avsnittet, gjør det mulig å generere denne filen ved hjelp av en enkel kommentar.
8.5. Bruke Google autotjeneste Bibliotek
For å generere registreringsfilen automatisk, kan du bruke @AutoService kommentar fra Googles autotjeneste bibliotek, slik:
@AutoService (Processor.class) public BuilderProcessor utvider AbstractProcessor {//…}
Denne merknaden behandles i seg selv av merkeprosessoren fra autotjenestebiblioteket. Denne prosessoren genererer META-INF / services / javax.annotation.processing.Processor filen som inneholder BuilderProsessor klassenavn.
9. Konklusjon
I denne artikkelen har vi demonstrert kommentarbehandling på kildenivå ved hjelp av et eksempel på å generere en Builder-klasse for en POJO. Vi har også gitt flere alternative måter å registrere merkeprosessorer på i prosjektet ditt.
Kildekoden for artikkelen er tilgjengelig på GitHub.