Lombok经常用,但是你知道它的原理是什么吗?(二)

Stella981
• 阅读 530

在上一篇Lombok经常用,但是你知道它的原理是什么吗?简单介绍了注解处理器,是用来处理编译期的注解的一个工具,我们只是自己生成了一些代码,但是和Lombok却不一样,因为Lombok是在原有类的基础上增加了一些类,你那么Lombok是如何做到修改原有类的内容呢?接下来我们就再进一步了解Lombok的原理。

Javac原理

既然我们是在编译期对类进行操作了,那么我们就需要了解在Java中Javac到底对程序做了什么。Javac对代码编译的过程其实就是用Java来写的,我们可以查看其源码对其简单的分析,如何下载源码,Debug源码这里我就不进行分析了,推荐一篇文章写的挺好的。Javac 源码调试教程

编译过程大致分为了三个阶段

  • 解析与填充符号表
  • 注解处理
  • 分析与字节码生成

这三个阶段的交互过程如下图所示。

Lombok经常用,但是你知道它的原理是什么吗?(二)

解析与填充符号表

这一步骤是两个步骤,包括了解析和填充符号,其中解析是分为词法分析语法分析两个步骤。

词法分析和语法分析

词法分析就是将源代码的字符流转变为Java中的标记(Token)集合,单个字符是程序编写过程中最小的元素,而标记(Token)则是编译过程中最小的元素,关键字、变量名、字面量、运算符都可以成为标记(Token)。比如在Java中int a = b+2,这段代码则表示了6个标记Token,分别是int、a、=、b、+、2。虽然关键字int是由三个字符构成的,但是它只是一个Token,不可以再拆分了。

语法分析是根据Token序列构造抽象对象树的过程,抽象语法树(Abstract syntax tree),是一种用来描述代码语法结构的树形表示方法,语法树的每一个节点都代表着程序代码中的一个语法结构,例如包、类型、修饰符、运算符、接口、返回值甚至是代码注释都是一个语法结构。

语法分析分析出来的树结构是由JCTree 来表示的,我们可以看一下它的子类有哪些。

Lombok经常用,但是你知道它的原理是什么吗?(二)

我们自己建一个类,可以观察它在编译过程中用树结构表示是一种怎样的结构。

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类,这些都是作为树的构成一种。

Lombok经常用,但是你知道它的原理是什么吗?(二)

填充符号表

> 填充符号表和我们的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;

其实上面的代码在结构上能构成准确的语法树,但是在语义上下面的运算是错误的。所以如果运行的话就会出现编译不通过,无法编译。

自己实现一个简单的Lombok

上面我们了解了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-&gt;{
            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 -&gt; {
                        messager.printMessage(Diagnostic.Kind.NOTE,jcVariableDecl.getName()+"has been processed");
                        jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));
                    });
                    super.visitClassDef(jcClassDecl);
                }
            });
        });
        return true;
    }

其实看起来比较难,原理比较简单,主要是我们对于API的不熟悉所以看起来不好懂,但是主要意思就是如下

  1. 找到@MySetter注解所标注的类,获得其语法树
  2. 遍历其语法树,找到其参数节点
  3. 自己建一个方法节点,并添加到语法树中

用图表示的话,我们建了一个测试类TestMySetter,我们知道其语法树的大致结构如下图所示。

Lombok经常用,但是你知道它的原理是什么吗?(二)

那么我们的目标就是将其语法树变成下图所示,因为最终生成字节码是根据语法树来生成的,所以我们在语法树中添加了方法的节点,那么在生成字节码的时候就会生成对应方法的字节码。

Lombok经常用,但是你知道它的原理是什么吗?(二)

其中生成方法节点的代码如下

private JCTree.JCMethodDecl makeSetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl){

    ListBuffer<jctree.jcstatement> statements = new ListBuffer&lt;&gt;();
    // 生成表达式 例如 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方法的创建。

参考

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Karen110 Karen110
2年前
​一篇文章总结一下Python库中关于时间的常见操作
前言本次来总结一下关于Python时间的相关操作,有一个有趣的问题。如果你的业务用不到时间相关的操作,你的业务基本上会一直用不到。但是如果你的业务一旦用到了时间操作,你就会发现,淦,到处都是时间操作。。。所以思来想去,还是总结一下吧,本次会采用类型注解方式。time包importtime时间戳从1970年1月1日00:00:00标准时区诞生到现在
Wesley13 Wesley13
2年前
Java中的屠龙之术——如何修改语法树
在Lombok经常用,但是你知道它的原理是什么吗?(https://www.oschina.net/action/GoToLink?urlhttps%3A%2F%2Fjuejin.im%2Fpost%2F5e54d38a6fb9a07cbf46b3ca),和Lombok经常用,但是你知道它的原理是什么吗?(二)(https://www.oschin
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
2年前
Lombok经常用,但是你知道它的原理是什么吗?
相信大家在项目中都使用过Lombok,因为能够简化我们许多的代码,但是该有的功能一点也不少。那么lombok到底是个什么呢,lombok是一个可以通过简单的注解的形式来帮助我们简化消除一些必须有但显得很臃肿的Java代码的工具,简单来说,比如我们新建了一个类,然后在其中写了几个字段,然后通常情况下我们需要手动去建立getter和setter方法啊,构造函
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这