アノテーションさえ十分に使いこなせていないが、、javaのlombokライブラリについて調べる機会があったので自分なりにまとめてみる。
lombokは、AST変換というマジックを使ってコンパイラの変換プロセスの中で生成されたAST(抽象構文木)を操作し、アノテーションに対応するコードを差し込んでいるようだった。ソースからざっくりとした流れを追ってみる。
動作の流れ
javacの変換の流れを見てみる。
META-INF/servicesのjavax.annotation.processing.Processorのlombok.launch.AnnotationProcessorHider$AnnotationProcessorのprocessが呼ばれる。processメソッドを見ると、実際の処理はcreateWrappedInstanceメソッドで生成されるオブジェクト(lombok.core.AnnotationProcessorクラス)に転送される。lombok.core.AnnotationProcessorのprocessメソッドからlombok.javac.apt.Processorのprocessメソッドが呼ばれる。- その中から、
lombok.javac.JavacTransformerのtransformが呼ばれるが、その前にRoundEnvironmentのgetRootElementsメソッドで得られるjavax.lang.model.element.Elementのオブジェクトは、com.sun.tools.javac.tree.JCCompilationUnitに変換されている。 transformメソッドで、JCCompilationUnitのオブジェクトは.lombok.javac.JavacASTクラスでラップされ、lombok.javac.JavacTransformer$AnnotationVisitorでASTをトラバースし、lombok.javac.JavacAnnotationHandlerクラスのハンドラーを呼んで変換される。
Javac AST
非公開API?が使われている。mainメソッドを書き換えてみる。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
package sample; import java.util.Collection; import java.util.Collections; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedAnnotationTypes; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.element.Element; import javax.lang.model.SourceVersion; import javax.lang.model.element.TypeElement; import com.sun.source.util.TreePath; import com.sun.source.util.Trees; import com.sun.tools.javac.model.JavacElements; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.JCTree.JCClassDecl; import com.sun.tools.javac.tree.JCTree.JCCompilationUnit; import com.sun.tools.javac.tree.JCTree.JCExpression; import com.sun.tools.javac.tree.JCTree.JCMethodDecl; import com.sun.tools.javac.tree.JCTree.JCMethodInvocation; import com.sun.tools.javac.tree.JCTree.JCStatement; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.util.List; @SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedAnnotationTypes("*") public class ExampleASTProsessor extends AbstractProcessor { /** * <pre> * public class HelloWorld { * public static void main(String[] args) { } * } * </pre> */ @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { JavacProcessingEnvironment procEnv = (JavacProcessingEnvironment) this.processingEnv; Trees trees = Trees.instance(processingEnv); TreeMaker treeMaker = TreeMaker.instance(procEnv.getContext()); JavacElements utils = procEnv.getElementUtils(); for (Element element : roundEnv.getRootElements()) { TreePath path = trees == null ? null : trees.getPath(element); JCCompilationUnit unit = (JCCompilationUnit) path.getCompilationUnit(); JCTree tc = unit.defs.get(0); if (tc instanceof JCClassDecl) { JCClassDecl classDecl = (JCClassDecl) tc; for (JCTree node : classDecl.defs) { if (node instanceof JCMethodDecl) { JCMethodDecl methodDecl = (JCMethodDecl) node; if (!methodDecl.getName().toString().equals("main")) { continue; } JCExpression printlnMethod = treeMaker.Ident(utils.getName("System")); printlnMethod = treeMaker.Select(printlnMethod, utils.getName("out")); printlnMethod = treeMaker.Select(printlnMethod, utils.getName("println")); List<JCExpression> printlnArgs = List.<JCExpression>of(treeMaker.Literal("Hello,World")); JCMethodInvocation printlnInvocation = treeMaker.Apply(List.<JCExpression> < a class="keyword" href = "http://d.hatena.ne.jp/keyword/nil" data - mce - href = "http://d.hatena.ne.jp/keyword/nil" > nil < / a > (), printlnMethod, printlnArgs ); methodDecl.body = treeMaker.Block(0, List.<JCStatement>of(treeMaker.Exec(printlnInvocation))); } } } } return true; } } |
com.sun.tools.javac.util.Listでappendしてもサイズが増えないのは何故?よくわからない。。上では、最後のBlockの初期化でJCExpressionStatementを渡している。
Eclipse AST
EclipseのJDTは、XML DOMモデルと同じ考え方の独自のDOMとASTを持っているらしい。DOMと同様な考え方で、コードブロックに任意のStatementを挿入したりすることができる。以下は、mainメソッドにHello, Worldを出力するStatementを挿入し、コンパイルして実行する例。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
package sample; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URI; import java.util.Arrays; import javax.tools.DiagnosticCollector; import javax.tools.JavaCompiler; import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; import javax.tools.ToolProvider; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.ASTVisitor; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.MethodDeclaration; import org.eclipse.jdt.core.dom.MethodInvocation; import org.eclipse.jdt.core.dom.QualifiedName; import org.eclipse.jdt.core.dom.StringLiteral; public class ASTManuplationTest { public static void main(String[] args) throws IOException { String source = "public class HelloWorld {" + " public static void main(String[] args) {" // Insert the following statement. // System.out.println("Hello, World"); + " }" + "}" ; ASTParser parser = ASTParser.newParser(AST.JLS8); parser.setSource(source.toCharArray()); parser.setKind(ASTParser.K_COMPILATION_UNIT); CompilationUnit unit = (CompilationUnit)parser.createAST(new NullProgressMonitor()); unit.accept(new ASTVisitor() { @SuppressWarnings("unchecked") public boolean visit(MethodDeclaration node) { AST ast = node.getAST(); MethodInvocation methodInvocation = ast.newMethodInvocation(); // System.out.println("Hello, World") QualifiedName qName = ast.newQualifiedName( ast.newSimpleName("System"), ast.newSimpleName("out")); methodInvocation.setExpression(qName); methodInvocation.setName(ast.newSimpleName("println")); StringLiteral literal = ast.newStringLiteral(); literal.setLiteralValue("Hello, World"); methodInvocation.arguments().add(literal); // Append the statement node.getBody().statements().add(ast.newExpressionStatement(methodInvocation)); return super.visit(node); } }); System.out.println("AST Manuplation Result"); System.out.println("--------------------------------------"); System.out.println(unit.toString()); if (compile(unit.toString())) { execute(); } } static boolean compile(String code) throws IOException { JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); String[] compileOptions = new String[] {"-d", "bin"} ; Iterable<String> compilationOptionss = Arrays.asList(compileOptions); DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>(); Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(new JavaSourceFromString("HelloWorld", code)); JavaCompiler.CompilationTask task = compiler.getTask( null, null, diagnostics, compilationOptionss, null, compilationUnits); boolean success = task.call(); System.out.println("Compile... " + (success ? "success": "fail")); return success; } static void execute() { System.out.println("\nLoad and execute the compiled class..."); System.out.println("--------------------------------------"); try { Class<?> klass = Class.forName("HelloWorld"); Method method = klass.getMethod("main", String[].class); method.invoke(null, new Object[] { null }); } catch (ClassCastException | ClassNotFoundException | IllegalAccessException | NoSuchMethodException | SecurityException | IllegalArgumentException | InvocationTargetException e) { new RuntimeException(e); } } static class JavaSourceFromString extends SimpleJavaFileObject { final String code; JavaSourceFromString(String name, String code) { super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension), Kind.SOURCE); this.code = code; } @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) { return code; } } } |
Manipulates AST with JDT
CompilationUnitは、コンパイル単位でソースファイルに該当する。createASTメソッドでは、ASTNodeクラスのオブジェクトが返ってくるが、CompilationUnitはASTNodeクラスを継承しているためKindをK_COMPILATION_UNITに指定すればキャスト可能である。その後、ASTVisitorでASTをトラバースし、メソッド定義のブロックにSystem.out.printlnのStatementを差し込んでいる。
jarファイルたち
どのクラスがどのjarに含まれているのかわかりづらかった。簡単に調べられる方法はあるのだろうか。。力技で探していった。
|
1 2 3 |
for f in `find . -type f -name "*eclipse*.jar"`; do jar -tf $f | grep "<classname>" && echo " -> $f" done |
必要だったのは以下のjarたち。
|
1 2 3 4 5 6 7 8 |
org.eclipse.core.contenttype_3.4.200.v20140207-1251.jar org.eclipse.core.jobs_3.6.0.v20140424-0053.jar org.eclipse.core.resources_3.9.1.v20140825-1431.jar org.eclipse.core.runtime_3.10.0.v20140318-2214.jar org.eclipse.equinox.common_3.6.200.v20130402-1505.jar org.eclipse.equinox.preferences_3.5.200.v20140224-1527.jar org.eclipse.jdt.core_3.10.0.v20140902-0626.jar org.eclipse.osgi_3.10.1.v20140909-1633.jar |
参考リンク
- http://www.programcreek.com/2011/01/best-java-development-tooling-jdt-and-astparser-tutorials/
- http://help.eclipse.org/luna/index.jsp?topic=/org.eclipse.jdt.doc.isv/guide/jdt_api_compile.htm
- https://docs.oracle.com/javase/jp/6/api/javax/tools/JavaCompiler.html
- http://www.docjar.com/docs/api/com/sun/tools/javac/tree/package-index.html
- http://www.coppermine.jp/docs/programming/2014/01/lombok.html


コメント