Javac
编译器
提到编译器,可能很多后端研发工程师只是在脑海中有一个模糊的概念,我也是这样,因为业务开发中几乎使用不到。我们都工作在编译器的上层。
对于Java
语言来说,它的整个编译期是不确定的过程,我们写的.java
类文件需要被编译成.class
字节码文件然后加载至虚拟机中,.class
字节码文件需要被编译成机器码进行执行,当然也有直接将.java
类文件编译成机器码的编译器。
Javac
编译器是一个将.java
文件编译成.class
字节码的编译器,它可以在不改变底层虚拟机的情况下对程序编码进行编译期的优化,即:语法糖。
从sun
公司提供的Javac
编译器代码(源码在JDK
的tool.jar
包中)实现来看,编译的过程可分为以下三个阶段:
Parse and Enter
:解析与填充符号表,编译器会将Java
文件解析成Abstract Syntax Tree
(AST
)抽象语法树,这个过程会校验语法的正确性等。Annotation Processing
:插入式注解处理器的注解处理,这个过程可以对语法树进行相关操作。Analyse and Generate
:分析语法树与字节码生成。
在JDK5
后,Java
语言引入了注解,这些注解与普通的Java
类一样,是在运行期间发挥作用的。JDK6
实现了JSR-269
规范,提供出一组插入式注解处理器的标准API
在编译期间对注解进行处理,我们可以把它看作是一组编译器的插件,或者称为钩子,我们可以使用这些API
对第一阶段产生的语法树进行修改,一旦语法树被修改,编译器将会回到第一阶段重新解析与填充符号表,直到所有的插入式注解处理器都没有再对语法树进行修改为止,每一次回到第一阶段称为一次Round
。
Javac
编译器的源码是com.sun.tools.javac.main.JavaCompiler
类,编译的方法是com.sun.tools.javac.main.JavaCompiler#compile
,其中插入注解处理器的初始化过程是在this.initProcessAnnotations(var3);
方法中完成的;执行过程是在com.sun.tools.javac.main.JavaCompiler#processAnnotations
方法中完成的,该方法中判断是否还有新的注解处理器需要执行,如果有的话,通过com.sun.tools.javac.processing.JavacProcessingEnvironment#doProcessing
方法得到一个新的JavaCompiler
类对象继续后续的处理。
Lombok
的原理
Lombok
正是利用Javac
编译器的第二阶段进行实现的,使用的也正是JDK6
提供的JSR-269
实现。在编译期操作语法树,实现字节码的动态生成。
造轮子
我们要实现的是一个简易的Lombok
,一个注解在编译期同时生成getter
和setter
方法。
自定义注解
首先要定义一个注解@GetterAndSetter
:
1 2 3 4 5 6 7 8 9 10 11
| import java.lang.annotation.*;
@Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) @Documented public @interface GetterAndSetter { }
|
自定义注解处理器
然后定义一个处理@GetterAndSetter
注解的处理器GetterAndSetterProcessor
,所有的处理器都建议去继承javax.annotation.processing.AbstractProcessor
抽象类,它提供了一些基本实现。
1 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
| import com.google.auto.service.AutoService; import com.sun.source.util.Trees; import com.sun.tools.javac.processing.JavacProcessingEnvironment; import com.sun.tools.javac.tree.JCTree; import com.sun.tools.javac.tree.TreeMaker; import com.sun.tools.javac.util.Context; import com.sun.tools.javac.util.Names; import org.sunchaser.lombok.impl.annotation.GetterAndSetter;
import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; import java.util.Set;
@SupportedAnnotationTypes({"org.sunchaser.lombok.impl.annotation.GetterAndSetter"}) @SupportedSourceVersion(SourceVersion.RELEASE_8) @AutoService(Processor.class) public class GetterAndSetterProcessor extends AbstractProcessor {
private Trees trees;
private TreeMaker treeMaker;
private Names names;
private Messager messager;
@Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); this.messager = processingEnv.getMessager(); this.trees = Trees.instance(processingEnv); Context context = ((JavacProcessingEnvironment) processingEnv).getContext(); this.treeMaker = TreeMaker.instance(context); this.names = Names.instance(context); }
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { messager.printMessage(Diagnostic.Kind.NOTE,"roundEnv --->" + roundEnv); if (!roundEnv.processingOver()) { Set<? extends Element> annotated = roundEnv.getElementsAnnotatedWith(GetterAndSetter.class); annotated.forEach(element -> { JCTree tree = (JCTree) trees.getTree(element); tree.accept(new GetterAndSetterTreeTranslator(treeMaker, names, messager)); }); } return true; }
@Override public Set<String> getSupportedAnnotationTypes() { return super.getSupportedAnnotationTypes(); }
@Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } }
|
注解处理器的实现细节
主要方法
我们来看下子类中实现的主要方法:
init(ProcessingEnvironment processingEnv)
初始化方法,在父抽象类中对入参ProcessingEnvironment
对象进行了初始化,我们可以通过这个对象得到很多工具类,例如Trees
、TreeMaker
和Names
等等。
process
父类中的抽象方法,子类进行具体的处理实现。输入参数RoundEnvironment
代表每一次Round
的环境。
getSupportedAnnotationTypes
指定当前处理器是用来处理哪个注解的,返回值是需要处理的注解的全限定类名字符串的集合。在JDK7
后,父抽象类基于@SupportedAnnotationTypes
注解进行了实现,子类中可不重写该方法,直接使用注解:@SupportedAnnotationTypes({"org.sunchaser.lombok.impl.annotation.GetterAndSetter"})
即可。
getSupportedSourceVersion
指定支持的JDK
版本,这里通常返回SourceVersion.latestSupported()
最新版本。在JDK7
后,父抽象类基于@SupportedSourceVersion
注解进行了实现,子类中可不重新进行实现,直接使用注解:@SupportedSourceVersion(SourceVersion.RELEASE_8)
即可。
工具类介绍
下面介绍一下init
方法中初始化的一些工具类:
javax.annotation.processing.Messager
编译期的日志打印工具类。使用processingEnv.getMessager();
进行初始化。
javax.lang.model.element.Element
对一个.java
文件结构的建模。一个Java
类被划分为各个Element
,每个部分都有对应的子类实现。
例如:
1 2 3 4 5 6 7 8 9 10 11 12
| package com.sunchaser;
public class Foo { private String str; private Foo foo; public Foo() {} public void setStr( String str ) { this.str = str; } }
|
可将Element
转换成JCTree
,组成AST
语法树。使用Trees.instance(processingEnv);
进行初始化。
构造JCTree
的工具类。使用TreeMaker.instance(((JavacProcessingEnvironment) processingEnv).getContext());
进行初始化。
名字处理工具类。使用Names.instance(context);
进行初始化。无论是类、方法、变量或者参数的名称都需要此类来创建,常用形式为names.fromString("setNameXXX")
。
1 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
| public class List<A> extends AbstractCollection<A> implements java.util.List<A> { public A head; public List<A> tail; private static final List<?> EMPTY_LIST = new List<Object>((Object)null, (List)null) { public List<Object> setTail(List<Object> var1) { throw new UnsupportedOperationException(); }
public boolean isEmpty() { return true; } };
public static <A> List<A> nil() { return EMPTY_LIST; } public static <A> List<A> of(A var0) { return new List(var0, nil()); }
public static <A> List<A> of(A var0, A var1) { return new List(var0, of(var1)); }
public static <A> List<A> of(A var0, A var1, A var2) { return new List(var0, of(var1, var2)); }
public static <A> List<A> of(A var0, A var1, A var2, A... var3) { return new List(var0, new List(var1, new List(var2, from(var3)))); } .... }
|
在这套抽象语法树的实现中,所有的List
都使用的是com.sun.tools.javac.util.List
,而不是java.util.ArrayList
。这是一个链式List
,尾节点为集合。常用的方法有List.nil()
和List.of()
。
JCTree
介绍
JCTree
是语法树元素的基类,包含一个重要的字段pos
,该字段用来指明当前语法树节点JCTree
在语法树中的位置。com.sun.tools.javac.tree.JCTree
是一个抽象类,且子类的构造函数都是protected
的,所以不能直接使用new
关键字创建语法树元素节点,而是使用访问者设计模式进行创建。
下面介绍几个重要的子类:
com.sun.tools.javac.tree.JCTree.JCStatement
:声明语法树节点,仍然是一个抽象类,下面是常用子类:com.sun.tools.javac.tree.JCTree.JCBlock
:语句块语法树节点。com.sun.tools.javac.tree.JCTree.JCReturn
:return
语句语法树节点。com.sun.tools.javac.tree.JCTree.JCClassDecl
:类定义语法树节点。com.sun.tools.javac.tree.JCTree.JCVariableDecl
:字段/变量定义语法树节点。
com.sun.tools.javac.tree.JCTree.JCMethodDecl
:方法定义语法树节点。com.sun.tools.javac.tree.JCTree.JCModifiers
:访问标志语法树节点。com.sun.tools.javac.tree.JCTree.JCExpression
:表达式语法树节点,仍然是一个抽象类,下面是常用子类:com.sun.tools.javac.tree.JCTree.JCAssign
:赋值语句语法树节点。com.sun.tools.javac.tree.JCTree.JCIdent
:标志符语法树节点,可以是变量、类型和关键字等等。
TreeMaker
介绍
TreeMaker
用于创建一系列的语法树节点,上面说到创建JCTree
语法树节点不能直接使用new
关键字创建,所以才有了TreeMaker
工具类,它会在创建时为JCTree
对象设置pos
字段,必须使用上下文相关的TreeMaker
对象来创建语法树节点。
下面介绍几个常用方法:
TreeMaker.Modifiers
用于创建访问标志的语法树节点JCModifiers
,主要方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public JCModifiers Modifiers(long flags) { return this.Modifiers(flags, List.nil()); }
public JCModifiers Modifiers(long flags, List<JCAnnotation> annotations) { JCModifiers tree = new JCModifiers(flags, annotations); boolean noFlags = (flags & 8796093033983L) == 0L; tree.pos = noFlags && annotations.isEmpty() ? -1 : this.pos; return tree; }
|
其中flags
可以使用枚举类com.sun.tools.javac.code.Flags
,例如treeMaker.Modifiers(Flags.PUBLIC + Flags.STATIC + Flags.FINAL)
创建的语法树节点为:public static final
。
TreeMaker.ClassDef
用于创建类定义的语法树节点JCClassDecl
,主要方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
public JCClassDecl ClassDef(JCModifiers modifiers, Name name, List<JCTypeParameter> typeParameter, JCExpression extending, List<JCExpression> implementing, List<JCTree> defs) { JCClassDecl tree = new JCClassDecl(modifiers, name, typeParameter, extending, implementing, defs, (ClassSymbol)null); tree.pos = this.pos; return tree; }
|
TreeMaker.VarDef
用于创建字段/变量定义的语法树节点JCVariableDecl
,主要方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
public JCVariableDecl VarDef(JCModifiers modifiers, Name name, JCExpression varType, JCExpression init) { JCVariableDecl tree = new JCVariableDecl(modifiers, name, varType, init, (VarSymbol)null); tree.pos = this.pos; return tree; }
|
TreeMaker.Ident
用于创建标识符的语法树节点JCIdent
,主要方法如下:
1 2 3 4 5 6 7 8 9
|
public JCIdent Ident(Name name) { JCIdent tree = new JCIdent(name, (Symbol)null); tree.pos = this.pos; return tree; }
|
TreeMaker.Return
用于创建return
语句的语法树节点JCReturn
,主要方法如下:
1 2 3 4 5 6 7 8
|
public JCReturn Return(JCExpression ex) { JCReturn tree = new JCReturn(ex); tree.pos = this.pos; return tree; }
|
TreeMaker.Apply
用于创建方法调用的语法树节点JCMethodInvocation
,主要方法如下:
1 2 3 4 5 6 7 8 9 10 11
|
public JCMethodInvocation Apply(List<JCExpression> argsTypes, JCExpression fn, List<JCExpression> args) { JCMethodInvocation tree = new JCMethodInvocation(argsTypes, fn, args); tree.pos = this.pos; return tree; }
|
TreeMaker.Select
用于创建域访问/方法访问的语法树节点JCFieldAccess
,主要方法如下:
1 2 3 4 5 6 7 8 9 10
|
public JCFieldAccess Select(JCExpression selected, Name selector) { JCFieldAccess tree = new JCFieldAccess(selected, selector, (Symbol)null); tree.pos = this.pos; return tree; }
|
TreeMaker.Assign
用于创建赋值语句的语法树节点JCAssign
,主要方法如下:
1 2 3 4 5 6 7 8 9 10
|
public JCAssign Assign(JCExpression leftEx, JCExpression rightEx) { JCAssign tree = new JCAssign(leftEx, rightEx); tree.pos = this.pos; return tree; }
|
TreeMaker.Exec
用于创建可执行语句的语法树节点JCExpressionStatement
,主要方法如下:
1 2 3 4 5 6 7 8
|
public JCExpressionStatement Exec(JCExpression ex) { JCExpressionStatement tree = new JCExpressionStatement(ex); tree.pos = this.pos; return tree; }
|
TreeMaker.Apply
和TreeMaker.Assign
需要在外面包一层TreeMaker.Exec
来获取一个可执行语句JCExpressionStatement
。
TreeMaker.Block
用于创建组合语句的语法树节点JCExpressionStatement
,主要方法如下:
1 2 3 4 5 6 7 8 9 10
|
public JCBlock Block(long flags, List<JCStatement> statements) { JCBlock tree = new JCBlock(flags, statements); tree.pos = this.pos; return tree; }
|
TreeMaker.MethodDef
用于创建方法定义的语法树节点JCMethodDecl
,主要方法如下:
1 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
|
public JCMethodDecl MethodDef(JCModifiers modifiers, Name name, JCExpression returnType, List<JCTypeParameter> typeParameters, List<JCVariableDecl> params, List<JCExpression> throwns, JCBlock methodBody, JCExpression defaultValue) { return this.MethodDef(modifiers, name, returnType, typeParameters, (JCVariableDecl)null, params, throwns, methodBody, defaultValue); }
public JCMethodDecl MethodDef(JCModifiers modifiers, Name name, JCExpression returnType, List<JCTypeParameter> typeParameters, JCVariableDecl variable, List<JCVariableDecl> params, List<JCExpression> throwns, JCBlock methodBody, JCExpression defaultValue) { JCMethodDecl tree = new JCMethodDecl(modifiers, name, returnType, typeParameters, variable, params, throwns, methodBody, defaultValue, (MethodSymbol)null); tree.pos = this.pos; return tree; }
|
process
执行
下面详细介绍process
方法中的具体处理逻辑:
首先要知道process
方法如果返回了true
,那么Javac
编译器会从编译期的第二阶段回到第一阶段:即重新回到解析与填充符号表阶段。
处理逻辑:如果!roundEnv.processingOver()
还未process
完成,则找到所有@GetterAndSetter
注解标注的类,依次遍历获得每个类的语法树,然后交给自定义的org.sunchaser.lombok.impl.process.GetterAndSetterTreeTranslator
类进行处理,全部处理完成后返回true
以便回到编译的第一阶段。
下面是GetterAndSetterTreeTranslator
类中具体操作语法树插入getter
和setter
方法的逻辑:
1 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 123 124 125 126 127 128
| import static com.sun.tools.javac.tree.JCTree.*;
public class GetterAndSetterTreeTranslator extends TreeTranslator {
private final TreeMaker treeMaker;
private final Names names;
private final Messager messager;
public GetterAndSetterTreeTranslator(TreeMaker treeMaker, Names names, Messager messager) { this.treeMaker = treeMaker; this.names = names; this.messager = messager; }
@Override public void visitClassDef(JCClassDecl jcClassDecl) { super.visitClassDef(jcClassDecl); jcClassDecl.defs.forEach(def -> { if (def.getKind().equals(Kind.VARIABLE)) { messager.printMessage(Diagnostic.Kind.NOTE,def + "----processed"); jcClassDecl.defs = jcClassDecl.defs.prepend(createGetterMethod((JCVariableDecl) def)); jcClassDecl.defs = jcClassDecl.defs.prepend(createSetterMethod((JCVariableDecl) def)); } }); }
private JCTree createGetterMethod(JCVariableDecl def) { return treeMaker.MethodDef( treeMaker.Modifiers(Flags.PUBLIC), names.fromString("get" + this.toFirstUpperCase(def.getName().toString())), (JCExpression) def.getType(), List.nil(), List.nil(), List.nil(), treeMaker.Block(0L,List.of( treeMaker.Return( treeMaker.Select( treeMaker.Ident(names.fromString("this")), names.fromString(def.getName().toString()) ) ) )), null ); }
private JCTree createSetterMethod(JCVariableDecl def) { JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), def.getName(), def.vartype, null); JCFieldAccess thisX = treeMaker.Select(treeMaker.Ident(names.fromString("this")), def.getName()); JCAssign assign = treeMaker.Assign(thisX, treeMaker.Ident(def.getName())); JCBlock methodBody = treeMaker.Block(0L, List.of(treeMaker.Exec(assign))); JCExpression returnType = treeMaker.Type(new Type.JCVoidType()); return treeMaker.MethodDef( treeMaker.Modifiers(Flags.PUBLIC), names.fromString("set" + this.toFirstUpperCase(def.getName().toString())), returnType, List.nil(), List.of(param), List.nil(), methodBody, null ); }
private String toFirstUpperCase(String str) { char[] charArray = str.toCharArray(); charArray[0] -= 32; return String.valueOf(charArray); } }
|
至此,一个简易Lombok
的功能就实现了,但是还无法正常使用,我们需要把我们的插入注解处理器类GetterAndSetterProcessor
使用SPI
的形式提供给外部使用。
SPI
实现
实现方式一:
手动在resources
目录下创建META-INF/services
目录,然后在services
目录下创建javax.annotation.processing.Processor
文件,文件内容为插入注解处理器类GetterAndSetterProcessor
的全限定类名。
实现方式二:
使用google
提供的auto-service
框架,该框架也是基于插入注解处理器实现的。其maven
坐标为:
1 2 3 4 5
| <dependency> <groupId>com.google.auto.service</groupId> <artifactId>auto-service</artifactId> <version>1.0-rc7</version> </dependency>
|
然后在我们自定义的插入注解处理器类GetterAndSetterProcessor
上打上auto-service
框架提供的@AutoService(Processor.class)
注解,在编译后即自动生成target/classes/META-INF/services/javax.annotation.processing.Processor
文件。
测试
我们无法在插入注解处理器类GetterAndSetterProcessor
所在的maven
模块进行测试,因为该处理器是使用SPI
对外提供服务的,所以我们需要新建一个maven
子模块,然后引入插入注解处理器GetterAndSetterProcessor
类所在的maven
模块的坐标,然后才可以使用我们自定义的注解,简单测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import org.sunchaser.lombok.impl.annotation.GetterAndSetter;
@GetterAndSetter public class ChainSetterTest { private String name; public static void main(String[] args) {
} }
|
运行main
方法即可。
可看到Build Output
中有如下输出:
1 2 3
| java: roundEnv --->[errorRaised=false, rootElements=[com.sunchaser.lomboktest.ChainSetterTest], processingOver=false] java: private String name----processed java: roundEnv --->[errorRaised=false, rootElements=[], processingOver=true]
|
打开target/classes/com/sunchaser/lomboktest/ChainSetterTest.class
字节码文件,可看到已生成字段name
的getName
和setName
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
package com.sunchaser.lomboktest;
public class ChainSetterTest { private String name;
public void setName(String name) { this.name = name; }
public String getName() { return this.name; }
public ChainSetterTest() { }
public static void main(String[] args) { } }
|
总结
至此,我们就基本实现了一个简易的Lombok
。
但是我留下了一个疑问,setter
方法如何生成链式的,貌似我没有找到对应的API
,Lombok
是通过另一个注解@Accessors(chain = true)
来实现链式setter
方法的,为什么它不直接在@Setter
注解中设置一个属性用来控制生成的setter
方法是否为链式的?看Lombok
的源码并未看懂这部分的实现,网上也没有找到对应资料,如果有同行研究过这个问题可留言进行交流,十分感谢!