Opprette et Java Compiler Plugin

1. Oversikt

Java 8 gir et API for oppretting Javac plugins. Dessverre er det vanskelig å finne god dokumentasjon for det.

I denne artikkelen skal vi vise hele prosessen med å lage en kompilérutvidelse som legger til tilpasset kode til *.klasse filer.

2. Oppsett

Først må vi legge til JDK-er tools.jar som en avhengighet for prosjektet vårt:

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

Hver kompilatorutvidelse er en klasse som implementeres com.sun.source.util.Plugin grensesnitt. La oss lage det i vårt eksempel:

La oss lage det i vårt eksempel:

offentlig klasse SampleJavacPlugin implementerer Plugin {@Override public String getName () {return "MyPlugin"; } @ Override public void init (JavacTask task, String ... args) {Context context = ((BasicJavacTask) task) .getContext (); Log.instance (kontekst) .printRawLines (Log.WriterKind.NOTICE, "Hello from" + getName ()); }}

Foreløpig skriver vi bare ut "Hei" for å sikre at koden vår blir hentet og inkludert i samlingen.

Vårt endelige mål vil være å lage et plugin som legger til kjøretidskontroller for hvert numerisk argument som er merket med en gitt kommentar, og kaste et unntak hvis argumentet ikke samsvarer med en betingelse.

Det er enda et nødvendig trinn for å gjøre utvidelsen synlig av Javac:den skal eksponeres gjennom ServiceLoader rammeverk.

For å oppnå dette må vi opprette en fil med navnet com.sun.source.util.Plugin med innhold som er pluginens fullt kvalifiserte klassenavn (com.baeldung.javac.SampleJavacPlugin) og plasser den i META-INF / tjenester katalog.

Etter det kan vi ringe Javac med -Xplugin: MyPlugin bytte om:

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

Noter det vi må alltid bruke en String returnert fra plugin-ene getName () metode som en -Xplugin alternativverdi.

3. Plugins livssyklus

EN plugin kalles av kompilatoren bare én gang, gjennom i det() metode.

For å bli varslet om påfølgende hendelser, må vi registrere en tilbakeringing. Disse ankommer før og etter hvert behandlingsstadium per kildefil:

  • PARSE - bygger en Abstrakt syntaks tre (AST)
  • TAST INN - kildekodeimport er løst
  • ANALYSERE - parserutgang (en AST) blir analysert for feil
  • GENERERE - generere binærfiler for målkildefilen

Det er to andre typer arrangementer - ANNOTATION_PROCESSING og ANNOTATION_PROCESSING_ROUND men vi er ikke interessert i dem her.

For eksempel, når vi ønsker å forbedre kompilering ved å legge til noen sjekker basert på informasjon om kildekoden, er det rimelig å gjøre det på PARSE ferdig hendelsesbehandler:

public void init (JavacTask task, String ... args) {task.addTaskListener (new TaskListener () {public void started (TaskEvent e) {} public void ferdig (TaskEvent e) {if (e.getKind ()! = TaskEvent .Kind.PARSE) {retur;} // Utfør instrumentering}}); }

4. Pakk ut AST-data

Vi kan få en AST generert av Java-kompilatoren gjennom TaskEvent.getCompilationUnit (). Detaljene kan undersøkes gjennom TreeVisitor grensesnitt.

Merk at bare a Tre element, for hvilket aksepterer() metoden kalles, sender hendelser til den besøkende.

For eksempel når vi kjører ClassTree.accept (besøkende), kun visitClass () utløses; vi kan ikke forvente det, si, visitMethod () er også aktivert for hver metode i den gitte klassen.

Vi kan bruke TreeScanner for å løse problemet:

offentlig ugyldig ferdig (TaskEvent e) {if (e.getKind ()! = TaskEvent.Kind.PARSE) {return; } e.getCompilationUnit (). godta (nytt 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 eksemplet er det nødvendig å ringe super.visitXxx (node, verdi) for å behandle den nåværende nodenes barn rekursivt.

5. Endre AST

For å vise hvordan vi kan endre AST, setter vi inn kjøretidskontroller for alle numeriske argumenter merket med a @Positive kommentar.

Dette er en enkel kommentar som kan brukes på metodeparametere:

@Documented @Retention (RetentionPolicy.CLASS) @Target ({ElementType.PARAMETER}) public @interface Positive {}

Her er et eksempel på bruk av merknaden:

offentlig ugyldighetstjeneste (@Positive int i) {}

Til slutt vil vi at bytekoden skal se ut som om den er samlet fra en kilde som denne:

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

Hva dette betyr er at vi vil ha en IllegalArgumentException å bli kastet for hvert argument som er merket med @Positive som er lik eller mindre enn 0.

5.1. Hvor skal man instrumentere?

La oss finne ut hvordan vi kan finne målsteder der instrumentasjonen skal brukes:

private static Set TARGET_TYPES = Stream.of (byte.class, short.class, char.class, int.class, long.class, float.class, double.class) .map (Class :: getName) .collect (Collectors. å sette()); 

For enkelhets skyld har vi bare lagt til primitive numeriske typer her.

Deretter la oss definere en shouldInstrument () metode som sjekker om parameteren har en type i TARGET_TYPES-settet, så vel som @Positive 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å fortsetter vi ferdig () metode i vår EksempelJavacPlugin klasse med å bruke en sjekk på alle parametere som oppfyller våre betingelser:

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

I dette eksemplet har vi reversert parameterlisten fordi det er mulig at mer enn ett argument er merket av @Positive. Siden hver sjekk er lagt til som den aller første metodeinstruksjonen, behandler vi dem RTL for å sikre riktig rekkefølge.

5.2. Hvordan instrumentere

Problemet er at "les AST" ligger i offentlig API-området, mens "modifisere AST" -operasjoner som "legg til null-sjekker" er en privat API.

For å løse dette, vi oppretter nye AST-elementer gjennom en TreeMaker forekomst.

Først må vi skaffe oss en Kontekst forekomst:

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

Så kan vi få tak i TreeMarker objekt gjennom TreeMarker.instance (kontekst) metode.

Nå kan vi bygge nye AST-elementer, for eksempel en hvis uttrykk kan konstrueres av et kall til TreeMaker.If ():

privat statisk JCTree.JCIf createCheck (VariableTree parameter, Context context) {TreeMaker fabrikk = TreeMaker.instance (context); Navn symboler Tabell = Names.instance (kontekst); returner factory.at ((((JCTree) parameter) .pos) .If (fabrikk.Parens (createIfCondition (fabrikk, symboler Tabell, parameter)), createIfBlock (fabrikk, symboler Tabell, parameter), null); }

Vær oppmerksom på at vi ønsker å vise riktig stack-sporingslinje når et unntak kastes fra sjekken vår. Derfor justerer vi AST-fabrikkposisjonen før vi oppretter nye elementer gjennom den med factory.at ((((JCTree) parameter) .pos).

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

privat statisk JCTree.JCBinary createIfCondition (TreeMaker-fabrikk, Navn symboler Tabell, Parameter VariableTree) {Navn parameterId = symboler Tabell.fraString (parameter.getName (). toString ()); returner fabrikken.Binær (JCTree.Tag.LE, fabrikk.Ident (parameterId), fabrikk.Literal (TypeTag.INT, 0)); }

Neste, den createIfBlock () metoden bygger en blokk som returnerer en IllegalArgumentException:

privat statisk JCTree.JCBlock createIfBlock (TreeMaker fabrikk, Navn symboler Tabell, VariableTree parameter) {String parameterName = parameter.getName (). toString (); Navn parameterId = symbolerTabell.fraString (parameternavn); String errorMessagePrefix = String.format ("Argument '% s' av typen% s er merket med @% s men fikk '", parameternavn, parameter.getType (), Positive.class.getSimpleName ()); String errorMessageSuffix = "'for det"; returner fabrikk.Block (0, com.sun.tools.javac.util.List.of (factory.Throw (factory.NewClass (null, nil (), factory.Ident (symbolerTable.fromString (IllegalArgumentException.class.getSimpleName () )), com.sun.tools.javac.util.List.of (factory.Binary (JCTree.Tag.PLUS, factory.Binary (JCTree.Tag.PLUS, factory.Literal (TypeTag.CLASS, errorMessagePrefix), fabrikk. Ident (parameterId)), fabrikk.Literal (TypeTag.CLASS, errorMessageSuffix))), null)))); }

Nå som vi kan bygge nye AST-elementer, må vi sette dem inn i AST utarbeidet av parseren. Vi kan oppnå dette ved å støpe offentlig ENPI elementer til privat API-typer:

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

6. Testing av programtillegget

Vi må kunne teste pluginet vårt. Det innebærer følgende:

  • kompiler testkilden
  • kjør de kompilerte binærfiler og sørg for at de oppfører seg som forventet

For dette må vi introdusere noen hjelpeklasser.

SimpleSourceFile eksponerer den gitte kildefilens tekst for Javac:

offentlig klasse SimpleSourceFile utvider SimpleJavaFileObject {private String content; offentlig SimpleSourceFile (String QualifiedClassName, String testSource) {super (URI.create (String.format ("file: //% s% s", QualifiedClassName.replaceAll ("\.", "/"), Kind.SOURCE. utvidelse)), Kind.SOURCE); innhold = testSource; } @Override public CharSequence getCharContent (boolean ignoreEncodingErrors) {return content; }}

SimpleClassFile holder kompileringsresultatet som en byte-array:

offentlig klasse SimpleClassFile utvider SimpleJavaFileObject {private ByteArrayOutputStream ut; 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 sørger for at kompilatoren bruker bytekodeholderen vår:

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

Endelig er alt dette bundet til kompilering i minnet:

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

Etter det trenger vi bare å kjø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); }}; Klasseklass; prøv {clazz = classLoader.loadClass (QualifiedClassName); } catch (ClassNotFoundException e) {throw new RuntimeException ("Can't load compiled test class", e); } Metodemetode; 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 slik ut:

offentlig klasse SampleJavacPluginTest {privat statisk sluttstreng CLASS_TEMPLATE = "pakke com.baeldung.javac; \ n \ n" + "Offentlig klassetest {\ n" + "offentlig statisk% 1 $ s tjeneste (@Positive% 1 $ si) { \ n "+" returnerer i; \ n "+"} \ n "+"} \ n "+" "; privat TestCompiler kompilator = ny TestCompiler (); privat TestRunner-løper = ny TestRunner (); @Test (forventet = IllegalArgumentException.class) offentlig tomrom givenInt_whenNegative_thenThrowsException () kaster Throwable {compileAndRun (double.class, -1); } private Object compileAndRun (Class argumentType, Object argument) kaster Throwable {String QualitClassName = "com.baeldung.javac.Test"; byte [] byteCode = compiler.compile (kvalifisertClassName, String.format (CLASS_TEMPLATE, argumentType.getName ())); return runner.run (byteCode, QualifiedClassName, "service", ny klasse [] {argumentType}, argument); }}

Her sammenstiller vi en Test klasse med en service() metode som har en parameter kommentert med @Positive. Så kjører vi Test klasse ved å sette en dobbeltverdi på -1 for metodeparameteren.

Som et resultat av å kjøre kompilatoren med pluginet vårt, vil testen kaste et IllegalArgumentException for den negative parameteren.

7. Konklusjon

I denne artikkelen har vi vist hele prosessen med å opprette, teste og kjøre et Java Compiler-plugin.

Den fulle kildekoden til eksemplene finner du på GitHub.


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