原文: http://nullwy.me/2017/04/java...
若是以为个人文章对你有用,请随意赞扬
javac 是 Java 代码的编译器 [openjdk, oracle ],初学 Java 的时候就应该接触过。本笔记整理一些 javac 相关的高级用法。html
javac 命令行工具,官方文档有完整的使用说明,doc。固然也能够,运行 javac -help
或 man 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
除了使用命令行工具编译 Java 代码,JDK 6 增长了规范 JSR-199 和 JSR-296,开始还提供相关的 API。Java 编译器的实现代码和 API 的总体结构如图所示[doc]:git
绿色标注的包是官方 API(Official API),即 JSR-199 和 JSR-296,黄色标注的包为(Supported API),紫色标注的包代码所有在 com.sun.tools.javac.*
包下,为内部 API(Internal API)和编译器的实现类。完整的包说明以下:github
javax.lang.model - 注解处理和编译器 Tree API 使用的语言模型 (JSR-296)shell
所有源码都位于 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.
首先,看下 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(); } }
JSR-269(Pluggable Annotation Processing API)。要理解注解处理,须要先了解 Java 代码的编译过程,编译过程以下图所示 [doc]:工具
整个过程就是
代码示例:
@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 的 CompilationTask
的 setProcessors
方法能够传入注解处理器,代码以下(被编译的 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。
针对语句 int y = x + 1;
的词法分析,即根据词法将字符序列转换为 token 序列,对应实现类为 com.sun.tools.javac.parser.Scanner。词法分析过程以下图所示 ref [RednaxelaFX ]:
语法分析,即根据语法由 token 序列生成抽象语法树,对应实现类为 com.sun.tools.javac.parser.Parser。生成的抽象语法树以下图所示:
依赖 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。