Oprettelse af et Java Compiler-plugin

1. Oversigt

Java 8 giver en API til oprettelse Javac plugins. Desværre er det svært at finde god dokumentation til det.

I denne artikel skal vi vise hele processen med at oprette en kompilérudvidelse, der tilføjer brugerdefineret kode til * .klasse filer.

2. Opsætning

Først skal vi tilføje JDK'er tools.jar som en afhængighed for vores projekt:

 com.sun tools 1.8.0 system $ {java.home} /../ lib / tools.jar 

Hver kompilérudvidelse er en klasse, der implementeres com.sun.source.util.Plugin interface. Lad os oprette det i vores eksempel:

Lad os oprette det i vores eksempel:

offentlig klasse SampleJavacPlugin implementerer plugin {@ Override public String getName () {return "MyPlugin"; } @ Overstyr offentlig ugyldig init (JavacTask-opgave, String ... args) {Context context = ((BasicJavacTask) task) .getContext (); Log.instance (kontekst) .printRawLines (Log.WriterKind.NOTICE, "Hej fra" + getName ()); }}

For øjeblikket udskriver vi bare "Hej" for at sikre, at vores kode med succes hentes og inkluderes i udarbejdelsen.

Vores endelige mål er at oprette et plugin, der tilføjer runtime-kontrol for hvert numerisk argument markeret med en given kommentar og kaste en undtagelse, hvis argumentet ikke matcher en betingelse.

Der er endnu et nødvendigt trin for at gøre udvidelsen synlig af Javac:den skal eksponeres gennem ServiceLoader ramme.

For at opnå dette er vi nødt til at oprette en fil med navnet com.sun.source.util.Plugin med indhold, som er vores plugins fuldt kvalificerede klassenavn (com.baeldung.javac.SampleJavacPlugin) og placer den i META-INF / tjenester vejviser.

Derefter kan vi ringe Javac med -Xplugin: MyPlugin kontakt:

baeldung / tutorials $ javac -cp ./core-java/target/classes -Xplugin: MyPlugin ./core-java/src/main/java/com/baeldung/javac/TestClass.java Hej fra MyPlugin

Noter det vi skal altid bruge en Snor returneret fra plugins getName () metode som en -Xplugin option værdi.

3. Plugin-livscyklus

EN plugin kaldes kun af compileren en gang gennem i det() metode.

For at blive underrettet om efterfølgende begivenheder skal vi registrere et tilbagekald. Disse ankommer før og efter hvert behandlingstrin pr. Kildefil:

  • PARSE - bygger en Abstrakt syntaks træ (AST)
  • GÅ IND - kildekodeimport er løst
  • ANALYSERE - parseroutput (en AST) analyseres for fejl
  • FREMBRINGE - generere binære filer til målkildefilen

Der er to andre begivenhedstyper - ANNOTATION_PROCESSING og ANNOTATION_PROCESSING_ROUND men vi er ikke interesserede i dem her.

For eksempel, når vi ønsker at forbedre kompilering ved at tilføje nogle kontroller baseret på kildekodeinfo, er det rimeligt at gøre det på PARSE færdig begivenhedshåndterer:

public void init (JavacTask task, String ... args) {task.addTaskListener (new TaskListener () {public void started (TaskEvent e) {} public ugyldig færdig (TaskEvent e) {if (e.getKind ()! = TaskEvent .Kind.PARSE) {return;} // Udfør instrumentering}}); }

4. Uddrag AST-data

Vi kan få en AST genereret af Java-kompilatoren gennem TaskEvent.getCompilationUnit (). Dens detaljer kan undersøges gennem TreeVisitor interface.

Bemærk, at kun a Træ element, for hvilket acceptere() metode kaldes, sender begivenheder til den givne besøgende.

For eksempel når vi udfører ClassTree.accept (besøgende), kun visitClass () udløses; det kan vi ikke forvente, siger visitMethod () aktiveres også for hver metode i den givne klasse.

Vi kan bruge TreeScanner for at overvinde problemet:

offentlig ugyldig færdig (TaskEvent e) {if (e.getKind ()! = TaskEvent.Kind.PARSE) {return; } e.getCompilationUnit (). accept (nyt TreeScanner () {@Override public Void visitClass (ClassTree node, Void aVoid) {return super.visitClass (node, aVoid); @Override public Void visitMethod (MethodTree node, Void aVoid) { returner super.visitMethod (node, aVoid);}}, null); }

I dette eksempel er det nødvendigt at ringe super.visitXxx (node, værdi) til rekursivt at behandle den aktuelle knudepunkts børn.

5. Rediger AST

For at vise, hvordan vi kan ændre AST, indsætter vi runtime-kontrol for alle numeriske argumenter markeret med a @Positiv kommentar.

Dette er en simpel kommentar, der kan anvendes på metodeparametre:

@Documented @Retention (RetentionPolicy.CLASS) @Target ({ElementType.PARAMETER}) offentlig @interface Positiv {}

Her er et eksempel på brug af kommentaren:

offentlig ugyldighedstjeneste (@Positive int i) {}

I sidste ende ønsker vi, at bytekoden skal se ud som om den er sammensat fra en kilde som denne:

public void service (@Positive int i) {if (i <= 0) {throw new IllegalArgumentException ("Et ikke-positivt argument (" + i + ") gives som en @Positiv parameter 'i'"); }}

Hvad dette betyder er, at vi ønsker en IllegalArgumentException at blive kastet for hvert argument markeret med @Positiv som er lig med eller mindre end 0.

5.1. Hvor skal man instrumentere?

Lad os finde ud af, hvordan vi kan finde målsteder, hvor instrumenteringen skal anvendes:

privat statisk sæt TARGET_TYPES = Stream.of (byte.class, short.class, char.class, int.class, long.class, float.class, double.class) .map (Class :: getName) .collect (Collectors. at sætte()); 

For enkelheds skyld har vi kun tilføjet primitive numeriske typer her.

Lad os derefter definere en shouldInstrument () metode, der kontrollerer, om parameteren har en type i TARGET_TYPES-sættet såvel som @Positiv kommentar:

private boolean shouldInstrument (VariableTree parameter) {return TARGET_TYPES.contains (parameter.getType (). toString ()) && parameter.getModifiers (). getAnnotations (). stream () .anyMatch (a -> Positive.class.getSimpleName () .equals (a.getAnnotationType (). toString ())); }

Så fortsætter vi færdig() metode i vores PrøveJavacPlugin klasse med anvendelse af en kontrol på alle parametre, der opfylder vores betingelser:

offentlig tomrum færdig (TaskEvent e) {if (e.getKind ()! = TaskEvent.Kind.PARSE) {return; } e.getCompilationUnit (). accept (nyt TreeScanner () {@Override public Void visitMethod (MethodTree method, Void v) {List parametersToInstrument = method.getParameters (). stream () .filter (SampleJavacPlugin.this :: shouldInstrument). indsamle (Collectors.toList ()); hvis (! parametersToInstrument.isEmpty ()) {Collections.reverse (parametersToInstrument); parametersToInstrument.forEach (p -> addCheck (metode, p, kontekst));} returner super.visitMethod (metode , v);}}, null); 

I dette eksempel har vi vendt parameterlisten, fordi der er en mulig sag, at mere end et argument er markeret med @Positiv. Da hver check tilføjes som den allerførste metodeinstruktion, behandler vi dem RTL for at sikre den korrekte rækkefølge.

5.2. Sådan instrumenteres

Problemet er, at "læs AST" ligger i offentlig API-område, mens "modificer AST" -operationer som "tilføj null-checks" er en privat API.

For at løse dette, vi opretter nye AST-elementer gennem en TreeMaker eksempel.

Først skal vi skaffe en Sammenhæng eksempel:

@Override public void init (JavacTask task, String ... args) {Context context = ((BasicJavacTask) task) .getContext (); // ...}

Derefter kan vi få TreeMarker objekt gennem TreeMarker.instance (kontekst) metode.

Nu kan vi bygge nye AST-elementer, f.eks. En hvis udtryk kan konstrueres ved et kald til TreeMaker.If ():

privat statisk JCTree.JCIf createCheck (Parameter VariableTree, Kontekstkontekst) {TreeMaker fabrik = TreeMaker.instance (kontekst); Navne symbolsTable = Names.instance (kontekst); returner factory.at ((((JCTree) parameter) .pos) .If (fabrik.Parens (createIfCondition (fabrik, symbolerTabel, parameter)), createIfBlock (fabrik, symbolerTabel, parameter), null); }

Bemærk, at vi ønsker at vise den korrekte staksporingslinje, når en undtagelse kastes fra vores check. Derfor justerer vi AST-fabrikspositionen, før vi opretter nye elementer igennem den med factory.at ((((JCTree) parameter) .pos).

Det createIfCondition () metoden bygger “parameterId< 0″ hvis tilstand:

privat statisk JCTree.JCBinary createIfCondition (TreeMaker fabrik, Navne symbolerTabel, VariableTree parameter) {Navn parameterId = symbolerTabel.fromString (parameter.getName (). toString ()); returner fabrik.Binær (JCTree.Tag.LE, fabrik.Ident (parameterId), fabrik.Literal (TypeTag.INT, 0)); }

Dernæst createIfBlock () metoden bygger en blok, der returnerer en IllegalArgumentException:

privat statisk JCTree.JCBlock createIfBlock (TreeMaker fabrik, Navne symbolerTabel, VariableTree parameter) {String parameterName = parameter.getName (). toString (); Navn parameterId = symbolerTabel.fromString (parameternavn); String errorMessagePrefix = String.format ("Argument '% s' af typen% s er markeret med @% s men fik '", parameternavn, parameter.getType (), Positive.class.getSimpleName ()); String errorMessageSuffix = "'for det"; returner fabrik.Blok (0, com.sun.tools.javac.util.List.of (factory.Throw (factory.NewClass (null, nil (), factory.Ident (symbolerTabel.fromString (IllegalArgumentException.class.getSimpleName () )), com.sun.tools.javac.util.List.of (fabrik.Binær (JCTree.Tag.PLUS, fabrik.Binær (JCTree.Tag.PLUS, fabrik.Literal (TypeTag.CLASS, errorMessagePrefix), fabrik. Ident (parameterId)), fabrik.Literal (TypeTag.CLASS, errorMessageSuffix))), null)))); }

Nu hvor vi er i stand til at opbygge nye AST-elementer, skal vi indsætte dem i AST udarbejdet af parseren. Vi kan opnå dette ved at støbe offentlig ENPI elementer til privat API-typer:

private void addCheck (MethodTree-metode, VariableTree-parameter, Kontekstkontekst) {JCTree.JCIf check = createCheck (parameter, kontekst); JCTree.JCBlock body = (JCTree.JCBlock) method.getBody (); body.stats = body.stats.prepend (check); }

6. Test af pluginet

Vi skal være i stand til at teste vores plugin. Det involverer følgende:

  • kompilere testkilden
  • køre de kompilerede binære filer og sikre, at de opfører sig som forventet

Til dette er vi nødt til at introducere et par hjælpeklasser.

SimpleSourceFile udsætter den givne kildefils tekst for Javac:

offentlig klasse SimpleSourceFile udvider SimpleJavaFileObject {private strengindhold; offentlig SimpleSourceFile (streng kvalificeret klasse navn, streng test kilde) {super (URI.create (String.format ("fil: //% s% s", kvalificeret klasse.replaceAll ("\.", "/"), Kind.SOURCE. udvidelse)), Kind.SOURCE); indhold = testKilde; } @ Override public CharSequence getCharContent (boolean ignoreEncodingErrors) {returner indhold; }}

SimpleClassFile holder kompileringsresultatet som et byte-array:

offentlig klasse SimpleClassFile udvider SimpleJavaFileObject {privat ByteArrayOutputStream ud; offentlig SimpleClassFile (URI uri) {super (uri, Kind.CLASS); } @ Override public OutputStream openOutputStream () kaster IOException {return out = new ByteArrayOutputStream (); } offentlig byte [] getCompiledBinaries () {return out.toByteArray (); } // getters}

SimpleFileManager sikrer, at compileren bruger vores bytecode-holder:

offentlig klasse SimpleFileManager udvider ForwardingJavaFileManager {private List compiled = new ArrayList (); // standardkonstruktører / getters @ Override offentlige JavaFileObject getJavaFileForOutput (Placeringsplacering, String className, JavaFileObject.Kind kind, FileObject søskende) {SimpleClassFile resultat = nyt SimpleClassFile (URI.create ("streng: //" + className)); compiled.add (resultat); returresultat } offentlig liste getCompiled () {return compiled; }}

Endelig er alt dette bundet til kompilering i hukommelsen:

public class TestCompiler {public byte [] compile (String QualifiedClassName, String testSource) {StringWriter output = new StringWriter (); JavaCompiler compiler = ToolProvider.getSystemJavaCompiler (); SimpleFileManager fileManager = ny SimpleFileManager (compiler.getStandardFileManager (null, null, null)); Liste compilationUnits = singletonList (ny SimpleSourceFile (kvalificeretClassName, testSource)); Liste argumenter = ny ArrayList (); argumenter.addAll (asList ("- classpath", System.getProperty ("java.class.path"), "-Xplugin:" + SampleJavacPlugin.NAME)); JavaCompiler.CompilationTask task = compiler.getTask (output, fileManager, null, argumenter, null, compilationUnits); task.call (); returner FileManager.getCompiled (). iterator (). næste (). getCompiledBinaries (); }}

Derefter behøver vi kun at køre binærfiler:

offentlig klasse TestRunner {public Object run (byte [] byteCode, String qualifiedClassName, String methodName, Class [] argumentTypes, Object ... args) throw Throwable {ClassLoader classLoader = new ClassLoader () {@Override protected Class findClass (String name) kaster ClassNotFoundException {return defineClass (navn, byteCode, 0, byteCode.length); }}; Klassecazz; prøv {clazz = classLoader.loadClass (qualifiedClassName); } catch (ClassNotFoundException e) {throw new RuntimeException ("Kan ikke indlæse kompileret testklasse", e); } Metode metode prøv {method = clazz.getMethod (methodName, argumentTypes); } catch (NoSuchMethodException e) {throw new RuntimeException ("Can't find the 'main ()' method in the compiled test class", e); } prøv {return method.invoke (null, args); } fange (InvocationTargetException e) {throw e.getCause (); }}}

En test kan se sådan ud:

public class SampleJavacPluginTest {private static final String CLASS_TEMPLATE = "pakke com.baeldung.javac; \ n \ n" + "Public class Test {\ n" + "public static% 1 $ s service (@Positive% 1 $ si) { \ n "+" returnerer i; \ n "+"} \ n "+"} \ n "+" "; privat TestCompiler compiler = ny TestCompiler (); privat TestRunner-løber = ny TestRunner (); @Test (forventet = IllegalArgumentException.class) offentlig ugyldighed givenInt_whenNegative_thenThrowsException () kaster Throwable {compileAndRun (double.class, -1); } privat objekt compileAndRun (klasse argumentType, objekt argument) kaster kastbar {String kvalificeret klasse = "com.baeldung.javac.Test"; byte [] byteCode = compiler.compile (kvalificeretClassName, String.format (CLASS_TEMPLATE, argumentType.getName ())); returner runner.run (byteCode, QualifiedClassName, "service", ny klasse [] {argumentType}, argument); }}

Her sammensætter vi en Prøve klasse med en service() metode, der har en parameter, der er kommenteret med @Positiv. Så kører vi Prøve klasse ved at indstille en dobbelt værdi på -1 for metodeparameteren.

Som et resultat af at køre compileren med vores plugin, kaster testen en IllegalArgumentException for den negative parameter.

7. Konklusion

I denne artikel har vi vist hele processen med at oprette, teste og køre et Java Compiler-plugin.

Den fulde kildekode for eksemplerne kan findes på GitHub.


$config[zx-auto] not found$config[zx-overlay] not found