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

创建一个需要生成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


ModelScope开源模型社区评测征集令

ModelScope开源模型社区评测专场重磅来袭,发布你的评测,免费使用模型库搭建属于你的应用,有机会获得AirPods和阿里云定制礼品,更有多重福利,点击这里查看活动详情。

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

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

用了那么久的Lombok,你知道它的原理么?的更多相关文章

  1. lombok使用总结

    前提 这篇文章主要介绍lombok的使用,至于lombok的源码和原理暂不探究,可以看上一篇文章插件化注解处理API去了解lombok的基本原理.参考资料: lombok官网 lombok官方教程-l ...

  2. Lombok的使用详解(最详尽的解释,覆盖讲解所有可用注解),解决@Builder.Default默认值问题

    原文:https://blog.csdn.net/f641385712/article/details/82081900 前言 Lombok是一款Java开发插件,使得Java开发者可以通过其定义的一 ...

  3. Lombok的使用与原理

        在面向对象编程中必不可少需要在代码中定义对象模型,而在基于Java的业务平台开发实践中尤其如此.相信大家在平时开发中也深有感触,本来是没有多少代码开发量的,但是因为定义的业务模型对象比较多,而 ...

  4. Java开发神器Lombok的使用与原理

    在面向对象编程中必不可少需要在代码中定义对象模型,而在基于Java的业务平台开发实践中尤其如此.相信大家在平时开发中也深有感触,本来是没有多少代码开发量的,但是因为定义的业务模型对象比较多,而需要重复 ...

  5. Lombok认知

    Lombok的简介 Lombok是一款Java开发插件,公司项目到处使用,整体效果很棒,代码更干净.Java开发人员可以节省出重复构建,诸如hashCode和equals这样的方法以及各种业务对象模型 ...

  6. 99%的程序员都在用Lombok,原理竟然这么简单?我也手撸了一个!|建议收藏!!!

    罗曼罗兰说过:世界上只有一种英雄主义,就是看清生活的真相之后依然热爱生活. 对于 Lombok 我相信大部分人都不陌生,但对于它的实现原理以及缺点却鲜为人知,而本文将会从 Lombok 的原理出发,手 ...

  7. 大白话讲解Promise(三)搞懂jquery中的Promise

    前两篇我们讲了ES6中的Promise以及Promise/A+规范,在Promise的知识体系中,jquery当然是必不可少的一环,所以本篇就来讲讲jquery中的Promise,也就是我们所知道的D ...

  8. 239. Sliding Window Maximum

    题目: Given an array nums, there is a sliding window of size k which is moving from the very left of t ...

  9. NFS服务搭建与配置

    启动NFS SERVER之前,首先要启动RPC服务(CentOS5.8下为portmap服务,CentOS6.6下为rpcbind服务,下同),否则NFS SERVER就无法向RPC服务注册了.另外, ...

  10. 彻底理解Javascript 中的 Promise(-------------------------------***---------------------------------)

    ES6原生提供了 Promise 对象. 到底是何方妖怪呢?打出来看看: 所谓 Promise,就是一个对象,用来传递异步操作的消息.它代表了某个未来才会知道结果的事件(通常是一个异步操作),并且这个 ...

随机推荐

  1. 技能get-ps抠颜色一样的图

    公司要插个小图片,从网上down下来的图片是不过是jpg的,背景不透明,这时候可以使用ps工具把这种同一颜色的内容扣下来. 操作步骤: 选择-色彩范围,然后用取样器取颜色,再调节拉条选取颜色范围,最后 ...

  2. [置顶] linux与windows之间传输文件工具rz上传大文件失败问题解决方案

    rz,sz是Linux/Unix同Windows进行ZModem文件传输的命令行工具. windows端需要支持ZModem的telnet/ssh客户端(比如SecureCRT),运行命令rz即是接收 ...

  3. SpringMVC异常之The request sent by the client was syntactically incorrect解决方案

    最近在做SpringMVC开发的时候,直接访问后台的controller,出现如下异常 这个问题是什么原因造成的呢? 后来经过测试发现,是表单提交的内容数据类型与实体的(也就是数据表字段)的数据类型不 ...

  4. Android源码在线查看网站

    一.aospxref http://aospxref.com/ 优点:更新速度快 缺点:历史版本较少 二.androidxref http://androidxref.com/ 优点:历史版本较多 缺 ...

  5. vim的使用进步

    vim的使用进步 1.如果遇到命令行中无法退出的 狂按esc按键 或者也可以使用v模式下切换一下,之后按esc 保存退出 保存退出--:wq 保存:w 不保存退出:q! i--插入模式 v- 可视化模 ...

  6. 你是怎么理解ES6中 Promise的?使用场景?

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一.介绍 Promise,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大 在以往我们如果处理多层异步操 ...

  7. SMASH:经典One-Shot神经网络搜索,仅需单卡 | ICLR 2018

    SMASH方法使用辅助网络生成次优权重来支持网络的快速测试,从结果来看,生成的权重与正常训练的权重在准确率上存在关联性,整体搜索速度很快,仅需要单卡进行搜索,提供了一个很好的新思路.   来源:晓飞的 ...

  8. KingbaseES V8R6集群运维案例之---自动清理集群主库wal日志

    ​ 案例说明: 在KingbaseES V8R6 主备流复制的集群,配置复制槽(replication slot).复制槽提供了一种自动化的方法来确保主控机在所有的后备机收到 WAL 段 之前不会移除 ...

  9. python 处理国家标准行业编码(编码·门类·大类·中类·小类)

    进度 今天完整地进行了行业维度的清洗分析,把行业代码根据国家标准清洗出格式为  "编码·门类·大类·中类·小类"  的数据格式 过程 1.先把国家标准编码转化为json数据 2.根 ...

  10. 03-【HAL库】STM32实现SYN6288模块语音播报.md

    一.什么是SYN6288模块 1.概述 ​ SYN6288 中文语音合成芯片是北京宇音天下科技有限公司于2010 年初推出的一款性/价比更高,效果更自然的一款中高端语音合成芯片.SYN6288 通过异 ...