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.