Java 編譯器 javac 筆記:javac API、註解處理 API 與 Lombok 原理

原文: http://nullwy.me/2017/04/java...
若是以爲個人文章對你有用,請隨意讚揚

javac 是 Java 代碼的編譯器 [openjdk, oracle ],初學 Java 的時候就應該接觸過。本筆記整理一些 javac 相關的高級用法。html

javac 命令行

javac 命令行工具,官方文檔有完整的使用說明,doc。固然也能夠,運行 javac -helpman javac 查看幫助信息。下面是經典的 hello world 代碼:java

package com.test.javac;
public class Hello {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

編譯與運行node

$ tree   # 代碼目錄結構
.
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── test
        │           └── javac
        │               └── Hello.java
        └── resources
$ mkdir -p target/classes   # 建立 class 文件的存放目錄
$ javac src/main/java/com/test/javac/Hello.java -d target/classes
$ java -cp "target/classes" com.test.javac.Hello 
hello world

javac 相關 API

除了使用命令行工具編譯 Java 代碼,JDK 6 增長了規範 JSR-199 和 JSR-296,開始還提供相關的 API。Java 編譯器的實現代碼和 API 的總體結構如圖所示[doc]:git

Compiler Package Overview

綠色標註的包是官方 API(Official API),即 JSR-199 和 JSR-296,黃色標註的包爲(Supported API),紫色標註的包代碼所有在 com.sun.tools.javac.* 包下,爲內部 API(Internal API)和編譯器的實現類。完整的包說明以下:github

所有源碼都位於 langtools 下,在 JDK 中的 tools.jar 能夠找到。com.sun.tools.javac.* 包下所有代碼中都有Sun標註的警告:api

This is NOT part of any supported API. If you write code that depends on this, you do so at your own risk. This code and its internal interfaces are subject to change or deletion without notice.

Java 編譯器 API

首先,看下 JSR-199 引入的 Java 編譯器 API。在沒有引入 JSR-199 前,只能使用 javac 源碼提供內部 API,上文提到的使用命令 javac 編譯 Hello.java 的等價寫法以下:oracle

import com.sun.tools.javac.main.Main;

public class JavacMain {
    public static void main(String[] args) {
        Main compiler = new Main("javac");
        compiler.compile(new String[]{"src/main/java/com/test/javac/Hello.java", "-d", "target/classes"});
    }
}

JSR-199 的等價寫法:ide

import javax.tools.*;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Arrays;

public class Jsr199Main {
    public static void main(String[] args) throws URISyntaxException, IOException {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();

        StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);

        File file = new File("src/main/java/com/test/javac/Hello.java");
        Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjectsFromFiles(Arrays.asList(file));

        compiler.getTask(null, fileManager, diagnostics, Arrays.asList("-d", "target/classes"), null, compilationUnits).call();

        for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
            System.out.format("Error on line %d in %s\n%s\n",
                    diagnostic.getLineNumber(), diagnostic.getSource().toUri(), diagnostic.getMessage(null));
        }

        fileManager.close();
    }
}

可插拔式註解處理 API

JSR-269(Pluggable Annotation Processing API)。要理解註解處理,須要先了解 Java 代碼的編譯過程,編譯過程以下圖所示 [doc]:工具

javac-flow.png

整個過程就是

  1. 源代碼通過詞法解析和語法解析,生成語法樹。而後將遇到的類符號以及在類內部定義的符號填充入(enter)符號表
  2. 全部註解處理器會被處理,若處理器生成新的代碼或 class 文件,編譯過程會從新開始,直到沒有新的文件生成。
  3. 語義分析和代碼生成,即類型檢查、控制流分析、泛型的類型擦除、去除語法糖、字節碼生成等操做。

代碼示例:

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("*")
public class VisitProcessor extends AbstractProcessor {

    private MyScanner scanner;

    @Override
    public void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.scanner = new MyScanner();
    }

    public boolean process(Set<? extends TypeElement> types, RoundEnvironment environment) {
        if (!environment.processingOver()) {
            for (Element element : environment.getRootElements()) {
                scanner.scan(element);
            }
        }
        return true;
    }

    public class MyScanner extends ElementScanner7<Void, Void> {

        public Void visitType(TypeElement element, Void p) {
            System.out.println("類 " + element.getKind() + ": " + element.getSimpleName());
            return super.visitType(element, p);
        }

        public Void visitExecutable(ExecutableElement element, Void p) {
            System.out.println("方法 " + element.getKind() + ": " + element.getSimpleName());
            return super.visitExecutable(element, p);
        }

        public Void visitVariable(VariableElement element, Void p) {
            if (element.getEnclosingElement().getKind() == ElementKind.CLASS) {
                System.out.println("字段 " + element.getKind() + ": " + element.getSimpleName());
            }
            return super.visitVariable(element, p);
        }
    }
}

編譯器 API 的 CompilationTasksetProcessors 方法能夠傳入註解處理器,代碼以下(被編譯的 java 文件就是 VisitProcessor.java):

JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
VisitProcessor processor = new VisitProcessor();

StandardJavaFileManager manager = compiler.getStandardFileManager(diagnostics, null, null);
File file = new File("src/main/java/com/test/proc/visit/VisitProcessor.java");
Iterable<? extends JavaFileObject> sources = manager.getJavaFileObjectsFromFiles(Arrays.asList(file));

CompilationTask task = compiler.getTask(null, manager, diagnostics, Arrays.asList("-d", "target/classes"), null, sources);
task.setProcessors(Arrays.asList(processor));
task.call();

manager.close();

或者也經過 javac 命令編譯,指定註解處理器經過 -processor 參數選項。另外,若 classpath 中存在目錄 META-INF/services/(或 jar 包中存在),並有 javax.annotation.processing.Processor 文件,在該文件中填寫的註解處理器類名(多個的話,換行填寫),編譯器就會自動使用這下填寫的註解處理器進行註解處理。

運行輸出結果以下:

類 CLASS: VisitProcessor
類 CLASS: MyScanner
方法 CONSTRUCTOR: <init>
方法 METHOD: visitType
方法 METHOD: visitExecutable
方法 METHOD: visitVariable
方法 CONSTRUCTOR: <init>
字段 FIELD: scanner
方法 METHOD: init
方法 METHOD: process

能夠看到整個類文件被掃描,包括內部類以及所有方法、構造方法和字段。註解處理在填充符號表以後進行,ElementScanner 類掃描的 Element 其實就是符號 Symbol。從 Symbol 類的定義能夠看到這一點。

public abstract class Symbol extends AnnoConstruct implements Element

填充符號表前一步是構造語法樹。對語法樹的掃描,com.sun.source.* 一樣提供了掃描器TreeScanner。使用 TreeScanner 掃描 java 代碼的示例代碼以下所示:

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("*")
public class VisitTreeProcessor extends AbstractProcessor {
    private Trees trees;
    private MyScanner scanner;

    @Override
    public void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.trees = Trees.instance(processingEnv);
        this.scanner = new MyScanner();
    }

    public boolean process(Set<? extends TypeElement> types, RoundEnvironment environment) {
        if (!environment.processingOver()) {
            for (Element element : environment.getRootElements()) {
                TreePath path = trees.getPath( element );
                scanner.scan(path, null);
            }
        }
        return true;
    }

    public class MyScanner extends TreePathScanner<Tree, Void> {

        public Tree visitClass(ClassTree node, Void p) {
            System.out.println("類 " + node.getKind() + ": " + node.getSimpleName());
            return super.visitClass(node, p);
        }

        public Tree visitMethod(MethodTree node, Void p) {
            System.out.println("方法 " + node.getKind() + ": " + node.getName());
            return super.visitMethod(node, p);
        }

        public Tree visitVariable(VariableTree node, Void p) {
            if (this.getCurrentPath().getParentPath().getLeaf() instanceof ClassTree) {
                System.out.println("字段 " + node.getKind() + ": " + node.getName());
            }
            return super.visitVariable(node, p);
        }
    }
}

運行輸出結果以下:

類 CLASS: VisitTreeProcessor
方法 METHOD: <init>
字段 VARIABLE: trees
字段 VARIABLE: scanner
方法 METHOD: init
方法 METHOD: process
類 CLASS: MyScanner
方法 METHOD: <init>
方法 METHOD: visitClass
方法 METHOD: visitMethod
方法 METHOD: visitVariable

須要注意的是,獲取語法樹是經過工具類 Trees 的 getTree 方法完成的。另外,能夠看到 com.sun.source.* 包下暴露的 API 對語法樹只能作只讀操做,功能有限,要想修改語法樹必須使用 javac 的內部 API。

javac 內部 API

針對語句 int y = x + 1; 的詞法分析,即根據詞法將字符序列轉換爲 token 序列,對應實現類爲 com.sun.tools.javac.parser.Scanner。詞法分析過程以下圖所示 ref [RednaxelaFX ]:

javac-scanner

語法分析,即根據語法由 token 序列生成抽象語法樹,對應實現類爲 com.sun.tools.javac.parser.Parser。生成的抽象語法樹以下圖所示:
javac-syntax-tree

Lombok 的實現原理

依賴 JSR-269 開發的典型的第三方庫有,代碼自動生成的 Lombok 和 Google Auto,代碼檢查的 Checker 和 Google Error Prone,編譯階段完成依賴注入的 Google Dagger 2 等。

如今看下 Lombok 的實現源碼。Lombok 提供 @NonNull, @Getter, @Setter, @ToString, @EqualsAndHashCode, @Data等註解,自動生成常見樣板代碼 boilerplate,解放開發效率。Lombok 支持 javac 和 ecj (Eclipse Compiler for Java)。對於 javac 編譯器對應的註解處理器是 LombokProcessor,而後通過一些處理過程,每一個註解都會有特定的 handler 來處理,@NonNull 對應 HandleNonNull、@Getter 對應 HandleGetter、@Setter 對應 HandleSetter、@ToString 對應 HandleToString、@EqualsAndHashCode 對應HandleEqualsAndHashCode、@Data 對應 HandleData。閱讀這些 handler 的實現,能夠看到樣板代碼的生成依賴的就是 com.sun.tools.javac.* 包。

爲了試驗和學習 javac 內部 API 的功能,本人嘗試從新實現 Lombok 的 @Data 註解,簡單實現了自動生成 getter 和 setter 的功能,代碼參見 github,使用 @Data 的代碼見 link

參考資料

  1. The Java programming language Compiler Group http://openjdk.java.net/group...
  2. 2008-03 The Hacker's Guide to Javac http://scg.unibe.ch/archive/p...
  3. 2015-09 Java Compiler API https://www.javacodegeeks.com...
  4. 2015-09 Java Annotation Processors https://www.javacodegeeks.com...
  5. 2011-05 How does lombok work? http://stackoverflow.com/q/61...
  6. 莫樞 RednaxelaFX :JVM分享——Java程序的編譯、加載與執行 http://www.valleytalk.org/201...
相關文章
相關標籤/搜索