Java 编译器 javac 笔记:javac API、注解处理 API 与 Lombok 原理

6
原文:http://nullwy.me/2017/04/java...
如果觉得我的文章对你有用,请随意赞赏

javac 是 Java 代码的编译器 [openjdk, oracle ],初学 Java 的时候就应该接触过。本笔记整理一些 javac 相关的高级用法。

javac 命令行

javac 命令行工具,官方文档有完整的使用说明,doc。当然也可以,运行 javac -helpman javac 查看帮助信息。下面是经典的 hello world 代码:

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

编译与运行

$ 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]:

Compiler Package Overview

绿色标注的包是官方 API(Official API),即 JSR-199 和 JSR-296,黄色标注的包为(Supported API),紫色标注的包代码全部在 com.sun.tools.javac.* 包下,为内部 API(Internal API)和编译器的实现类。完整的包说明如下:

全部源码都位于 langtools 下,在 JDK 中的 tools.jar 可以找到。com.sun.tools.javac.* 包下全部代码中都有Sun标注的警告:

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 的等价写法如下:

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 的等价写法:

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...

如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

载入中...