Java fordító beépülő modul létrehozása

1. Áttekintés

A Java 8 API-t biztosít a létrehozáshoz Javac bővítmények. Sajnos nehéz megfelelő dokumentációt találni hozzá.

Ebben a cikkben bemutatjuk a fordítói kiterjesztés létrehozásának teljes folyamatát, amely egyéni kódot ad hozzá *.osztály fájlokat.

2. Beállítás

Először hozzá kell adnunk a JDK-kat eszközök.jar projektünk függőségeként:

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

Minden fordító kiterjesztés egy osztály, amely megvalósítja com.sun.source.util.Plugin felület. Készítsük el a példánkban:

Készítsük el a példánkban:

public class SampleJavacPlugin implementálja a Plugin {@Orride public String getName () {return "MyPlugin"; } @Orride public void init (JavacTask task, String ... args) {Context context = ((BasicJavacTask) task) .getContext (); Log.instance (context) .printRawLines (Log.WriterKind.NOTICE, "Hello from" + getName ()); }}

Egyelőre csak a „Hello” -t nyomtatjuk, hogy megbizonyosodhassunk arról, hogy kódunkat sikeresen felvette és felvette az összeállításba.

Végső célunk egy olyan plugin létrehozása, amely futásidejű ellenőrzéseket ad minden adott kommentárral megjelölt numerikus argumentumhoz, és kivételt vetünk, ha az argumentum nem felel meg egy feltételnek.

Van még egy szükséges lépés, hogy a bővítmény felfedezhető legyen Javac:keresztül kell kitenni ServiceLoader keretrendszer.

Ennek eléréséhez létre kell hoznunk egy nevű fájlt com.sun.source.util.Plugin olyan tartalommal, amely a beépülő modulunk teljesen minősített osztályneve (com.baeldung.javac.SampleJavacPlugin), és helyezze a META-INF / szolgáltatások Könyvtár.

Ezt követően hívhatunk Javac a ... val -Xplugin: MyPlugin kapcsoló:

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

Vegye figyelembe, hogy mindig használnunk kell a Húr visszatért a bővítményről getName () módszer mint a -Xplugin opció értéke.

3. A beépülő modul életciklusa

A plugint csak egyszer hívja meg a fordító a benne() módszer.

A későbbi eseményekről való értesítéshez regisztrálnunk kell egy visszahívást. Ezek minden fájlkezelési szakasz előtt és után érkeznek forrásfájlonként:

  • PARSE - épít egy Absztrakt szintaxis fa (AST)
  • BELÉP - a forráskód importálása megoldott
  • ELEMZÉS - elemző kimenetet (AST) elemeznek hibákra
  • GENERÁL - bináris fájlok létrehozása a célforrás fájlhoz

Két további rendezvénytípus létezik - ANNOTATION_FELDOLGOZÁS és ANNOTATION_PROCESSING_ROUND de minket itt nem érdekelnek.

Például, ha a fordítást a forráskód információin alapuló ellenőrzések hozzáadásával szeretnénk javítani, akkor ésszerű ezt a A PARSE befejezte eseménykezelő:

public void init (JavacTask feladat, String ... args) {task.addTaskListener (új TaskListener () {public void elindult (TaskEvent e) {} public void kész (TaskEvent e) {if (e.getKind ()! = TaskEvent .Kind.PARSE) {return;} // Műszerezés végrehajtása}}); }

4. Bontsa ki az AST adatokat

A Java fordító által generált AST-t a TaskEvent.getCompilationUnit (). Részleteit a TreeVisitor felület.

Vegye figyelembe, hogy csak a Fa elem, amelyre az elfogad() metódust hívják, eseményeket küld az adott látogatónak.

Például amikor végrehajtunk ClassTree.accept (látogató), csak visitClass () kivált; erre nem számíthatunk, mondjuk visitMethod () szintén aktiválódik az adott osztály minden módszeréhez.

Tudjuk használni TreeScanner a probléma leküzdésére:

public void kész (TaskEvent e) {if (e.getKind ()! = TaskEvent.Kind.PARSE) {return; } e.getCompilationUnit (). accept (new TreeScanner () {@Orride public Void visitClass (ClassTree node, Void aVoid) {return super.visitClass (node, aVoid); @Override public Void visitMethod (MethodTree node, Void aVoid) { return super.visitMethod (csomópont, aVoid);}}, null); }

Ebben a példában hívni kell super.visitXxx (csomópont, érték) rekurzív módon feldolgozni az aktuális csomópont gyermekeit.

5. Módosítsa az AST-t

Az AST módosításának bemutatásához futásidejű ellenőrzéseket illesztünk be az összes a-val jelölt argumentumra @Pozitív annotáció.

Ez egy egyszerű megjegyzés, amely alkalmazható a módszer paramétereire:

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

Íme egy példa a kommentár használatára:

public void service (@Positive int i) {}

Végül azt akarjuk, hogy a bájtkód úgy nézzen ki, mintha egy ilyen forrásból állna össze:

public void service (@Positive int i) {if (i <= 0) {dobja be az új IllegalArgumentException-t ("Nem pozitív argumentumot (" + i + ") @Positive 'i' paraméterként adunk meg); }}

Ez azt jelenti, hogy szeretnénk egy IllegalArgumentException dobni minden érvel, amelyet megjelöltek @Pozitív amely egyenlő vagy kisebb, mint 0.

5.1. Hol kell hangszerelni

Tudjuk meg, hogyan találhatjuk meg azokat a célhelyeket, ahol a műszert alkalmazni kell:

privát statikus készlet TARGET_TYPES = Stream.of (byte.class, short.class, char.class, int.class, long.class, float.class, double.class) .map (Class :: getName) .collect (Collectors. beállít()); 

Az egyszerűség kedvéért csak primitív numerikus típusokat adtunk ide.

Ezután definiáljuk a shouldInstrument () metódus, amely ellenőrzi, hogy a paraméternek van-e típusa a TARGET_TYPES halmazban, valamint a @Pozitív kommentár:

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

Akkor folytatjuk a befejezett() módszer a mi SampleJavacPlugin osztály ellenőrzést alkalmazva az összes paraméterre, amely megfelel a feltételeinknek:

public void kész (TaskEvent e) {if (e.getKind ()! = TaskEvent.Kind.PARSE) {return; } e.getCompilationUnit (). accept (new TreeScanner () {@Orride public Void visitMethod (MethodTree method, Void v) {List parametersToInstrument = method.getParameters (). stream () .filter (SampleJavacPlugin.this :: shouldInstrument). gyűjteni (Collectors.toList ()); if (! parametersToInstrument.isEmpty ()) {Collections.reverse (parametersToInstrument); parametersToInstrument.forEach (p -> addCheck (metódus, p, kontextus));} return , v);}}, null); 

Ebben a példában megfordítottuk a paraméterek listáját, mert lehetséges, hogy egynél több argumentumot jelölünk @Pozitív. Mivel minden ellenőrzés hozzáadódik a legelső metódus utasításhoz, az RTL-t feldolgozzuk a helyes sorrend biztosítása érdekében.

5.2. Hogyan kell hangszerelni

A probléma az, hogy az „olvasott AST” a nyilvános Az API terület, míg az „AST módosítása” műveletek, mint például az „null-ellenőrzések hozzáadása” a magán API.

Ennek megoldására, új AST elemeket hozunk létre a TreeMaker példa.

Először meg kell szereznünk a Kontextus példa:

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

Ezután megszerezhetjük a TreeMarker objektum a TreeMarker.instance (kontextus) módszer.

Most új AST elemeket építhetünk, például egy ha kifejezés felépíthető a hívásra TreeMaker. Ha ():

privát statikus JCTree.JCIf createCheck (VariableTree paraméter, kontextus kontextus) {TreeMaker gyár = TreeMaker.instance (kontextus); Név szimbólumokTáblázat = Név.instance (kontextus); return factory.at ((((JCTree) paraméter) .pos) .If (gyár.Parens (createIfCondition (gyári, szimbólumokTáblázat, paraméter)), createIfBlock (gyári, symbolTable, paraméter), null); }

Felhívjuk figyelmét, hogy meg akarjuk mutatni a helyes verem nyomvonalat, ha az ellenőrzésünk alól kivételt vetünk. Ezért állítjuk be az AST gyári helyzetét, mielőtt új elemeket hoznánk létre rajta keresztül gyár.at ((((JCTree) paraméter) .pos).

A createIfCondition () módszer építi aparaméterId< 0″ ha feltétel:

privát statikus JCTree.JCBinary createIfCondition (TreeMaker gyár, Names symbolsTable, VariableTree paraméter) {Name parameterId = symbolTable.fromString (paraméter.getName (). toString ()); return factory.Binary (JCTree.Tag.LE, factory.Ident (parameterId), factory.Literal (TypeTag.INT, 0)); }

Ezután a createIfBlock () metódus egy blokkot épít, amely egy IllegalArgumentException:

privát statikus JCTree.JCBlock createIfBlock (TreeMaker gyár, Names symbolsTable, VariableTree paraméter) {String paraméterNév = paraméter.getName (). toString (); Név paraméterId = symbolTable.fromString (paraméterNév); String errorMessagePrefix = String.format ("A% s típusú"% s "% s" argumentumot @% s jelöli, de megkapta a "", paraméterNév, paraméter.getType (), Positive.class.getSimpleName ()); String errorMessageSuffix = "'érte"; return 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), gyári). Ident (parameterId)), gyár.Literal (TypeTag.CLASS, errorMessageSuffix))), null)))); }

Most, hogy új AST elemeket tudunk építeni, be kell illesztenünk őket az elemző által készített AST-be. Ezt öntéssel érhetjük el nyilvános API elemeket magán API típusok:

private void addCheck (MethodTree módszer, VariableTree paraméter, kontextus kontextus) {JCTree.JCIf check = createCheck (paraméter, kontextus); JCTree.JCBlock body = (JCTree.JCBlock) method.getBody (); body.stats = body.stats.prepend (check); }

6. A beépülő modul tesztelése

Képesnek kell lennünk tesztelni a beépülő modulunkat. Ez a következőket foglalja magában:

  • állítsa össze a teszt forrását
  • futtassa az összeállított bináris fájlokat, és gondoskodjon arról, hogy azok az elvárt módon viselkedjenek

Ehhez be kell vezetnünk néhány segédosztályt.

SimpleSourceFile az adott forrásfájl szövegét kiteszi a Javac:

public class SimpleSourceFile kiterjeszti a SimpleJavaFileObject {private String tartalmat; public SimpleSourceFile (String képzettClassName, String testSource) {szuper (URI.create (String.format ("fájl: //% s% s", minősítettClassName.replaceAll ("\.", "/")), Kind.SOURCE. kiterjesztés)), Kind.FORRÁS); tartalom = testSource; } @Orride public CharSequence getCharContent (boolean ignoreEncodingErrors) {return content; }}

SimpleClassFile a fordítási eredményt bájtömbként tartja:

a public class SimpleClassFile kiterjeszti a SimpleJavaFileObject {private ByteArrayOutputStream out-t; public SimpleClassFile (URI uri) {szuper (uri, Kind.CLASS); } @Orride public OutputStream openOutputStream () dobja az IOException-t {return out = new ByteArrayOutputStream (); } nyilvános bájt [] getCompiledBinaries () {return out.toByteArray (); } // getters}

SimpleFileManager biztosítja, hogy a fordító a byte kódtartónkat használja:

public class SimpleFileManager kiterjeszti a ForwardingJavaFileManager {private List összeállítva = new ArrayList (); // szabványos konstruktorok / getters @Orride public JavaFileObject getJavaFileForOutput (Helyszín, String osztályNév, JavaFileObject.Kind fajta, FileObject testvér) {SimpleClassFile eredmény = új SimpleClassFile (URI.create ("string: //" + osztálynév)); összeállított.add (eredmény); visszatérési eredmény; } public list getCompiled () {return compiled; }}

Végül mindez a memóriában lévő összeállításhoz kötődik:

public class TestCompiler {public byte [] compile (String képzettClassName, String testSource) {StringWriter kimenet = new StringWriter (); JavaCompiler fordító = ToolProvider.getSystemJavaCompiler (); SimpleFileManager fileManager = új SimpleFileManager (compiler.getStandardFileManager (null, null, null)); Lista compilationUnits = singletonList (új SimpleSourceFile (képzettClassName, testSource)); List argumentumok = new ArrayList (); arguments.addAll (asList ("- classpath", System.getProperty ("java.class.path"), "-Xplugin:" + SampleJavacPlugin.NAME)); JavaCompiler.CompilationTask task = compiler.getTask (output, fileManager, null, argumentumok, null, compilationUnits); feladat.hívás (); return fileManager.getCompiled (). iterator (). next (). getCompiledBinaries (); }}

Ezt követően csak a bináris fájlokat kell futtatnunk:

public class TestRunner {public Object run (byte [] byteCode, String qualClassName, String methodName, Class [] argumentTypes, Object ... args) dobja Throwable {ClassLoader classLoader = new ClassLoader () {@Override protected Class findClass (String name) dobja a ClassNotFoundException {return defineClass (név, byteCode, 0, byteCode.length); }}; Osztály clazz; próbáld ki a {clazz = classLoader.loadClass (képzettClassName); } catch (ClassNotFoundException e) {dobjon új RuntimeException-t ("Nem lehet betölteni a lefordított tesztosztályt", e); } Method method; próbáld ki a {method = clazz.getMethod (methodName, argumentTypes) parancsot; } catch (NoSuchMethodException e) {dobjon új RuntimeException-t ("Nem található a 'main ()' módszer a lefordított tesztosztályban", e); } próbáld ki a {return metódust.invoke (null, args); } catch (InvocationTargetException e) {dobja e.getCause (); }}}

Egy teszt így nézhet ki:

public class SampleJavacPluginTest {private static final String CLASS_TEMPLATE = "package com.baeldung.javac; \ n \ n" + "public class Test {\ n" + "public static% 1 $ s service (@Positive% 1 $ si) { \ n "+" return i; \ n "+"} \ n "+"} \ n "+" "; privát TestCompiler fordító = new TestCompiler (); privát TestRunner futó = új TestRunner (); @Test (várható = IllegalArgumentException.class) public void givenInt_whenNegative_thenThrowsException () dobja Throwable {compileAndRun (double.class, -1); } private Object compileAndRun (Class argumentType, Object argument) dobja a Throwable {String képzettClassName = "com.baeldung.javac.Test" parancsot; byte [] byteCode = fordító.compile (képzettClassName, String.format (CLASS_TEMPLATE, argumentType.getName ())); return runner.run (byteCode, képzettClassName, "szolgáltatás", új osztály [] {argumentType}, argumentum); }}

Itt állítunk össze egy Teszt osztály a szolgáltatás() metódus, amelynek paramétere van @Pozitív. Aztán futtatjuk a Teszt osztály dupla -1 értékének beállításával a method paraméterhez.

A fordító pluginnel való futtatásának eredményeként a teszt egy IllegalArgumentException a negatív paraméterhez.

7. Következtetés

Ebben a cikkben bemutattuk a Java Compiler beépülő modul létrehozásának, tesztelésének és futtatásának teljes folyamatát.

A példák teljes forráskódja megtalálható a GitHub oldalon.