How lombok works
アノテーションさえ十分に使いこなせていないが、、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メソッドを書き換えてみる。
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
を挿入し、コンパイルして実行する例。
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に含まれているのかわかりづらかった。簡単に調べられる方法はあるのだろうか。。力技で探していった。
for f in `find . -type f -name "*eclipse*.jar"`; do jar -tf $f | grep "<classname>" && echo " -> $f" done
必要だったのは以下のjarたち。
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