Redian新闻
>
用了那么久的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. 声明插入式注解处理器

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;}

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

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

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

process 1process 2: wzj has been processedprocess 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和阿里云定制礼品,更有多重福利点击链接查看活动详情。

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
电影风格的秘密,你知道多少?说三道四(109) 见贤思齐 取法乎上光知道用【Doge】表情,你知道Doge是什么狗么?摊开来聊聊996和内卷,那些你知道和不知道的事情。。。618剁手剁得欢,你知道快递多不容易吗?学做Tapas中的一道素菜:芦笋蘑菇Chinese Men Still Get a Pass on Domestic Labor. Even From Women.为了让全民吸毒,你知道美国有多努力吗?改善拖延必经的3个阶段(不难),你知道吗?这些医学小秘密对身体有好处,你知道吗(八)?平淡忙碌中寻找快乐的我EMBO Rep I清华大学程功团队揭示登革病毒适应性进化促进登革热持续流行的原因特别有才的人往往是在为人处世方面有些缺陷的,比如陈景润,那绝对在现在拿不到TENURE。。Good Book with Red Book,假期我们一起 Hard Work!话题互动|这些关于左撇子的真相,你知道多少?一日一诗:“袅袅而出,轻轻飘散/ 我们只看到/ 它的婉转 低徊,却忽略它的悠长 高远”||王国良:烟火(读诗版)谈了这么久的元宇宙营销,终于被DFS玩明白了!为什么绿表火了那么多年?夏天手上戴点绿,这28块表不容错过…秋季胶囊衣橱 | 9件单品搞定24套look,好看又省心大家觉得澳洲留学市场多久才会真的恢复?大学真的赚了那么多吗?这些医学小秘密对身体有好处,你知道吗(十)?短波笔记(13)-川普、国会听证会为什么读了那么多书,语文还是学不好?冠脉痉挛不容忽视,你可能不知道它有多“狠”知名童书下架!中国每年出版 4 万种图书,你知道该怎么给孩子选吗?Lombok 同时使用 @Data 和 @Builder 的巨坑,千万别乱用!韩媒:no look,国民屈辱!你默默关注许久的科技大厂开始招收newgrad了你知道吗?为了成为一只导盲犬,你知道它有多努力吗看完投行实习生做的Pitch Book,老板当场让他提前转正…小长假又来了,你知道关于哈芝节的这些知识吗?大漠亲子游攻略:走了那么多国家,这才是我最留恋的地方发生了那么多事,如何对未来有信心?今日聚焦:问责27人!“毒教材”事件终于等来结果!“影响学生一辈子”,你知道,可为什么还犯错?A爆了!看了那么多科普,这套篇篇都值得五星!
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。