10分钟教你如何hack掉Java编译器原创
1.编译器一般编译流程
2.javac的编译流程是怎样的
3.如何hack掉Java编译器
4.运行时DI和编译期DI的区别
1、程序编译执行流程
1.1、一般执行流程
1.2、编译案例
符号表: 是一种用于数据结构,源程序中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。 在编译程序工作过程中,会不断收集、记录和使用源程序中一些语法符号的类型和特征等相关信息,这些信息一般以表格形式存储于系统中,如常数表、变量表、数组名表、过程名表、标号表等,这些统称为符号表。
2、Java程序编译类型
2.1、Java程序编译执行过程
-
在前端编译时,把Java源文件编译为Class文件;
-
在解释执行时,会收集运行数据,根据热点代码进行JIT编译优化,生成本地机器码,加快程序的执行。
-
关于类加载器以及系统启动执行流程:一篇图文彻底弄懂类加载器与双亲委派机制
-
具体的加载Class文件到JVM的流程:一篇图文彻底弄懂Class文件是如何被加载进JVM的
3、javac
3.1、javac中的主要类
3.2、javac主要处理流程
-
initProcessAnnotations(processors)
: -
准备过程:初始化插入式注解处理器
-
Parse
:parseFiles(sourceFileObjects) 解析步骤,读取一系列的Java源文件,把解析的Token序列结果映射到AST-Nodes(抽象语法树各个节点): -
词法分析
:将字符流转换为标记(Token
)集合(符号流); -
语法分析
:根据token序列构造抽象语法树,后续操作都建立在抽象语法树上,语法分析相关类:Parser
; -
Enter
:enterTrees方法负责填充符号表,编译器将在其作用域范围内找到所有定义的符号,主要包含以下两个阶段: -
第一阶段:注册所有类到其相应的作用域范围,在这一步编译器为每个类符号记录一个MemberEnter对象,该对象将用于第二阶段;
-
第二阶段:使用上面的MemberEnter对象继续完善类符号相关信息。主要包括:确定类的参数,超类和接口。
-
Annotate
:processAnnotations(): -
注解处理器的执行过程。如果存在注解处理器,并且请求了注解处理,则将处理在指定的编译单元中找到的所有注解。JSR 269定义了用于编写此类插件的接口,后面会有详细介绍。
-
delegateCompiler.compile2()
:分析及字节码生成 -
Attribute
:语义分析过程,标注检查,主要包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等;同时会进行常量折叠(int a = 1+2 折叠为 int a =3); -
Flow
:语义分析过程,数据及控制流分析。这一步是对程序上下文逻辑更进一步的验证,可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受检验异常都被正确处理了等问题。 -
final类型的局部变量就是通过在这一步分析来保证不被重新赋值的;因为局部变量不像类变量,在Class文件中有CONSTANT_Fieldref_info符号引用,记录了访问标志。
-
Desugar
:解除语法糖(inner classes, class literals, assertions, foreach loops),重写AST; -
Generate
:生成字节码,同时会进行少量代码添加和转换工作。如: -
添加实例构造器
<init>()
方法和类构造器<clinit>()
方法; -
把字符串相加操作替换为StringBuffer或者StringBuilder(JDK 1.5+);
4.注解处理器
处理注解
流程,这个流程是通过提供一组 插入式注解处理器
的标准API(Java规范提案 JSR 269: Pluggable Annotation Processing API )在编译期间对注解进行处理。我们可以把它看做是一组 编译器的插件
,在插件中可以读取,修改和添加抽象语法树中的任意元素。JSR269是从Java6开始提供; 在Java5 之前注解处理器尚未成熟,注解处理器的API并不是JDK标准,而是通过独立的apt工具(Annotation Processor Tool,分发于 com.sun.mirror
包下)来编写自定义处理器。
插入式注解处理器
在处理注解期间修改了AST(抽象语法树),编译器将回到解析与填充符号表的过程重新处理,直到所有插入式注解处理器都没有在修改AST为止,每一次循环成为一个 Round
,如下图:4.1、注解处理器与反射的区别
编译期
利用注解进行检查和改写语法树的能力,与反射的 运行期
干预不同,大大提高了执行效率。4.2、如何实现一个注解处理器
javax.annotation.processing.Processor
接口,遵循给定的协定。 为了方便实现,同时提供了 javax.annotation.processing.AbstractProcessor
类实现具有自定义处理器通用功能的抽象实现。 以下是该接口的关键需要实现的方法,注释处理期间,Java编译器将调用这两个方法:
2 3 4 5 6 7 8 9 10 11 |
*第一个方法被调用一次以初始化插件 */ public synchronized void init(ProcessingEnvironment processingEnv) /** * 在每次注释循环中被调用,在所有回合完成后再被调用一次 * @return 这些annotations注解是否由此 Processor 处理,返回ture表示该注解已经被处理, 不会再有后续其他处理器处理进行处理; 返回false表示仍可被其他后续处理器处理 */ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) |
-
javax.annotation.processing.SupportedAnnotationTypes
:用于注册处理器支持的注解。有效值是注释类型的标准名称,允许使用通配符。 -
javax.annotation.processing.SupportedSourceVersion
:用于注册处理器支持的源代码版本。 -
javax.annotation.processing.SupportedOptions
:此注释用于注册允许通过命令行传递的自定义选项。
http://scg.unibe.ch/archive/projects/Erni08b.pdf
|
|
|
|
4.2.1、写一个注解
2 |
} |
4.2.2、写一个注解处理器
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
* 注意,此例使用到了sun.tools中的类,可能会导致不稳定. * 开发者不应该调用sun包,Oracle一直在提醒开发者,调用sun.*包里面的方法是危险的。 * sun包并不包含在Java平台的标准中,它与操作系统相关, * 在不同的操作系统如Solaris,Windows,Linux,Mac等中的实现也各不相同,并且可能随着JDK版本而变化。详细说明: * http://www.oracle.com/technetwork/java/faq-sun-packages-142232.html * * Created by arthinking on 30/1/2020. */ @SupportedAnnotationTypes("com.itzhai.annotation.process.demo.ForceAssertions") @SupportedSourceVersion(SourceVersion.RELEASE_8) public class ForceAssertionsProcessor extends AbstractProcessor { // 计数器用于向用户报告已应用的替换次数 private int tally; // Trees JSR269的工具类,连接程序元素和树节点的桥梁。 // 例如,给定一个method元素,我们可以获得其关联的AST树节点 private Trees trees; // TreeMaker 编译器的内部组件,用于创建树节点的工厂 private TreeMaker make; // Name.Table 编译器的一个内部组件, Name是内部编译器字符串的抽象。 // 出于效率原因,Javac使用存储在公共大型缓冲区中的哈希字符串。 private Names names; @Override public synchronized void init(ProcessingEnvironment env) { super.init(env); trees = Trees.instance(env); // 我们使用处理环境来处理必要的编译器组件。在编译器内,对编译器的每次调用都使用单个处理环境(或context上下文,内部称为上下文)。 // 把JSR269的ProcessingEnvironment转换为实际的编译器类型JavacProcessingEnvironment,以便能够调用更多的内部方法 JavacProcessingEnvironment javacProcessingEnvironment = (JavacProcessingEnvironment)env; // 使用context上下文来确保每个编译器调用都存在每个编译器组件的单个副本。 Context context = javacProcessingEnvironment.getContext(); // 在编译器中,我们仅使用 Component.instance(context) 来获取对该阶段的引用 make = TreeMaker.instance(context); names = Names.instance(context); // tally 计数器用于向用户报告已应用的替换次数。 tally = 0; } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { if (!roundEnv.processingOver()) { // 遍历所有的程序元素,并且重写每个类的AST Set<? extends Element> elements = roundEnv.getRootElements(); for (Element each : elements) { if (each.getKind() == ElementKind.CLASS) { // 把JSR269的 Tree 转换为实际的JCTree类型,以便可以访问所有的AST元素。 JCTree tree = (JCTree) trees.getTree(each); // 通过对TreeTranslator进行子类化来完成树翻译, // TreeTranslator本身是TreeVisitor的子类。 // 这些类都不是JSR269的一部分,而是Java编译器内部的类。 TreeTranslator visitor = new Inliner(); tree.accept(visitor); } } } else { // 输出处理的断言语句的数量 processingEnv.getMessager().printMessage( Diagnostic.Kind.NOTE, tally + " assertions inlined."); } return false; } /** * Inliner类实现了AST的重写 */ private class Inliner extends TreeTranslator { /** * 为了改变assert语句,我们这里重写了 visitAssert(JCAssert tree) 方法 * @param tree */ @Override public void visitAssert(JCAssert tree) { // 必须调用超类方法,以确保将转换也应用于节点的子代。 super.visitAssert(tree); // 改写逻辑在makeIfThrowException这个方法中,结果赋值给 TreeTranslator.result result = makeIfThrowException(tree); tally++; } /** * 具体的assert语句转换逻辑: * assert cond : detail; * 转换为: * if (!cond) throw new AssertionError(detail); * * 该方法将一个断言语句作为参数,并返回一个if语句。 * 这是一个有效的返回值,因为两个树节点都是语句,因此与Java语法等效。 * * @param node * @return */ private JCStatement makeIfThrowException(JCAssert node) { // make: if (!(condition) throw new AssertionError(detail); // 获取断言的 detail List<JCExpression> args = node.getDetail() == null ? List.<JCExpression>nil() : List.of(node.detail); // 创建了一个AST节点,该节点创建了“AssertionError”的新实例。 JCExpression expr = make.NewClass( null, null, // 使用Name.Table获取编译器内部字符串表示形式 make.Ident(names.fromString("AssertionError")), args, null); // 返回一个if语句 return make.If( // 倒置 assert的条件 make.Unary(JCTree.Tag.NOT, node.cond), // 创建一个 throw 表达式 make.Throw(expr), null); } } } |
4.2.3、通过SPI注册你的注解处理器
javax.annotation.processing.Processor
文件中填写注解处理器,一行一个,本例子中该文件的内容为:
|
|
4.2.4、打包并且使用你的lib包
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.3.2</version> <configuration> <!-- 设置为true以打印有关编译器相关的日志 --> <verbose>true</verbose> <!-- 允许在单独的进程中运行编译器。如果为false,则使用内置编译器;如果为true,则使用可执行文件。 要使compilerVersion标签生效,需要将fork设为true,用于明确表示编译版本配置可用 --> <fork>true</fork> <!-- 指定插件将使用的编译器的版本 --> <compilerVersion>1.8</compilerVersion> <!-- 源代码使用的JDK版本 --> <source>1.8</source> <!--<executable>${JAVA_HOME}/bin/javac</executable>--> <!-- 需要生成的目标class文件的编译版本 --> <target>1.8</target> <!-- 需要生成的目标class文件的编译版本 --> <encoding>utf-8</encoding> <!-- 重点! https://stackoverflow.com/questions/38926255/maven-annotation-processing-processor-not-found 默认的,编译器会找到Processor配置,并且执行注解处理器,但此时注解处理器还没编译好,所以会报错,为了避免这种错误,需要做一下参数配置: --> <proc>none</proc> <!-- 这个选项用来传递编译器自身不包含但是却支持的参数选项 --> <compilerArguments> <!-- 重点!自定义注解处理器使用到了 com.sun.tools 包中的类,所以这里要确保引用 tools.jar--> <classpath>${JAVA_HOME}/lib/tools.jar</classpath> </compilerArguments> </configuration> </plugin> |
重点!
的地方,不能配错了,否则可能导致打包失败。4.2.5、使用案例
2 3 4 5 6 7 8 |
<dependency> <groupId>com.itzhai</groupId> <artifactId>annotation-process</artifactId> <version>0.0.1-SNAPSHOT</version> <scope>compile</scope> </dependency> </dependencies> |
2 3 4 5 6 7 8 9 10 11 12 |
/** * java -ea com.itzhai.annotation.process.demo.ForceAssertExample * @param args */ public static void main(String[] args) { String str = null; assert str != null : "Must not be null"; } } |
javap -v
查看对应的反汇编代码:@ForceAssertions
,我们把它加到类上面,重新编译,发现assert已经被替换掉了:该例子完整代码: https://github.com/arthinking/pluggable-annotation-processor
4.3、注解处理器其他相关应用
4.3.1、Lombok
Lombok
,可以消除POJO中冗长的get, set, hashCode, equals, 构造参数等代码,这也是通过注解处理器来实现的。 Lombok
基于JSR 269,并且hack了javac和jdt以便能够访问和修改类的抽象语法树的内部实现。@Builder
功能更,可以参考此文: https://www.cnblogs.com/throwable/p/9139908.html
4.3.2、Dagger
Dagger
是一种快速,轻量级的依赖注入框架,该框架可用于Java和Android,该框架在编译时注入以获得更高的行能。 Dagger是第一个实现标准 javax.inject
注解的DI框架(JSR 330)。 其底层也是通过注解处理器实现的,其核心处理类是 ComponentProcessor
,继承了Google Auto提供的抽象注解处理框架的 BasicAnnotationProcessor
实现的。依赖注入
是 控制反转
原理的具体应用,不同的框架以不同的方式实现依赖注入,这里我们对比以下两类:-
运行时
依赖注入
,通常基于反射,更易于使用,但是会导致运行时更慢,Spring就是运行时的DI框架; -
编译时生成具体的代码,这意味着所有繁重的操作都是在编译期间执行的,编译时DI增加了复杂性,但是通常执行的更快,Dagger就是编译时
依赖注入
。
4.3.3、Checker
@NonNull
注解表明ref必须引用到非空的对象:
2 3 4 5 6 |
public class Example { void sample() { @NonNull Object ref = null; } } |
|
|
2 3 4 5 6 |
found : @Nullable required: @NonNull Object @NonNull Object ref = null; ^ 1 error |
References
https://www.edureka.co/blog/just-in-time-compiler/
https://www.cnblogs.com/blogtech/p/10000162.html
https://www.cnblogs.com/LittleHann/p/4754446.html
http://scg.unibe.ch/archive/projects/Erni08b.pdf
https://www.jianshu.com/p/63038c7c515a
https://www.cnblogs.com/throwable/p/9139908.html
https://www.jcp.org/en/jsr/detail?id=269
https://www.slideshare.net/ltearno/gwt-and-jsr-269s-pluggable-annotation-processing-api
https://deors.wordpress.com/2011/10/08/annotation-processors/
https://www.baeldung.com/dagger-2
https://objectcomputing.com/resources/publications/sett/java-annotation-dependency-injection-beyond
欢迎关注微信公众号《Java架构杂谈》。