资讯专栏INFORMATION COLUMN

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

lookSomeone / 1498人阅读

摘要:对语法树的扫描,同样提供了扫描器。词法分析过程如下图所示语法分析,即根据语法由序列生成抽象语法树,对应实现类为。生成的抽象语法树如下图所示的实现原理依赖开发的典型的第三方库有,代码自动生成的和,代码检查的和,编译阶段完成依赖注入的等。

原文: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]:

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

javax.annotation.processing - 注解处理 (JSR-296)

javax.lang.model - 注解处理和编译器 Tree API 使用的语言模型 (JSR-296)

javax.lang.model.element - 语言元素

javax.lang.model.type - 类型

javax.lang.model.util - 语言模型工具

javax.tools - Java 编译器 API (JSR-199)

com.sun.source.* - 编译器 Tree API,提供 javac 工具使用的抽象语法树 AST 的只读访问

com.sun.tools.javac.* - 内部 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 diagnostics = new DiagnosticCollector<>();

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

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

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

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

        fileManager.close();
    }
}
可插拔式注解处理 API

JSR-269(Pluggable Annotation Processing API)。要理解注解处理,需要先了解 Java 代码的编译过程,编译过程如下图所示 [doc]:

整个过程就是

源代码经过词法解析和语法解析,生成语法树。然后将遇到的类符号以及在类内部定义的符号填充入(enter)符号表。

所有注解处理器会被处理,若处理器生成新的代码或 class 文件,编译过程会重新开始,直到没有新的文件生成。

语义分析和代码生成,即类型检查、控制流分析、泛型的类型擦除、去除语法糖、字节码生成等操作。

代码示例:

@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 types, RoundEnvironment environment) {
        if (!environment.processingOver()) {
            for (Element element : environment.getRootElements()) {
                scanner.scan(element);
            }
        }
        return true;
    }

    public class MyScanner extends ElementScanner7 {

        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 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 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: 
方法 METHOD: visitType
方法 METHOD: visitExecutable
方法 METHOD: visitVariable
方法 CONSTRUCTOR: 
字段 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 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 {

        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: 
字段 VARIABLE: trees
字段 VARIABLE: scanner
方法 METHOD: init
方法 METHOD: process
类 CLASS: MyScanner
方法 METHOD: 
方法 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 ]:

语法分析,即根据语法由 token 序列生成抽象语法树,对应实现类为 com.sun.tools.javac.parser.Parser。生成的抽象语法树如下图所示:

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。

参考资料

The Java programming language Compiler Group http://openjdk.java.net/group...

2008-03 The Hacker"s Guide to Javac http://scg.unibe.ch/archive/p...

2015-09 Java Compiler API https://www.javacodegeeks.com...

2015-09 Java Annotation Processors https://www.javacodegeeks.com...

2011-05 How does lombok work? http://stackoverflow.com/q/61...

莫枢 RednaxelaFX :JVM分享——Java程序的编译、加载与执行 http://www.valleytalk.org/201...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74629.html

相关文章

  • Lombok介绍、使用方法和总结

    摘要:使用方法能以简单的注解形式来简化代码,提高开发人员的开发效率。能通过注解的方式,在编译时自动为属性生成构造器方法。出现的神奇就是在源码中没有和方法,但是在编译生成的字节码文件中有和方法。没法实现多种参数构造器的重载。 1 Lombok背景介绍 官方介绍如下: Project Lombok makes java a spicier language by addi...

    30e8336b8229 评论0 收藏0
  • 使用神器Lombok优雅编码

    摘要:提高编码效率使代码更简洁消除冗长代码避免修改字段名字时忘记修改方法名提高下逼格以上就是的优点,当然,的优点远远不止以上几点,使用,你可以更加优雅高效的编辑代码。实战完成了上述准备之后,就可以愉快的使用进行编码了。接下来是使用简化后的代码。 Lombok介绍 近来偶遇一款撸码神器,介绍给大家~相信许多小伙伴都深有体会,POJO类中的千篇一律的getter/setter,construct...

    _ang 评论0 收藏0
  • 使用lombok来简化你的Java Bean

    摘要:可标注在类内部生成一个名为类名的内部类,用于快速构建。流程是这样的编译源代码,并生成语法树寻找实现了的代码,并调用。寻找被标注了注解的类,修改生成的语法树。将语法树生成为字节码就到这里了它还具备很多好用的功能,你可以去这里看看。 能做什么? 在使用lombok之前: public class Book { private Integer id; private St...

    taowen 评论0 收藏0
  • 途牛原创|使用 lombok 简化 Java 代码

    摘要:使用,简化代码为了简化与,提供了一种机制,帮助我们自动生成这些样板代码。但是,在实际项目中,完全没有使用到。源码审查是一个源码审查工具。最新版已经支持的全部注解,不再认为是没有使用的变量。 一个典型的 Java 类 public class A { private int a; private String b; public int getA() { ret...

    RyanHoo 评论0 收藏0
  • Java编译期优化思维导图

    摘要:本文参考自来自周志明深入理解虚拟机第版,拓展内容建议读者可以阅读下这本书。和构造方法一一对应,是同一概念在两个级别的含义收敛的操作自动保证执行父类的执行语句块初始化类变量字符串加操作替换为或的操作 showImg(https://segmentfault.com/img/remote/1460000016240419?w=3876&h=3614); 本文参考自来自周志明《深入理解Jav...

    sorra 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<