Java-kommentarbehandling og oprettelse af en Builder

1. Introduktion

Denne artikel er en introduktion til Java-annoteringsbehandling på kildeniveau og giver eksempler på brug af denne teknik til at generere yderligere kildefiler under kompilering.

2. Anvendelser af annoteringsbehandling

Behandling af annotations på kildeniveau dukkede først op i Java 5. Det er en praktisk teknik til at generere yderligere kildefiler under kompileringsfasen.

Kildefilerne behøver ikke at være Java-filer - du kan generere nogen form for beskrivelse, metadata, dokumentation, ressourcer eller andre typer filer baseret på kommentarer i din kildekode.

Annotationsbehandling bruges aktivt i mange allestedsnærværende Java-biblioteker, for eksempel til at generere metaclasses i QueryDSL og JPA, for at udvide klasser med kedelpladekode i Lombok-biblioteket.

En vigtig ting at bemærke er begrænsningen af ​​annoteringsbehandlings-API'en - den kan kun bruges til at generere nye filer, ikke til at ændre eksisterende.

Den bemærkelsesværdige undtagelse er Lombok-biblioteket, der bruger annoteringsbehandling som en bootstrapping-mekanisme til at inkludere sig selv i kompileringsprocessen og ændre AST via nogle interne kompilator-API'er. Denne hacky-teknik har intet at gøre med det tilsigtede formål med annoteringsbehandling og diskuteres derfor ikke i denne artikel.

3. Annotation Processing API

Annoteringsbehandlingen udføres i flere runder. Hver runde starter med, at compileren søger efter kommentarerne i kildefilerne og vælger de annoteringsprocessorer, der passer til disse kommentarer. Hver annoteringsprocessor kaldes til gengæld til de tilsvarende kilder.

Hvis der genereres filer under denne proces, startes en anden runde med de genererede filer som input. Denne proces fortsætter, indtil der ikke genereres nye filer under behandlingsfasen.

Hver annoteringsprocessor kaldes til gengæld til de tilsvarende kilder. Hvis der genereres filer under denne proces, startes en anden runde med de genererede filer som input. Denne proces fortsætter, indtil der ikke genereres nye filer under behandlingsfasen.

Annotationsbehandlings-API'en er placeret i javax.annotation.processing pakke. Den vigtigste grænseflade, du bliver nødt til at implementere, er Processor interface, som har en delvis implementering i form af AbstraktProcessor klasse. Denne klasse er den, vi vil udvide til at oprette vores egen kommentarprocessor.

4. Opsætning af projektet

For at demonstrere mulighederne for behandling af annoteringer udvikler vi en simpel processor til generering af flydende objektbyggeri til annoterede klasser.

Vi vil opdele vores projekt i to Maven-moduler. En af dem, annoteringsprocessor modul, indeholder selve processoren sammen med kommentaren, og en anden, kommentar-bruger modul, indeholder den kommenterede klasse. Dette er en typisk anvendelse af annoteringsbehandling.

Indstillingerne for annoteringsprocessor modul er som følger. Vi skal bruge Googles autotjeneste-bibliotek til at generere processor-metadatafil, som vil blive diskuteret senere og maven-compiler-plugin indstillet til Java 8 kildekoden. Versionerne af disse afhængigheder ekstraheres til egenskabsafsnittet.

Seneste versioner af auto-service biblioteket og maven-compiler-plugin kan findes i Maven Central repository:

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

Det kommentar-bruger Maven-modul med de annoterede kilder har ikke brug for nogen speciel tuning, bortset fra at tilføje en afhængighed af annotationsprocessormodulet i afsnittet om afhængigheder:

 com.baeldung kommentar-behandling 1.0.0-SNAPSHOT 

5. Definition af en kommentar

Antag, at vi har en simpel POJO-klasse i vores annotationsbruger modul med flere felter:

offentlig klasseperson {privat int alder; privat strengnavn; // getters og setters ...}

Vi ønsker at oprette en bygherrehjælperklasse til at instantiere Person klasse mere flydende:

Person person = ny PersonBuilder () .setAge (25) .setName ("John") .build ();

Det her PersonBuilder klasse er et oplagt valg for en generation, da dens struktur er fuldstændigt defineret af Person setter metoder.

Lad os oprette en @BuilderProperty kommentar i annoteringsprocessor modul til settermetoderne. Det giver os mulighed for at generere Bygger klasse for hver klasse, der har sine sættermetoder kommenteret:

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

Det @Mål kommentar med ElementType.METHOD parameter sikrer, at denne kommentar kun kan sættes på en metode.

Det KILDE fastholdelsespolitik betyder, at denne kommentar kun er tilgængelig under kildebehandling og ikke er tilgængelig under kørsel.

Det Person klasse med egenskaber kommenteret med @BuilderProperty kommentar ser ud som følger:

offentlig klasseperson {privat int alder; privat strengnavn; @BuilderProperty ugyldigt setAge (int age) {this.age = age; } @BuilderProperty public void setName (strengnavn) {this.name = navn; } // getters ...}

6. Implementering a Processor

6.1. Oprettelse af en AbstraktProcessor Underklasse

Vi starter med at udvide AbstraktProcessor klasse inde i annoteringsprocessor Maven-modul.

Først skal vi specificere kommentarer, som denne processor er i stand til at behandle, og også den understøttede kildekodeversion. Dette kan gøres enten ved at implementere metoderne getSupportedAnnotationTypes og getSupportedSourceVersion af Processor interface eller ved at kommentere din klasse med @SupportedAnnotationTypes og @SupportedSourceVersion kommentarer.

Det @AutoService kommentar er en del af auto-service bibliotek og giver mulighed for at generere processorens metadata, som vil blive forklaret i de følgende afsnit.

@SupportedAnnotationTypes ("com.baeldung.annotation.processor.BuilderProperty") @SupportedSourceVersion (SourceVersion.RELEASE_8) @AutoService (Processor.class) public class BuilderProcessor udvider AbstractProcessor {@Override-public-bean-processer ; }}

Du kan ikke kun angive navnene på de konkrete annoteringsklasser, men også jokertegn som f.eks “Com.baeldung.annotation. *” til at behandle annoteringer inde i com.baeldung.annotation pakke og alle dens underpakker eller endda “*” til at behandle alle kommentarer.

Den eneste metode, som vi bliver nødt til at implementere, er behandle metode, der udfører selve behandlingen. Det kaldes af kompilatoren for hver kildefil, der indeholder de matchende annoteringer.

Kommentarer sendes som den første Indstil annoteringer argument, og oplysningerne om den aktuelle behandlingsrunde videregives som RoundEnviroment roundEnv argument.

Returneringen boolsk værdi skal være rigtigt hvis din annoteringsprocessor har behandlet alle de godkendte annoteringer, og du ikke ønsker, at de skal sendes til andre annoteringsprocessorer nede på listen.

6.2. Indsamling af data

Vores processor gør ikke rigtig noget nyttigt endnu, så lad os udfylde det med kode.

Først skal vi gentage alle annoteringstyper, der findes i klassen - i vores tilfælde er kommentarer sæt vil have et enkelt element svarende til @BuilderProperty kommentar, selvom denne kommentar forekommer flere gange i kildefilen.

Alligevel er det bedre at implementere behandle metode som en iterationscyklus, for fuldstændighedens skyld:

@Override offentlig boolsk proces (Sæt annoteringer, RoundEnvironment roundEnv) {for (TypeElement-annotation: annotations) {Set annotatedElements = roundEnv.getElementsAnnotatedWith (annotation); // ...} returner sandt; }

I denne kode bruger vi Runde miljø instans for at modtage alle elementer, der er kommenteret med @BuilderProperty kommentar. I tilfælde af Person klasse svarer disse elementer til sætnavn og setAge metoder.

@BuilderProperty annotationsbruger kunne fejlagtigt kommentere metoder, der ikke er settere. Setter-metodenavnet skal starte med sæt, og metoden skal modtage et enkelt argument. Så lad os adskille hveden fra agnet.

I den følgende kode bruger vi Collectors.partitioningBy () samler til at opdele kommenterede metoder i to samlinger: korrekt kommenterede settere og andre fejlagtigt kommenterede metoder:

Kort annotatedMethods = annotatedElements.stream (). collect (Collectors.partitioningBy (element -> ((ExecutableType) element.asType ()). getParameterTypes (). size () == 1 && element.getSimpleName (). toString (). startsWith ("sæt"))); List setters = annotatedMethods.get (true); Liste otherMethods = annotatedMethods.get (false);

Her bruger vi Element.asType () metode til at modtage en forekomst af Type Spejl klasse, der giver os nogle muligheder for at introspektere typer, selvom vi kun er på kildebehandlingsstadiet.

Vi bør advare brugeren om forkert annoterede metoder, så lad os bruge Messager eksempel tilgængelig fra AbstractProcessor.processingEnv beskyttet felt. Følgende linjer udsender en fejl for hvert fejlagtigt kommenteret element under kildebehandlingstrinnet:

otherMethods.forEach (element -> processingEnv.getMessager (). printMessage (Diagnostic.Kind.ERROR, "@BuilderProperty skal anvendes på en setXxx-metode" + "med et enkelt argument", element));

Selvfølgelig, hvis den korrekte sættersamling er tom, er der ingen mening med at fortsætte det aktuelle type element-sæt iteration:

hvis (setters.isEmpty ()) {fortsæt; }

Hvis setters-samlingen har mindst et element, skal vi bruge det til at hente det fuldt kvalificerede klassenavn fra det vedlagte element, som i tilfælde af setter-metoden ser ud til at være selve kildeklassen:

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

Den sidste information, vi har brug for til at generere en builder-klasse, er et kort mellem navnene på setterne og navnene på deres argumenttyper:

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

6.3. Generering af outputfilen

Nu har vi alle de oplysninger, vi har brug for til at generere en builder-klasse: navnet på kildeklassen, alle dens setternavne og deres argumenttyper.

For at generere outputfilen bruger vi Filer forekomst leveres igen af ​​objektet i AbstractProcessor.processingEnv beskyttet ejendom:

JavaFileObject builderFile = processingEnv.getFiler () .createSourceFile (builderClassName); prøv (PrintWriter ud = ny PrintWriter (builderFile.openWriter ())) {// skriver genereret fil for at ud ...}

Den komplette kode for writeBuilderFile metoden er angivet nedenfor. Vi behøver kun at beregne pakkenavnet, fuldt kvalificerede navn på builderklassen og enkle klassenavne for kildeklassen og builderklassen. Resten af ​​koden er ret ligetil.

privat ugyldigt 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 ("objekt = nyt"); out.print (simpleClassName); out.println ("();"); out.println (); out.print ("offentlig"); out.print (simpleClassName); out.println ("build () {"); out.println ("return 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 ("value) {"); out.print ("object."); out. print (methodName); out.println ("(værdi);"); out.println ("returner dette;"); out.println ("}"); out.println ();}); out.println ("}"); }}

7. Kørsel af eksemplet

For at se kodegenerering i aktion skal du enten kompilere begge moduler fra den fælles overordnede rod eller først kompilere annoteringsprocessor modul og derefter kommentar-bruger modul.

Den genererede PersonBuilder klasse kan findes inde i annotationsbruger / mål / genererede kilder / annotationer / com / baeldung / annotation / PersonBuilder.java fil og skal se sådan ud:

pakke com.baeldung.annotation; offentlig klasse PersonBuilder {privat person objekt = ny person (); public Person build () {return object; } offentlig PersonBuilder-sætnavn (java.lang.String-værdi) {object.setName (værdi); returner dette; } offentlig PersonBuilder setAge (int-værdi) {object.setAge (værdi); returner dette; }}

8. Alternative måder at registrere en processor på

For at bruge din annoteringsprocessor i kompileringsfasen har du flere andre muligheder afhængigt af din brugssag og de værktøjer, du bruger.

8.1. Brug af Annotation Processor Tool

Det apt værktøj var et specielt kommandolinjeprogram til behandling af kildefiler. Det var en del af Java 5, men siden Java 7 blev det udfaset til fordel for andre muligheder og fjernet fuldstændigt i Java 8. Det vil ikke blive diskuteret i denne artikel.

8.2. Brug af Compiler Key

Det -processor compiler-nøgle er en standard JDK-facilitet til at udvide kildebehandlingsfasen af ​​compileren med din egen annoteringsprocessor.

Bemærk, at selve processoren og kommentaren allerede skal kompileres som klasser i en separat kompilering og findes på klassestien, så det første du skal gøre er:

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

Så gør du den faktiske kompilering af dine kilder med -processor nøgle, der angiver den annoteringsprocessorklasse, du lige har samlet:

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

For at specificere flere annoteringsprocessorer på én gang kan du adskille deres klassenavne med kommaer på denne måde:

javac -processorpakke1.Processor1, pakke2.Processor2 SourceFile.java

8.3. Brug af Maven

Det maven-compiler-plugin tillader angivelse af annoteringsprocessorer som en del af konfigurationen.

Her er et eksempel på tilføjelse af annoteringsprocessor til compiler-pluginet. Du kan også angive den mappe, som genererede kilder skal placeres i, ved hjælp af generatedSourcesDirectory konfigurationsparameter.

Bemærk, at BuilderProcessor klasse skal allerede være kompileret, for eksempel importeret fra en anden krukke i buildafhængighederne:

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

8.4. Tilføjelse af en processorkrukke til Classpath

I stedet for at specificere annoteringsprocessoren i kompilatorindstillingerne kan du blot tilføje en specielt struktureret krukke med processorklassen til kompilatorens klassesti.

For at hente det automatisk skal compileren kende navnet på processorklassen. Så du skal angive det i META-INF / services / javax.annotation.processing.Processor fil som et fuldt kvalificeret klasse navn på processoren:

com.baeldung.annotation.processor.BuilderProcessor

Du kan også angive flere processorer fra denne krukke, der skal afhentes automatisk ved at adskille dem med en ny linje:

pakke1.Processor1 pakke2.Processor2 pakke3.Processor3

Hvis du bruger Maven til at bygge denne krukke og prøver at placere denne fil direkte i src / main / resources / META-INF / services bibliotek, vil du støde på følgende fejl:

[FEJL] Dårlig servicekonfigurationsfil eller undtagelse kastet under konstruktion af processorobjekt: javax.annotation.processing.Processor: Udbyder com.baeldung.annotation.processor.BuilderProcessor ikke fundet

Dette skyldes, at compileren forsøger at bruge denne fil under kildebehandling fase af selve modulet, når BuilderProcessor filen er endnu ikke kompileret. Filen skal enten placeres i en anden ressourcebibliotek og kopieres til META-INF / tjenester -mappe under ressourcekopieringstrinnet i Maven-buildet eller (endnu bedre) genereret under build.

Google auto-service bibliotek, der diskuteres i det følgende afsnit, tillader generering af denne fil ved hjælp af en simpel kommentar.

8.5. Brug af Google auto-service Bibliotek

For at generere registreringsfilen automatisk kan du bruge @AutoService kommentar fra Googles auto-service bibliotek, som dette:

@AutoService (Processor.class) offentlig BuilderProcessor udvider AbstractProcessor {//…}

Denne kommentar behandles i sig selv af kommentarprocessoren fra autotjenestebiblioteket. Denne processor genererer META-INF / services / javax.annotation.processing.Processor fil, der indeholder BuilderProcessor klasse navn.

9. Konklusion

I denne artikel har vi demonstreret annoteringsbehandling på kildeniveau ved hjælp af et eksempel på generering af en Builder-klasse til en POJO. Vi har også leveret flere alternative måder at registrere annoteringsprocessorer på i dit projekt.

Kildekoden til artiklen er tilgængelig på GitHub.