Stvaranje dodatka za Java Compiler

1. Pregled

Java 8 pruža API za stvaranje Javac dodaci. Nažalost, teško je pronaći dobru dokumentaciju za to.

U ovom ćemo članku prikazati cijeli postupak stvaranja proširenja kompajlera koji dodaje prilagođeni kôd * .razred datoteke.

2. Postavljanje

Prvo, moramo dodati JDK-ove alata.jar kao ovisnost za naš projekt:

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

Svako proširenje kompajlera je klasa koja implementira com.sun.source.util.Plugin sučelje. Stvorimo ga u našem primjeru:

Stvorimo ga u našem primjeru:

javna klasa SampleJavacPlugin implementira dodatak {@Override javni niz getName () {return "MyPlugin"; } @Override javni void init (zadatak JavacTask, String ... args) {Kontekst konteksta = ((BasicJavacTask) zadatak) .getContext (); Log.instance (context) .printRawLines (Log.WriterKind.NOTICE, "Hello from" + getName ()); }}

Za sada samo ispisujemo "Hello" kako bismo osigurali da naš kôd bude uspješno preuzet i uključen u kompilaciju.

Krajnji će nam cilj biti stvoriti dodatak koji dodaje provjere vremena izvođenja za svaki numerički argument označen zadanom napomenom i izuzeti ako argument ne odgovara stanju.

Još je jedan neophodan korak kako bi proširenje učinilo vidljivim do Javac:treba ga izložiti kroz ServiceLoader okvir.

Da bismo to postigli, moramo stvoriti datoteku s imenom com.sun.source.util.Plugin sa sadržajem koji je potpuno kvalificirani naziv klase našeg dodatka (com.baeldung.javac.SampleJavacPlugin) i smjestite ga u META-INF / usluge imenik.

Nakon toga možemo nazvati Javac s -Xplugin: MyPlugin sklopka:

baeldung / tutoriali $ javac -cp ./core-java/target/classes -Xplugin: MyPlugin ./core-java/src/main/java/com/baeldung/javac/TestClass.java Pozdrav iz MyPlugina

Imajte na umu da uvijek moramo koristiti a Niz vraćen s dodatka getName () metoda kao a -Xplugin vrijednost opcije.

3. Životni ciklus dodatka

A dodatak poziva prevoditelj samo jednom, putem u tome() metoda.

Da bismo bili obaviješteni o naknadnim događajima, moramo registrirati povratni poziv. Oni stižu prije i nakon svake faze obrade po izvornoj datoteci:

  • PARSE - gradi an Stablo apstraktne sintakse (AST)
  • UNESI - riješen je uvoz izvornog koda
  • ANALIZIRATI - izlaz analizatora (AST) analizira se na pogreške
  • GENERIRATI - generiranje binarnih datoteka za ciljnu izvornu datoteku

Postoje još dvije vrste događaja - OBRADA ANOTACIJA i ANNOTATION_PROCESSING_ROUND ali ovdje nas ne zanimaju.

Na primjer, kada želimo poboljšati kompilaciju dodavanjem nekih provjera na temelju informacija o izvornom kodu, razumno je to učiniti na PARSE završen voditelj događaja:

javna void init (JavacTask zadatak, String ... args) {task.addTaskListener (novi TaskListener () {javna void započeta (TaskEvent e) {} javna void završena (TaskEvent e) {if (e.getKind ()! = TaskEvent .Kind.PARSE) {return;} // Izvođenje instrumentacije}}); }

4. Izdvojite AST podatke

AST koji generira Java kompajler možemo dobiti putem TaskEvent.getCompilationUnit (). Njegovi detalji mogu se ispitati putem TreeVisitor sučelje.

Imajte na umu da samo a Drvo element, za koji prihvatiti() poziva se metoda, upućuje događaje danom posjetitelju.

Na primjer, kada izvršimo ClassTree.accept (posjetitelj), samo visitClass () se aktivira; ne možemo očekivati ​​da, recimo, visitMethod () je također aktiviran za svaku metodu u datoj klasi.

Možemo koristiti TreeScanner za prevladavanje problema:

javna praznina je završena (TaskEvent e) {if (e.getKind ()! = TaskEvent.Kind.PARSE) {return; } e.getCompilationUnit (). accept (new TreeScanner () {@Override public Void visitClass (ClassTree čvor, Void aVoid) {return super.visitClass (node, aVoid); @Override public Void visitMethod (čvor MethodTree, Void aVoid) { vrati super.visitMethod (čvor, aVoid);}}, null); }

U ovom primjeru potrebno je nazvati super.visitXxx (čvor, vrijednost) rekurzivno obrađivati ​​djecu trenutnog čvora.

5. Izmijenite AST

Da bismo prikazali kako možemo izmijeniti AST, umetnut ćemo provjere vremena izvođenja svih numeričkih argumenata označenih s @Pozitivan bilješka.

Ovo je jednostavna napomena koja se može primijeniti na parametre metode:

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

Evo primjera upotrebe napomene:

javna void usluga (@Positive int i) {}

Na kraju, želimo da bajtkod izgleda kao da je sastavljen iz izvora poput ovog:

javna void usluga (@Positive int i) {if (i <= 0) {throw new IllegalArgumentException ("Nepozitivan argument (" + i + ") dat je kao @Pozitivan parametar 'i'"); }}

To znači da želimo IlegalArgumentException biti bačen za svaki argument označen s @Pozitivan što je jednako ili manje od 0.

5.1. Gdje instrumentirati

Otkrijmo kako možemo locirati ciljana mjesta na kojima bi se instrumentacija trebala primijeniti:

privatni statički set TARGET_TYPES = Stream.of (byte.class, short.class, char.class, int.class, long.class, float.class, double.class) .map (Class :: getName) .collect (Collectors. postaviti()); 

Radi jednostavnosti ovdje smo dodali samo primitivne numeričke vrste.

Dalje, definirajmo a shouldInstrument () metoda koja provjerava ima li parametar tip u skupu TARGET_TYPES, kao i @Pozitivan napomena:

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

Zatim ćemo nastaviti završeno () metoda u našem SampleJavacPlugin razred s primjenom provjere na sve parametre koji ispunjavaju naše uvjete:

javna praznina je završena (TaskEvent e) {if (e.getKind ()! = TaskEvent.Kind.PARSE) {return; } e.getCompilationUnit (). accept (new TreeScanner () {@Preuzmi javnu prazninu visitMethod (MethodTree metoda, Void v) {List parametersToInstrument = method.getParameters (). stream () .filter (SampleJavacPlugin.this :: shouldInstrument). collect (Collectors.toList ()); if (! parametersToInstrument.isEmpty ()) {Collections.reverse (parametersToInstrument); parametersToInstrument.forEach (p -> addCheck (method, p, context));} return super.visitMethod (method , v);}}, null); 

U ovom smo primjeru preokrenuli popis parametara jer postoji mogući slučaj da je označeno više argumenata @Pozitivan. Kako se svaka provjera dodaje kao prva uputa o metodi, obrađujemo ih RTL kako bismo osigurali ispravan redoslijed.

5.2. Kako instrumentirati

Problem je u tome što "čitaj AST" leži u javnost API područje, dok su operacije "modificiraj AST" poput "dodavanje null provjera" privatni API.

Da biste to riješili, stvorit ćemo nove AST elemente putem a Drvosječa primjer.

Prvo, moramo dobiti a Kontekst primjer:

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

Tada možemo dobiti TreeMarker objekt kroz TreeMarker.instance (kontekst) metoda.

Sada možemo graditi nove AST elemente, npr. An ako izraz se može konstruirati pozivom na TreeMaker.If ():

privatni statički JCTree.JCIf createCheck (parametar VariableTree, kontekst konteksta) {Factory TreeMaker = TreeMaker.instance (kontekst); Imena symbolsTable = Names.instance (kontekst); vratiti factory.at ((((JCTree) parametar) .pos) .If (factory.Parens (createIfCondition (factory, symbolsTable, parametar)), createIfBlock (factory, symbolsTable, parametar), null); }

Imajte na umu da želimo prikazati ispravnu liniju praćenja steka kada se izuzme izuzetak iz naše provjere. Zbog toga prilagođavamo tvornički položaj AST prije nego što pomoću njega stvorimo nove elemente factory.at ((((JCTree) parametar) .pos).

The createIfCondition () metoda gradi "parametarId< 0″ ako stanje:

privatni statički JCTree.JCBinary createIfCondition (tvornica TreeMaker, Imena symbolsTable, parametar VariableTree) {Name parameterId = symbolsTable.fromString (parameter.getName (). toString ()); povratak factory.Binary (JCTree.Tag.LE, factory.Ident (parameterId), factory.Literal (TypeTag.INT, 0)); }

Dalje, createIfBlock () metoda gradi blok koji vraća IllegalArgumentException:

privatni statički JCTree.JCBlock createIfBlock (tvornica TreeMaker, Imena symbolsTable, parametar VariableTree) {Niz parametraName = parameter.getName (). toString (); Ime parameterId = symbolsTable.fromString (imeName); String errorMessagePrefix = String.format ("Argument '% s' tipa% s označen je s @% s, ali je dobio '", parameterName, parameter.getType (), Positive.class.getSimpleName ()); String errorMessageSuffix = "'za to"; vrati 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), tvornica. Ident (parameterId)), factory.Literal (TypeTag.CLASS, errorMessageSuffix))), null)))); }

Sad kad smo u mogućnosti izgraditi nove AST elemente, moramo ih umetnuti u AST koji je pripremio parser. To možemo postići lijevanjem javnost API elementi za privatni Vrste API-ja:

private void addCheck (metoda MethodTree, parametar VariableTree, kontekst konteksta) {JCTree.JCIf check = createCheck (parametar, kontekst); JCTree.JCBlock body = (JCTree.JCBlock) method.getBody (); body.stats = body.stats.prepend (provjera); }

6. Testiranje dodatka

Moramo biti u mogućnosti testirati svoj dodatak. Uključuje sljedeće:

  • sastaviti testni izvor
  • pokrenite sastavljene binarne datoteke i osigurajte da se ponašaju prema očekivanjima

Za to moramo uvesti nekoliko pomoćnih razreda.

SimpleSourceFile izlaže tekst zadane izvorne datoteke datoteci Javac:

javna klasa SimpleSourceFile proširuje SimpleJavaFileObject {sadržaj privatnog niza; public SimpleSourceFile (String qualiClassName, String testSource) {super (URI.create (String.format ("datoteka: //% s% s", qualiClassName.replaceAll ("\.", "/"), Kind.SOURCE. produženje)), Kind.IZVOR); content = testSource; } @Override public CharSequence getCharContent (boolean ignoreEncodingErrors) {return content; }}

SimpleClassFile sadrži rezultat kompilacije kao bajtni niz:

javna klasa SimpleClassFile proširuje SimpleJavaFileObject {private ByteArrayOutputStream out; javni SimpleClassFile (URI uri) {super (uri, Kind.CLASS); } @Override public OutputStream openOutputStream () baca IOException {return out = new ByteArrayOutputStream (); } javni bajt [] getCompiledBinaries () {return out.toByteArray (); } // dobivači}

SimpleFileManager osigurava da kompajler koristi naš držač bajt koda:

javna klasa SimpleFileManager proširuje ForwardingJavaFileManager {privatni popis sastavljen = novi ArrayList (); // standardni konstruktori / getteri @Override javni JavaFileObject getJavaFileForOutput (Lokacija lokacije, Ime klase niza, JavaFileObject.Kind vrsta, brat ili sestra FileObject) {SimpleClassFile rezultat = novi SimpleClassFile (URI.create ("string: //" + className)); compiled.add (rezultat); povratni rezultat; } javni popis getCompiled () {povratak sastavljen; }}

Napokon, sve je to vezano za kompilaciju u memoriji:

javna klasa TestCompiler {javni bajt [] kompajlirati (String qualiClassName, String testSource) {StringWriter output = new StringWriter (); JavaCompiler compiler = ToolProvider.getSystemJavaCompiler (); SimpleFileManager fileManager = novi SimpleFileManager (compiler.getStandardFileManager (null, null, null)); Popis compilationUnits = singletonList (novi SimpleSourceFile (qualiClassName, testSource)); Argumenti popisa = novi ArrayList (); argument.addAll (asList ("- put do klase", System.getProperty ("java.class.path"), "-Xplugin:" + SampleJavacPlugin.NAME)); JavaCompiler.CompilationTask zadatak = compiler.getTask (izlaz, FileManager, null, argumenti, null, compilationUnits); task.call (); vrati datotekuManager.getCompiled (). iterator (). next (). getCompiledBinaries (); }}

Nakon toga trebamo samo pokrenuti binarne datoteke:

javna klasa TestRunner {javni objekt koji se izvodi (bajt [] byteCode, String qualiClassName, String methodName, Class [] argumentTypes, Object ... args) baca mogućnost bacanja {ClassLoader classLoader = new ClassLoader () {@Override zaštićena klasa findClass (naziv niza) baca ClassNotFoundException {return defineClass (ime, byteCode, 0, byteCode.length); }}; Razredna klaza; isprobajte {clazz = classLoader.loadClass (qualiClassName); } catch (ClassNotFoundException e) {throw new RuntimeException ("Ne mogu učitati kompajliranu testnu klasu", e); } Metoda metode; isprobajte {method = clazz.getMethod (methodName, argumentTypes); } catch (NoSuchMethodException e) {throw new RuntimeException ("Ne mogu pronaći metodu 'main ()' u kompajliranoj testnoj klasi", e); } pokušajte {return method.invoke (null, args); } catch (InvocationTargetException e) {throw e.getCause (); }}}

Test bi mogao izgledati ovako:

javna klasa SampleJavacPluginTest {privatni statički završni niz CLASS_TEMPLATE = "paket com.baeldung.javac; \ n \ n" + "test javne klase {\ n" + "javna statička% 1 $ s usluga (@Positive% 1 $ si) { \ n "+" return i; \ n "+"} \ n "+"} \ n "+" "; privatni kompajler TestCompilera = novi TestCompiler (); privatni runner TestRunner = novi TestRunner (); @Test (očekuje se = IllegalArgumentException.class) javna praznina givenInt_whenNegative_thenThrowsException () baca Throwable {compileAndRun (double.class, -1); } private Object compileAndRun (Class argumentType, Object argument) baca mogućnost bacanja {String qualifiedClassName = "com.baeldung.javac.Test"; byte [] byteCode = compiler.compile (qualiClassName, String.format (CLASS_TEMPLATE, argumentType.getName ())); vratiti runner.run (byteCode, qualiClassName, "usluga", nova klasa [] {argumentType}, argument); }}

Ovdje sastavljamo a Test razred s a servis() metoda koja ima parametar označen s @Pozitivan. Zatim pokrećemo Test klase postavljanjem dvostruke vrijednosti -1 za parametar metode.

Kao rezultat pokretanja kompajlera s našim dodatkom, test će izbaciti IlegalArgumentException za negativni parametar.

7. Zaključak

U ovom smo članku prikazali cjelovit postupak stvaranja, testiranja i pokretanja dodatka Java Compiler.

Potpuni izvorni kod primjera može se naći na GitHubu.


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