Vytváranie doplnku Java Compiler

1. Prehľad

Java 8 poskytuje API na vytváranie Javac doplnky. Bohužiaľ je ťažké nájsť pre ňu dobrú dokumentáciu.

V tomto článku si ukážeme celý proces vytvárania rozšírenia kompilátora, ktoré pridáva vlastný kód *.trieda súbory.

2. Inštalácia

Najprv musíme pridať JDK toolss.jar ako závislosť pre náš projekt:

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

Každé rozšírenie kompilátora je trieda, ktorá implementuje com.sun.source.util.Plugin rozhranie. Vytvorme to v našom príklade:

Vytvorme to v našom príklade:

verejná trieda SampleJavacPlugin implementuje doplnok {@Override public String getName () {return "MyPlugin"; } @Override public void init (úloha JavacTask, reťazec ... args) {Context context = ((BasicJavacTask) úloha) .getContext (); Log.instance (kontext) .printRawLines (Log.WriterKind.NOTICE, "Hello from" + getName ()); }}

Zatiaľ tlačíme „Dobrý deň“, aby sme zaistili, že náš kód bude úspešne vyzdvihnutý a zahrnutý do kompilácie.

Naším konečným cieľom bude vytvoriť doplnok, ktorý pridá runtime kontroly pre každý číselný argument označený danou anotáciou a vyvolať výnimku, ak sa argument nezhoduje s podmienkou.

Je ešte jeden krok, aby bolo rozšírenie viditeľné pre Javac:mala by byť vystavená cez ServiceLoader rámec.

Aby sme to dosiahli, musíme vytvoriť súbor s názvom com.sun.source.util.Plugin s obsahom, ktorý je plne kvalifikovaným názvom triedy nášho pluginu (com.baeldung.javac.SampleJavacPlugin) a umiestnite ho do META-INF / služby adresár.

Potom môžeme zavolať Javac s -Xplugin: MyPlugin prepínač:

baeldung / tutoriály $ javac -cp ./core-java/target/classes -Xplugin: MyPlugin ./core-java/src/main/java/com/baeldung/javac/TestClass.java Dobrý deň od MyPlugin

Poznač si to musíme vždy použiť a String vrátené z doplnku getName () metóda ako a -Xplugin hodnota opcie.

3. Životný cyklus doplnku

A plugin zavolá kompilátor iba raz, prostredníctvom init () metóda.

Aby sme boli informovaní o následných udalostiach, musíme zaregistrovať spätné volanie. Prichádzajú pred a po každej fáze spracovania pre každý zdrojový súbor:

  • PARZE - stavia Abstraktný strom syntaxe (AST)
  • VSTÚPTE - import zdrojového kódu je vyriešený
  • ANALÝZA - výstup analyzátora (AST) sa analyzuje na chyby
  • GENEROVAŤ Generovanie binárnych súborov pre cieľový zdrojový súbor

Existujú dva ďalšie druhy udalostí - ANNOTATION_PROCESSING a ANNOTATION_PROCESSING_ROUND ale tu ich nezaujímame.

Napríklad, keď chceme vylepšiť kompiláciu pridaním niektorých kontrol na základe informácií o zdrojovom kóde, je rozumné to urobiť na PARSE hotovo obsluha udalosti:

public void init (úloha JavacTask, reťazec ... args) {task.addTaskListener (nový TaskListener () {verejné void spustené (TaskEvent e) {} verejné void dokončené (TaskEvent e) {if (e.getKind ()! = TaskEvent .Kind.PARSE) {return;} // Vykonať inštrumentáciu}}); }

4. Extrahujte údaje AST

Cez. Môžeme získať AST vygenerovaný kompilátorom Java TaskEvent.getCompilationUnit (). Jeho podrobnosti možno preskúmať prostredníctvom TreeVisitor rozhranie.

Upozorňujeme, že iba a Strom prvok, pre ktorý súhlasiť() nazýva metóda odosiela udalosti danému návštevníkovi.

Napríklad keď vykonávame ClassTree.accept (návštevník), iba visitClass () je spustený; nemôžeme to očakávať, povedzme, visitMethod () je tiež aktivovaný pre každú metódu v danej triede.

Môžeme použiť TreeScanner na prekonanie problému:

public void dokončený (TaskEvent e) {if (e.getKind ()! = TaskEvent.Kind.PARSE) {návrat; } e.getCompilationUnit (). accept (new TreeScanner () {@Override public Void visitClass (ClassTree node, Void aVoid) {return super.visitClass (node, aVoid); @Override public Void visitMethod (uzol MethodTree, Void aVoid) { návrat super.visitMethod (node, aVoid);}}, null); }

V tomto príklade je potrebné zavolať super.visitXxx (uzol, hodnota) rekurzívne spracovať deti aktuálneho uzla.

5. Upravte AST

Na ukážku toho, ako môžeme upraviť AST, vložíme runtime kontroly všetkých číselných argumentov označených a @Pozitívne anotácia.

Toto je jednoduchá anotácia, ktorú možno použiť na parametre metódy:

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

Tu je príklad použitia anotácie:

verejná neplatná služba (@Positive int i) {}

Nakoniec chceme, aby bytecode vyzeral, akoby bol kompilovaný zo zdroja, ako je tento:

public void service (@Positive int i) {if (i <= 0) {throw new IllegalArgumentException ("Negatívny argument (" + i + ") je uvedený ako @Positive parameter 'i'"); }}

To znamená, že chceme IllegalArgumentException sa hodí za každý argument označený @Pozitívne ktorá je rovná alebo menšia ako 0.

5.1. Kam inštrumentovať

Poďme zistiť, ako môžeme vyhľadať cieľové miesta, kde by sa malo prístrojové vybavenie použiť:

súkromná statická množina TARGET_TYPES = Stream.of (byte.class, short.class, char.class, int.class, long.class, float.class, double.class) .map (Class :: getName) .collect (Collectors. nastaviť()); 

Pre jednoduchosť sme sem pridali iba primitívne číselné typy.

Ďalej definujeme a shouldInstrument () metóda, ktorá kontroluje, či má parameter typ v množine TARGET_TYPES, ako aj @Pozitívne anotácia:

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

Potom budeme pokračovať v hotové () metóda v našom SampleJavacPlugin triedy s uplatnením šeku na všetky parametre, ktoré spĺňajú naše podmienky:

public void dokončený (TaskEvent e) {if (e.getKind ()! = TaskEvent.Kind.PARSE) {návrat; } e.getCompilationUnit (). accept (new TreeScanner () {@Override public Void visitMethod (MethodTree method, Void v) {List parametersToInstrument = method.getParameters (). stream () .filter (SampleJavacPlugin.this :: shouldInstrument). collect (Collectors.toList ()); if (! parametersToInstrument.isEmpty ()) {Collections.reverse (parametersToInstrument); parametersToInstrument.forEach (p -> addCheck (metóda, p, kontext));} návrat super.visitMethod (metóda) , v);}}, null); 

V tomto príklade sme obrátili zoznam parametrov, pretože je možné, že je označených viac ako jeden argument @Pozitívne. Pretože každá kontrola je pridaná ako úplne prvá metodická inštrukcia, spracujeme ich RTL, aby sme zaistili správne poradie.

5.2. Ako inštrumentovať

Problém je v tom, že „prečítané AST“ sa nachádza v verejné Oblasť API, zatiaľ čo operácie „modifikácie AST“, ako napríklad „pridať kontroly nuly“, sú a súkromné API.

Ak to chcete vyriešiť, vytvoríme nové prvky AST prostredníctvom a TreeMaker inštancia.

Najprv musíme získať a Kontext inštancia:

@Override public void init (úloha JavacTask, reťazec ... args) {Context context = ((BasicJavacTask) úloha) .getContext (); // ...}

Potom môžeme získať TreeMarker objekt prostredníctvom TreeMarker.instance (kontext) metóda.

Teraz môžeme zostaviť nové prvky AST, napr ak výraz môže byť konštruovaný volaním na TreeMaker If ():

private static JCTree.JCIf createCheck (parameter VariableTree, kontextový kontext) {TreeMaker factory = TreeMaker.instance (kontext); Názvy symbolsTable = Názvy.inštancia (kontext); návrat factory.at ((((JCTree) parameter) .pos) .If (factory.Parens (createIfCondition (factory, symbolsTable, parameter)), createIfBlock (factory, symbolsTable, parameter), null); }

Upozorňujeme, že chceme ukázať správny sledovací riadok zásobníka, keď je z našej kontroly vyvolaná výnimka. Preto upravujeme pozíciu továrne AST predtým, ako prostredníctvom nej vytvoríme nové prvky factory.at ((((JCTree) parameter) .pos).

The createIfCondition () metóda stavia „parameterId< 0″ ak stav:

private static JCTree.JCBinary createIfCondition (TreeMaker factory, Names symbolsTable, VariableTree parameter) {Name parameterId = symbolsTable.fromString (parameter.getName (). toString ()); návrat factory.Binary (JCTree.Tag.LE, factory.Ident (parameterId), factory.Literal (TypeTag.INT, 0)); }

Ďalej createIfBlock () metóda vytvorí blok, ktorý vráti IllegalArgumentException:

private static JCTree.JCBlock createIfBlock (TreeMaker factory, Names symbolsTable, VariableTree parameter) {String parameterName = parameter.getName (). toString (); Názov parameterId = symbolyTable.fromString (parameterName); String errorMessagePrefix = String.format ("Argument '% s' typu% s je označený znakom @% s, ale dostal '', parameterName, parameter.getType (), Positive.class.getSimpleName ()); String errorMessageSuffix = "'za to"; vrátiť factory.Block (0, com.sun.tools.javac.util.List.of (factory.Throw (factory.NewClass (null, nil (), factory.Ident (symbolsTable.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), továreň) Ident (parameterId)), factory.Literal (TypeTag.CLASS, errorMessageSuffix))), null))))); }

Teraz, keď sme schopní zostaviť nové prvky AST, musíme ich vložiť do AST pripraveného syntaktickým analyzátorom. To môžeme dosiahnuť castingom verejné API prvky do súkromné Typy API:

private void addCheck (metóda MethodTree, parameter VariableTree, kontextový kontext) {JCTree.JCIf check = createCheck (parameter, kontext); JCTree.JCBlock body = (JCTree.JCBlock) method.getBody (); body.stats = body.stats.prepend (kontrola); }

6. Testovanie doplnku

Musíme byť schopní otestovať náš plugin. Zahŕňa:

  • skompilovať zdroj testu
  • spustite skompilované binárne súbory a zaistite, aby sa správali podľa očakávania

Na tento účel musíme zaviesť niekoľko pomocných tried.

SimpleSourceFile vystaví text daného zdrojového súboru Javac:

verejná trieda SimpleSourceFile rozširuje SimpleJavaFileObject {súkromný obsah reťazca; public SimpleSourceFile (String qualifiedClassName, String testSource) {super (URI.create (String.format ("file: //% s% s", qualifiedClassName.replaceAll ("\.", "/"), Kind.SOURCE. rozšírenie)), Druh.SOURCE); content = testSource; } @Override public CharSequence getCharContent (boolean ignoreEncodingErrors) {návrat obsahu; }}

SimpleClassFile drží výsledok kompilácie ako bajtové pole:

verejná trieda SimpleClassFile rozširuje SimpleJavaFileObject {private ByteArrayOutputStream von; public SimpleClassFile (URI uri) {super (uri, Kind.CLASS); } @Override public OutputStream openOutputStream () vyvolá IOException {return out = new ByteArrayOutputStream (); } verejný bajt [] getCompiledBinaries () {return out.toByteArray (); } // zakladatelia}

SimpleFileManager zaisťuje, že kompilátor používa nášho držiteľa bytecode:

verejná trieda SimpleFileManager rozširuje ForwardingJavaFileManager {kompilovaný súkromný zoznam = nový ArrayList (); // štandardné konštruktory / getre @Override verejné JavaFileObject getJavaFileForOutput (umiestnenie, reťazec ClassName, JavaFileObject.Kind druh, FileObject súrodenec) {SimpleClassFile result = new SimpleClassFile (URI.create ("string: //" + className)); compiled.add (výsledok); návratový výsledok; } public List getCompiled () {návrat kompilovaný; }}

Nakoniec je to všetko viazané na kompiláciu v pamäti:

verejná trieda TestCompiler {verejný bajt [] kompilácia (String qualifiedClassName, String testSource) {StringWriter výstup = nový StringWriter (); Kompilátor JavaCompiler = ToolProvider.getSystemJavaCompiler (); SimpleFileManager fileManager = nový SimpleFileManager (compiler.getStandardFileManager (null, null, null)); Zoznam kompiláciíUnits = singletonList (nový SimpleSourceFile (KvalifikovanéClassName, TestSource)); Zoznam argumentov = new ArrayList (); argumenty.addAll (asList ("- classpath", System.getProperty ("java.class.path"), "-Xplugin:" + SampleJavacPlugin.NAME)); JavaCompiler.CompilationTask task = compiler.getTask (výstup, fileManager, null, argumenty, null, compilationUnits); task.call (); vrátiť fileManager.getCompiled (). iterátor (). next (). getCompiledBinaries (); }}

Potom musíme spustiť iba binárne súbory:

public class TestRunner {public Object run (byte [] byteCode, String qualifiedClassName, String methodName, Class [] argumentTypes, Object ... args) hodí Throwable {ClassLoader classLoader = new ClassLoader () {@Override chránená Class findClass (názov reťazca) hodí ClassNotFoundException {return defineClass (name, byteCode, 0, byteCode.length); }}; Trieda clazz; try {clazz = classLoader.loadClass (KvalifikovanáClassName); } catch (ClassNotFoundException e) {throw new RuntimeException ("Can't load compiled test class", e); } Metóda metódy; try {method = clazz.getMethod (methodName, argumentTypes); } catch (NoSuchMethodException e) {throw new RuntimeException ("Can't find the 'main ()' method in the compiled test class", e); } try {return method.invoke (null, args); } catch (InvocationTargetException e) {throw e.getCause (); }}}

Test môže vyzerať takto:

verejná trieda SampleJavacPluginTest {private static final String CLASS_TEMPLATE = "balíček com.baeldung.javac; \ n \ n" + "test verejnej triedy {\ n" + "verejná statická služba% 1 $ s (@Positive% 1 $ si) { \ n "+" návrat i; \ n "+"} \ n "+"} \ n "+" "; súkromný kompilátor TestCompiler = nový TestCompiler (); súkromný bežec TestRunner = nový TestRunner (); @Test (očakáva sa = IllegalArgumentException.class) public void givenInt_whenNegative_thenThrowsException () hodí Throwable {compileAndRun (double.class, -1); } private Object compileAndRun (Class argumentType, Object argument) hodí Throwable {String kvalifikovaneClassName = "com.baeldung.javac.Test"; byte [] byteCode = kompilátor.compile (kvalifikovanáClassName, String.format (CLASS_TEMPLATE, argumentType.getName ())); návrat runner.run (byteCode, kvalifikovanáClassName, "služba", nová Trieda [] {argumentType}, argument); }}

Tu zostavujeme a Test trieda s a služba () metóda, ktorá má parameter anotovaný pomocou @Pozitívne. Potom spustíme Test triedy nastavením dvojnásobnej hodnoty -1 pre parameter metódy.

Výsledkom spustenia kompilátora s našim doplnkom bude test IllegalArgumentException pre záporný parameter.

7. Záver

V tomto článku sme si ukázali celý proces vytvárania, testovania a spustenia doplnku Java Compiler.

Celý zdrojový kód príkladov nájdete na GitHub.


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