百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

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

lipiwang 2025-06-09 23:25 2 浏览 0 评论

序言

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

知识点

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

分析

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

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

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

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

回答

如何解析源代码

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

代码编译都有哪些步骤

整个编译过程大致如下:

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

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

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

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

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

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

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

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

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

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

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

实战

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

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

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

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

@MySetterGetter  // 打上我们的注解
public class Test {
    private String wzj;
}

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

整体代码如下:

@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<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 拿到被注解标注的所有的类
        Set<? extends Element> 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<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);
                        }
                    }
                    
                    // 对于变量进行生成方法的操作
                    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<JCTree.JCStatement> 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)
        );
    }
}

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

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

a. 注解

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

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

b. 父类

AbstractProcessor是本次的核心类,编译器在编译的时候会扫描此类的子类。其中有一个子类必须实现的核心方法 public boolean process(Set<? extends TypeElement> 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)

2.编译processor

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

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

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

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

  • 在你的工程的resources下面创建一个包,名称为:META-INFO.services
  • 然后创建一个文件,名称为:javax.annotation.processing.Processor
  • 将你的注解处理器的地址填入,我的配置是这样的:

com.study.practice.nameChecker.MySetterGetterProcessor

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

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

它的内容再回顾一下:

@MySetterGetter  // 打上我们的注解
public class Test {
    private String wzj;
}

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

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

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

process 1
process 2
注: wzj has been processed
process 1

最后会生成Test.class文件。

5.成果

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

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

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

附件

treemarker 的介绍:

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

作者 | 王再军(曦峰)

原文链接:
https://click.aliyun.com/m/1000353148/

本文为阿里云原创内容,未经允许不得转载。

相关推荐

Redis 常见面试问题总结和答案(redisson面试题)

什么是Redis?Redis(RemoteDictionaryServer)Redis是一个开源的使用ANSIC语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、Ke...

青云QingCloud新增Memcached功能 完善缓存集

北京2015年1月30日电/美通社/--基础云服务商青云QingCloud日前宣布,为了更好的满足用户对缓存服务多样性的需求,正式推出基于Memcached的缓存集群服务。此次发布的M...

还不懂什么是Redis?一文详解Redis,入门学习看这一篇就够了

在我们日常的开发中,无不都是使用数据库来进行数据的存储,由于一般的系统任务中通常不会存在高并发的情况,所以这样看起来并没有什么问题,可是一旦涉及大数据量的需求,比如一些商品抢购的情景,或者是主页访问量...

redis一些常见的面试题(附答案)(redis经典面试题)

1、什么是redis?Redis是一个基于内存的高性能key-value数据库。2、Reids的特点  Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库统...

Java面试官最爱问的那些Redis面试题盘点

一、概述二、数据类型STRINGLISTSETHASHZSET三、数据结构字典跳跃表四、使用场景计数器缓存查找表消息队列会话缓存分布式锁实现其它五、Redis与Memcached数据类型数据持久化...

Redis为什么「操作速度」这么快以及在实际开发中的「应用场景」

Redis在目前分布式开发大环境下应用的场景比较多,但是大家有没有思考过为什么大家都愿意使用Redis呢?以及Redis为什么速度会这么快的原因呢?还有我们实际开发过程中使用Redis的场景又有哪些呢...

Redis分片(分布式缓存)(redis分片存储)

分片(partitioning)就是将你的数据拆分到多个Redis实例的过程,这样每个实例将只包含所有键的子集.(相关推荐:Redis教程)1分片何用Redis的分片承担着两个主要目标:o允...

您应该了解的20大系统设计概念(系统设计有哪些)

您应该了解的20大系统设计概念我应该从基础开始,比如可扩展性、可用性、可靠性这些。这些都是系统设计的核心要素。接下来,可能需要考虑性能相关的,比如延迟和吞吐量,这两者常常被一起讨论。然后是存储,数据库...

一文彻底弄懂 TPS RPS QPS(tps/cps)

以下是关于RPS、QPS、TPS的核心区别与关联的总结,结合实际场景和优化建议:一、核心定义与区别RPS:RequestsPerSecond每秒请求数客户端到服务器的完整请求数量Web服务...

redis,memcached,nginx网络组件

1.理解阻塞io,非阻塞io,同步io,异步io的区别2.理解BIO和AIO的区别io多路复用只负责io检测,不负责io操作阻塞io中的write,能写多少是多少,只要写成功就返回,譬如准备写500字...

为什么 Redis 单线程却能支撑高并发?

#暑期创作大赛#为什么Redis单线程却能支撑高并发?Redis和memcached有什么区别?Redis的线程模型是什么?为什么Redis单线程却能支撑高并发?这个是问Redis的...

值得一看的35个Redis面试题总结(redis面试题汇总)

Redis是一个基于内存的高性能key-value数据库。作者:民工哥1.什么是redis?Redis是一个基于内存的高性能key-value数据库。2.Reids的特点  Redis本质上是一...

内存数据库优缺点一览:Redis、Memcached、MongoDB和Ignite

内存数据库是一种将数据存储在内存中的数据库,相比于传统的磁盘数据库,内存数据库具有更高的性能、更低的延迟和更好的并发能力。内存数据库广泛应用于高速缓存、实时分析、游戏、社交网络等场景。本文将对比介绍四...

Java面试 | 关于Redis 的面试题(redis面试题2021)

1.Redis是什么?Redis本质上是一个Key-Value类型的内存数据库,很像memcached,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。因...

数字化与智能化技术:分布式缓存数据库Redis

一、Redis概述Redis远程字典服务(RemoteDictionaryServer),是一个开源的使用ANSIC语言编写的,基于内存的分布式缓存Key-Value数据库。1、Redis数据...

取消回复欢迎 发表评论: