• 用了那么久的Lombok,你知道它的原理么?


    序言

    在写Java代码的时候,最烦写setter/getter方法,自从有了Lombok插件不用再写那些方法之后,感觉再也回不去了,那你们是否好奇过Lombok是怎么把setter/getter方法给你加上去的呢?有的同学说我们Java引入Lombok之后会污染依赖包,那我们可不可以自己写一个工具来代替Lombok呢?

    知识点

    • Java编译过程
    • 了解Lombok原理
    • 了解插入式注解处理器

    分析

    序言提到的问题其实都是同一个问题,就是如何去获取和修改Java源代码?

    要回答这个问题,我们需要回答这几个问题:

    1. Java编译器是如何解析Java源代码的?
    2. 编译器编译源代码都有哪些步骤?
    3. 我们在编译器工作的时候,怎么才能去增加内容或者是进行代码分析?

    希望大家看完本文能够自己写一个简易的Lombok工具。

    回答

    如何解析源代码

    其实从我们的代码到被编译,中间隔了一个数据结构,叫做AST(抽象树)。具体的形式,可以查看下面的图片。右边的便是AST的数据结构了。

    图片

    代码编译都有哪些步骤

    整个编译过程大致如下:

    图片

    图片来自openjdk

    1.初始化插入注解处理器

    2.解析与填充符号表过程

    a.词法分析、语法分析。将源代码的字符流转变为标记集合,构造出抽象语法树。

    b.填充符号表。产生符号地址和符号信息。

    3.插入式注解处理器的注解处理过程:插入式注解处理器的执行阶段。后面我会给大家带来两个此方面的实用实战例子。

    4.分析与字节码生成过程

    a.标注检查。对语法的静态信息检查。

    b.数据流及控制流分析。对程序动态运行过程进行检查。

    c.解语法糖。将简化代码编写的语法糖还原为原有的形式。

    d.字节码生成。将前面各个步骤所生成的信息转化成为字节码。

    我们知道了上面的理论之后,接下来我们进行实战。带着大家一起去修改AST(抽象树)。添加自己的代码。

    实战

    如何自己实现一个自动添加Setter/Getter的工具

    首先,我们创建一个自己的注解。

    @Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留
    @Target(ElementType.TYPE) // 用于修饰类
    public @interface MySetterGetter {
    }
    
    • 1
    • 2
    • 3
    • 4

    创建一个需要生成setter/getter方法的实体类

    @MySetterGetter  // 打上我们的注解
    public class Test {
        private String wzj;
    }
    
    • 1
    • 2
    • 3
    • 4

    接下来就来看一看如何来生成我们想要的字符串。

    整体代码如下:

    @SupportedAnnotationTypes("com.study.practice.nameChecker.MySetterGetter")
    @SupportedSourceVersion(SourceVersion.RELEASE_8)
    public class MySetterGetterProcessor extends AbstractProcessor {
        // 主要是输出信息
        private Messager messager;
        private JavacTrees javacTrees;
    
        private TreeMaker treeMaker;
        private Names 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 annotations, RoundEnvironment roundEnv) {
            // 拿到被注解标注的所有的类
            Set elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(MySetterGetter.class);
            elementsAnnotatedWith.forEach(element -> {
                // 得到类的抽象树结构
                JCTree tree = javacTrees.getTree(element);
                // 遍历类,对类进行修改
                tree.accept(new TreeTranslator(){
                    @Override
                    public void visitClassDef(JCTree.JCClassDecl jcClassDecl) {
                        List 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);
                            }
                        }
                        
                        // 对于变量进行生成方法的操作
                        for (JCTree.JCVariableDecl jcVariableDecl : jcVariableDeclList) {
                            messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " has been processed");
                            jcClassDecl.defs = jcClassDecl.defs.prepend(makeSetterMethodDecl(jcVariableDecl));
    
                            jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
                        }
    
    
            // 生成返回对象
            JCTree.JCExpression methodType = treeMaker.Type(new Type.JCVoidType());
    
            return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewSetterMethodName(jcVariableDecl.getName()), methodType, List.nil(), parameters, List.nil(), block, null);
        }
        /**
         * 生成 getter 方法
         * @param jcVariableDecl
         * @return
         */
        private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl){
            ListBuffer statements = new ListBuffer<>();
            // 生成表达式
            JCTree.JCReturn aReturn = treeMaker.Return(treeMaker.Ident(jcVariableDecl.getName()));
            statements.append(aReturn);
            JCTree.JCBlock block = treeMaker.Block(0, statements.toList());
            // 无入参
            // 生成返回对象
            JCTree.JCExpression returnType = treeMaker.Type(jcVariableDecl.getType().type);
            return treeMaker.MethodDef(treeMaker.Modifiers(Flags.PUBLIC), getNewGetterMethodName(jcVariableDecl.getName()), returnType, List.nil(), List.nil(), List.nil(), block, null);
        }
        /**
         * 拼装Setter方法名称字符串
         * @param name
         * @return
         */
        private Name getNewSetterMethodName(Name name) {
            String s = name.toString();
            return names.fromString("set" + s.substring(0,1).toUpperCase() + s.substring(1, name.length()));
        }
        /**
         * 拼装 Getter 方法名称的字符串
         * @param name
         * @return
         */
        private Name getNewGetterMethodName(Name name) {
            String s = name.toString();
            return names.fromString("get" + s.substring(0,1).toUpperCase() + s.substring(1, name.length()));
        }
        /**
         * 生成表达式
         * @param lhs
         * @param rhs
         * @return
         */
        private JCTree.JCExpressionStatement makeAssignment(JCTree.JCExpression lhs, JCTree.JCExpression rhs) {
            return treeMaker.Exec(
                    treeMaker.Assign(lhs, rhs)
            );
        }
    }
    
    • 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

    代码有点多,我们逐一拆解说明:

    下面这是整个代码结构的脑图,后面的讲解会基于这个顺序。

    图片

    a. 注解

    @SupportedAnnotationTypes 表示我们需要监听的注解,比如我们之前定义的 @MySetterGetter。

    @SupportedSourceVersion 表示我们想要对什么版本的Java源代码进行处理。

    b. 父类

    AbstractProcessor是本次的核心类,编译器在编译的时候会扫描此类的子类。其中有一个子类必须实现的核心方法 public boolean process(Set annotations, RoundEnvironment roundEnv),此方法如果是返回为true就说明编译的那个类抽象树的结构又变化,需要重新进行词法分析和语法分析(可以查看上面提到的那个编译流程图)。如果返回的是false就说明没有变化。

    c. process方法

    主要的操作逻辑是:

    1.拿到所有被我们MySetterGetter标注的类。

    2.遍历所有的类,生成类的抽象树结构。

    3.对类进行操作:

    a.找到类中所有的变量。

    b.对变量进行生成Set和Get方法。

    4.返回 true,说明类结构变了,需要重新解析。如果是false说明没有变,不用重新解析。

    d. 操作JCTree树

    主要是在操作抽象树,可以查看文末附件中的文章进行学习。

    e. 方法名称拼接

    这一块儿和字符串拼接没啥区别,用过反射的同学应该也都清楚这个操作了。

    到此为止,我们就已经介绍完了Lombok的原理。怎么样是不是很简单。接下来,就让我们把它运行起来,投入到实战之中。

    f. 运行

    最后来看一下如何正确的运行这个我们写的工具。

    1. 环境

    我的系统环境是 macOs Monterey;

    java版本是

    openjdk version "1.8.0_302"
    OpenJDK Runtime Environment (Temurin)(build 1.8.0_302-b08)
    OpenJDK 64-Bit Server VM (Temurin)(build 25.302-b08, mixed mode)
    
    • 1
    • 2
    • 3

    2. 编译processor

    在你存放 MySetterGetter 和 MySetterGetterProcessor 两个类的目录下进行编译。

    javac -cp $JAVA_HOME/lib/tools.jar MySetterGetter.java MySetterGetterProcessor.java
    
    • 1

    执行成功后会出现这三个class文件。

    图片

    3. 声明插入式注解处理器

    图片

    1.在你的工程的resources下面创建一个包,名称为:META-INFO.services

    2.然后创建一个文件,名称为:javax.annotation.processing.Processor

    3.将你的注解处理器的地址填入,我的配置是这样的:

    com.study.practice.nameChecker.MySetterGetterProcessor

    4. 用我们的工具去编译目标类

    比如我们本次是要编译那个test.java。

    它的内容再回顾一下:

    @MySetterGetter  // 打上我们的注解
    public class Test {
        private String wzj;
    }
    
    • 1
    • 2
    • 3
    • 4

    然后我们就去编译它(注意类前面的路径。这个你们得换成自己的工程目录。)

    javac -processor com.study.practice.nameChecker.MySetterGetterProcessor com/study/practice/nameChecker/Test.java
    
    • 1

    执行之后如果没有修改我的代码的话会打印这几个字符串:

    process 1
    process 2
    注: wzj has been processed
    process 1
    
    • 1
    • 2
    • 3
    • 4

    最后会生成Test.class文件。

    图片

    5. 成果

    最后的class文件解析出来就是这个样子的。如下图所示:

    图片

    看到Setter/Getter方法就说明我们已经大功告成了!是不是很简单。

    到此为止,我们就学会了如何自己写一个属于自己的简易Lombok的插件了。

    附件

    treemarker 的介绍:

    http://www.docjar.com/docs/api/com/sun/tools/javac/tree/TreeMaker.html

  • 相关阅读:
    算法导论第16章 贪心算法之活动选择
    程序员都看不懂的代码
    基于Java的宠物商店管理系统设计与实现(源码+lw+部署文档+讲解等)
    面向对象编程-正则表达式
    第1章 计算机系统概述
    java-net-php-python-ssm动漫论坛系统查重PPT计算机毕业设计程序
    bin文件太大
    leetcode 72. Edit Distance 编辑距离(中等)
    【Spring Boot+Vue.js+JPA+Mysql】实现前后端分离的名片系统(附源码 超详细必看 可作为大作业使用)
    补充游戏思考13:游戏服务器杂谈(主要讲mmorpg,年更系列,未完待续10/20)
  • 原文地址:https://blog.csdn.net/agonie201218/article/details/126295798