How lombok works

アノテーションさえ十分に使いこなせていないが、、javaのlombokライブラリについて調べる機会があったので自分なりにまとめてみる。

lombokは、AST変換というマジックを使ってコンパイラの変換プロセスの中で生成されたAST(抽象構文木)を操作し、アノテーションに対応するコードを差し込んでいるようだった。ソースからざっくりとした流れを追ってみる。

動作の流れ

javacの変換の流れを見てみる。

  • META-INF/servicesjavax.annotation.processing.Processorlombok.launch.AnnotationProcessorHider$AnnotationProcessorprocessが呼ばれる。processメソッドを見ると、実際の処理はcreateWrappedInstanceメソッドで生成されるオブジェクト(lombok.core.AnnotationProcessorクラス)に転送される。
  • lombok.core.AnnotationProcessorprocessメソッドからlombok.javac.apt.Processorprocessメソッドが呼ばれる。
  • その中から、lombok.javac.JavacTransformertransformが呼ばれるが、その前にRoundEnvironmentgetRootElementsメソッドで得られる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クラスのオブジェクトが返ってくるが、CompilationUnitASTNodeクラスを継承しているためKindをK_COMPILATION_UNITに指定すればキャスト可能である。その後、ASTVisitorでASTをトラバースし、メソッド定義のブロックにSystem.out.printlnStatementを差し込んでいる。

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

参考リンク

byebyehaikikyou

日記やIT系関連のネタ、WordPressに関することなど様々な事柄を書き付けた雑記です。ITエンジニア経験があるのでプログラミングに関することなどが多いです。

シェアする

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

コメントする

Translate »