在上一篇Lombok常常用,可是你知道它的原理是什么吗?简单介绍了注解处理器,是用来处理编译期的注解的一个工具,咱们只是本身生成了一些代码,可是和Lombok却不同,由于Lombok是在原有类的基础上增长了一些类,你那么Lombok是如何作到修改原有类的内容呢?接下来咱们就再进一步了解Lombok的原理。java
既然咱们是在编译期对类进行操做了,那么咱们就须要了解在Java中Javac到底对程序作了什么。Javac对代码编译的过程其实就是用Java来写的,咱们能够查看其源码对其简单的分析,如何下载源码,Debug源码这里我就不进行分析了,推荐一篇文章写的挺好的。Javac 源码调试教程。git
编译过程大体分为了三个阶段程序员
这三个阶段的交互过程以下图所示。github
这一步骤是两个步骤,包括了解析和填充符号,其中解析是分为词法分析和语法分析两个步骤。app
词法分析就是将源代码的字符流转变为Java中的标记(Token)集合,单个字符是程序编写过程当中最小的元素,而标记(Token)则是编译过程当中最小的元素,关键字、变量名、字面量、运算符均可以成为标记(Token)。好比在Java中int a = b+2
,这段代码则表示了6个标记Token
,分别是int、a、=、b、+、2
。虽然关键字int是由三个字符构成的,可是它只是一个Token,不能够再拆分了。ide
语法分析是根据Token序列构造抽象对象树的过程,抽象语法树(Abstract syntax tree),是一种用来描述代码语法结构的树形表示方法,语法树的每个节点都表明着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至是代码注释都是一个语法结构。工具
语法分析分析出来的树结构是由JCTree
来表示的,咱们能够看一下它的子类有哪些。post
咱们本身建一个类,能够观察它在编译过程当中用树结构表示是一种怎样的结构。测试
public class HelloJvm { private String a; private String b; public static void main(String[] args) { int c = 1+2; System.out.println(c); print(); } private static void print(){ } }
你们注意我划红线的地方,能够看到这些都是JCTree的子类。咱们能够知道编译期的树是以JCCompilationUnit
为根节点,而后做为类的构成元素例如方法、私有变量、class类,这些都是做为树的构成一种。this
填充符号表和咱们的Lombok原理关联不大,这里了解便可。
完成了语法分析和词法分析之后,下一步就是填充符号表的过程,符号表是由一组符号地址和符号信息构成的表格,能够将它想象成哈希表中的K-V值对的形式(符号表不必定是哈希表实现,可使有序符号表,树状符号表、栈结构符号表等)。符号表中所登记的信息在编译的不一样阶段都要用到,在语义分析中,符号表所登记的内容将用于语义检查(如检查一个名字的使用和原先的说明是否一致)和产生中间代码。在目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的依据。
第一步的解析和填充符号表完成之后,接下来就是咱们的重头戏注解处理器了。由于在这一步就是Lombok实现原理的关键。
在JDK1.5以后,Java语言提供了对注解的支持,这些注解与普通的Java代码同样,是在运行期间发挥做用的。在JDK1.6中实现了对JSR-269的规范,提供了一组插入式注解处理器的标准API在编译期间对注解进行处理,咱们能够把它看做是一组编译器的插件,在这些插件里面,能够读取,修改,添加抽象语法树中的任意元素。
若是这些插件在处理注解期间对语法树进行了修改,那么编译器将回到解析及填充符号表的过程从新处理,直到全部的插入式注解处理器都没有了再对语法树进行修改成止。每一次循环成为一个Round。
有了编译器注解处理的标准API后,咱们的代码才有可能干涉编译器的行为,因为语法树中的任意元素,甚至包括代码注释均可以在插件之中访问到,因此经过插入式注解处理器实现的插件在功能上有很大的发挥空间。只要有足够多的创意,程序员可使用插入式注解处理器来实现许多本来只能在编码中完成的事情。
语法分析以后,编译器得到了程序代码的抽象语法树表示,语法树能表示一个结构正确的源程序的抽象,可是没法保证源程序是符合逻辑的。而语义分析的主要任务就是对结构上正确的源程序进行上下文有关性质的审查,如进行类型检查。
好比咱们有如下代码
int a = 1; boolean b = false; char c = 2;
下面咱们有可能出现以下运算
int d = b+c;
其实上面的代码在结构上能构成准确的语法树,可是在语义上下面的运算是错误的。因此若是运行的话就会出现编译不经过,没法编译。
上面咱们了解了javac的过程,那么咱们直接来本身写一个简单的在已有类中添加代码的小工具,咱们就只生成set方法。首先写一个自定义的注解类。
@Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留 @Target(ElementType.TYPE) // 用于修饰类 public @interface MySetter { }
而后写对于此注解类的注解处理器类
@SupportedSourceVersion(SourceVersion.RELEASE_8) @SupportedAnnotationTypes("aboutjava.annotion.MySetter") public class MySetterProcessor extends AbstractProcessor { private Messager messager; private JavacTrees javacTrees; private TreeMaker treeMaker; private Names names; /** * @Description: 1. Message 主要是用来在编译时期打log用的 * 2. JavacTrees 提供了待处理的抽象语法树 * 3. TreeMaker 封装了建立AST节点的一些方法 * 4. Names 提供了建立标识符的方法 */ @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); this.messager = processingEnv.getMessager(); this.javacTrees = JavacTrees.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) { return false; } }
此处咱们注意咱们在init方法中得到一些编译阶段的一些环境信息。咱们从环境中提取出一些关键的类,描述以下。
JavacTrees
:提供了待处理的抽象语法树TreeMaker
:封装了操做AST抽象语法树的一些方法Names
:提供了建立标识符的方法Messager
:主要是在编译器打日志用的而后接下来咱们利用所提供的工具类对已存在的AST抽象语法树进行修改。主要的修改逻辑存在于process
方法中,若是返回是true的话,那么javac过程会再次从新从解析与填充符号表处开始进行。process
方法的逻辑主要以下
@Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MySetter.class); elementsAnnotatedWith.forEach(e->{ JCTree tree = javacTrees.getTree(e); tree.accept(new TreeTranslator(){ @Override public void visitClassDef(JCTree.JCClassDecl jcClassDecl) { List<JCTree.JCVariableDecl> jcVariableDeclList = List.nil(); // 在抽象树中找出全部的变量 for (JCTree jcTree : jcClassDecl.defs){ if (jcTree.getKind().equals(Tree.Kind.VARIABLE)){ JCTree.JCVariableDecl jcVariableDecl = (JCTree.JCVariableDecl) jcTree; jcVariableDeclList = jcVariableDeclList.append(jcVariableDecl); } } // 对于变量进行生成方法的操做 jcVariableDeclList.forEach(jcVariableDecl -> { messager.printMessage(Diagnostic.Kind.NOTE,jcVariableDecl.getName()+"has been processed"); jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl)); }); super.visitClassDef(jcClassDecl); } }); }); return true; }
其实看起来比较难,原理比较简单,主要是咱们对于API的不熟悉因此看起来很差懂,可是主要意思就是以下
@MySetter
注解所标注的类,得到其语法树用图表示的话,咱们建了一个测试类TestMySetter
,咱们知道其语法树的大体结构以下图所示。
那么咱们的目标就是将其语法树变成下图所示,由于最终生成字节码是根据语法树来生成的,因此咱们在语法树中添加了方法的节点,那么在生成字节码的时候就会生成对应方法的字节码。
其中生成方法节点的代码以下
private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl){ ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>(); // 生成表达式 例如 this.a = a; JCTree.JCExpressionStatement aThis = makeAssignment(treeMaker.Select(treeMaker.Ident(names.fromString("this")), jcVariableDecl.getName()), treeMaker.Ident(jcVariableDecl.getName())); statements.append(aThis); JCTree.JCBlock block = treeMaker.Block(0, statements.toList()); // 生成入参 JCTree.JCVariableDecl param = treeMaker.VarDef(treeMaker.Modifiers(Flags.PARAMETER), jcVariableDecl.getName(), jcVariableDecl.vartype, null); List<JCTree.JCVariableDecl> parameters = List.of(param); // 生成返回对象 JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType()); return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC),getNewMethodName(jcVariableDecl.getName()),methodType,List.nil(),parameters,List.nil(),block,null); } private Name getNewMethodName(Name name){ String s = name.toString(); return names.fromString("set"+s.substring(0,1).toUpperCase()+s.substring(1,name.length())); } private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) { return treeMaker.Exec( treeMaker.Assign( lhs, rhs ) ); }
最后咱们执行下面三个命令
javac -cp $JAVA_HOME/lib/tools.jar aboutjava/annotion/MySetter* -d javac -processor aboutjava.annotion.MySetterProcessor aboutjava/annotion//TestMySetter.java javap -p aboutjava/annotion/TestMySetter.class
能够看到输出的内容以下
Compiled from "TestMySetter.java" public class aboutjava.annotion.TestMySetter { private java.lang.String name; public void setName(java.lang.String); public aboutjava.annotion.TestMySetter(); }
能够看到字节码中已经生成了咱们须要的setName
方法。
到目前为止大概将Lombok的原理讲明白了,其实就是对于抽象语法树的各类操做。其实你们还能够利用编译期作许多的事情,例如代码规范的检查之类的。这里我只写了关于set方法的建立,你们有兴趣的能够本身写代码本身试一下关于Lombok的get方法的建立。
有感兴趣的能够关注一下我新建的公众号,搜索[程序猿的百宝袋]。或者直接扫下面的码也行。