Lombok
1. 什么是 Lombok ?
简单介绍一下: Lombok 是一个 Java 语言的开发工具库。在面对对象的编程语言中,我们常常会需要构建大量的 POJO 对象,同时维护对应的 get set 方法。我相信使用过 Java 的开发者们能都能体会到那种重复编写get set 方法的痛苦(即使IDE有自动生成的功能,当你补充或者删除一个属性的时候,你还得从新生成)。Lombok 最主要的功能就是解决这样的一个问题。
Lombok 是属于开源组织 projectlombok 下的一个项目,作者主要是 Reinier Zwitserloot
Lombok 单词的意思是什么?网上是这样解释的:
The word Lombok is a word that comes from the local Sasak language. Translated into Indonesian it means ‘straight’. Lombok is also a lesser-used word for chilli in Bahasa Indonesia, which has led many people to believe the island is named for its spicy cuisine.
Lombok 的作用【这段来自官网主页的介绍】: Project Lombok is a java library that automatically plugs into your editor and build tools, spicing up your java.Never write another getter or equals method again, with one annotation your class has a fully featured builder, Automate your logging variables, and much more.
ps:Lombok 的源码仓库
https://github.com/projectlombok/lombok
ps:的官网
https://projectlombok.org/
2. 如何使用Lombok ?
-
引入 lombok 的 jar ,这里你可以通过 lombok 的源码库去获取最新的版本,或者到 maven 官方仓库寻找。
-
在对应的类上添加注解即可,例如:
/** * 用户对象 */ @Getter @Setter public class UserPO implements Serializable { /** 主键ID */ private String id; /** 本系统用户账号 */ private String account; /** 显示姓名 */ private String username; }
-
这个存在一个问题是:通过注解,在编译的时候,会帮我们自动生成 get 和 set 方法,但是没编译之前,idea 是认为它没有 get 和 set 方法的,所以当你创建一个 UserPO 对象的时候,你是获取不到它的 get 和 set 方法的,那怎么办 ?这其实也是 Lombok 的一个问题之一,不过现在主流的 Idea 中都提供了 Lombok 的插件用于解决这个问题。比如 Intellij Idea中,可以通过在插件市场下载安装 Lombok plugin 。
3. Lombok 是怎么实现的?
以下内容参考和引用: https://blog.csdn.net/w1014074794/article/details/128244097
lombok 其实本质上就是在编译 java 源码到字节码的时候,通过识别注解,自动生成了对应的 get 和 set 方法等等。
Java 在编译的时候,还能动态生成生成和修改字节码?它是怎么做的呢?
一切的开始还得从 Java 6 说起,自Java 6 起 ,javac 指令开始支持 JSR 269 Pluggable Annotation Processing API 规范,只要程序实现了该API,就能在java源码编译时调用定义的注解。
具体的执行流程:
- javac对源代码进行分析,生成一棵抽象语法树(AST)
- 运行过程中调用实现了 “JSR 269 API” 的程序
- 通过实现了 “JSR 269 API” 的程序,操作抽象语法树(AST)
- javac使用修改后的抽象语法树(AST)生成字节码文件
现在我们就直接手动进行简单的实现
3.1 先创建两个简单的工程
工程A:util
工程B:test
工程 B 依赖工程 A
【ps:一定要构建两个项目,不然编译的时候会存在异常:Javax.annotation.processing.Processor: Provider xxx not found,因为在编译的时候,javac会去找所有jar包及项目(模块)里 resource/META-INF/services/javax.annotation.processing.Processor 这个文件中配置的类信息,记住是类信息,它会通过classloader去加载这个类,此时如果是在同一个项目(模块)中的文件,因为是在编译期,尚未生成class文件,自然也就找不到对应的类。】
方法1:去掉javax.annotation.processing.Processor文件,等编译完成后,再将文件拷贝到对应的目录去(但每次编译后都需要拷贝)
方法2:去掉javax.annotation.processing.Processor文件,在需要使用编译的项目(模块)添加此文件(推荐,无依赖)
方法3:使用谷歌的autoService注解(推荐)
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0.1</version>
</dependency>
我们这里通过两个工程的方式来解决这个问题,其实就是方法2
3.2 工程 A utils
A. 添加依赖包:
<!--Processor中的解析过程需要依赖tools.jar-->
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.6.0</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
B. 构建自定义注解 CustomGetSet
@Retention(RetentionPolicy.SOURCE) // 在源码中有效
@Target(ElementType.TYPE) // 作用于类,接口
public @interface CustomGetSet {
}
C. 构建自定义注解处理器 CustomGetSetProcessor
核心思路就是继承 AbstractProcessor 类,在 process() 方法中操作抽象语法树
public class CustomGetSetProcessor extends AbstractProcessor {
private Messager messager; // 编译时期输入日志的
private JavacTrees javacTrees; // 提供了待处理的抽象语法树
private TreeMaker treeMaker; // 封装了创建AST节点的一些方法
private Names names; // 提供了创建标识符的方法
//支持的注解类型
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annotataions = new LinkedHashSet<String>();
annotataions.add(CustomGetSet.class.getCanonicalName());
return annotataions;
}
//支持的java版本
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@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(CustomGetSet.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);
}
}
// 对于变量进行生成方法的操作
jcVariableDeclList.forEach(jcVariableDecl -> {
messager.printMessage(Diagnostic.Kind.NOTE, jcVariableDecl.getName() + " has been processed");
jcClassDecl.defs = jcClassDecl.defs.prepend(makeGetterMethodDecl(jcVariableDecl));
});
super.visitClassDef(jcClassDecl);
}
});
});
return true;
}
/**
* 生成 getter 方法
* @param jcVariableDecl
* @return
*/
private JCTree.JCMethodDecl makeGetterMethodDecl(JCTree.JCVariableDecl jcVariableDecl){
ListBuffer<JCTree.JCStatement> statements = new ListBuffer<>();
// 生成表达式 例如 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),
getNewGetterMethodName(jcVariableDecl.getName()), methodType, List.nil(),
parameters, 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)
);
}
}
3.3 工程 B test
A. 在工程目录下构建一个 resources 文件夹,文件夹下构建 META-INF/services/javax.annotation.processing.Processor 文件,文件内容是你在工程 A 中定义的 CustomGetSetProcessor 自定义处理器的全限定类名。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wmi5My94-1693276492024)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c5d550cd-d781-4fac-8e44-40b576e77c25/Untitled.png)]
B. 在工程 B 中,我们定义一个测试类 TestBean 对象,并使用自定义注解 CustomGetSet
@CustomGetSet
public class TestBean {
private String name;
}
3.4 测试编译后的 TestBean 是否生成 get 方法
你可以通过 javac 指令的方式编译,也可以通过 maven 的方式,我因为是以 maven 的方式构建的工程,因此就通过 maven 的方式进行操作
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ng8ZHujT-1693276492025)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/376606e4-7b52-4b78-a8ad-cae9c32c8760/Untitled.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nW8kwnwD-1693276492026)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/eef4243a-0c87-4083-a893-44a14d70fe8f/Untitled.png)]
查看编译后的 TestBean 的字节码,的确是生成了 get 方法。至此操作就结束了。当然 Lombok 的实现复杂得多,但是总体的流程和思路就是如此。有兴趣的,大家可以去 github 上学习学习源码。地址小编上面已经给出来了。
4. Lombok我们应该使用?
想必坚持和反对的声音应该都不少吧,哈哈哈,说到该不该使用 Lombok,我觉得我们得先了解 Lombok 的优缺点,同时更应该结合自己项目的使用情况来决定是否使用。
优点:
- 可用来帮助开发人员消除Java的冗长代码,尤其是对于简单的Java对象(POJO)。
- 提高效率
- 简单,通过简单的一些注解就可以实现
缺点:
-
代码的可阅读性变差了,调试代码的时候不是很友好。【ps:尤其是在你IDEA没有安装Lombok插件的时候】
-
插件问题,如果未安装插件,使用IDE打开一个基于Lombok的项目的话会提示找不到方法等错误。导致项目编译失败。所以只要你负责的项目使用了Lombok,就必须安装插件。
-
不了解实现细节,引起的坑。比如:
当我们使用@Data定义一个类的时候,会自动帮我们生成equals()方法 。 但是如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的话, 会默认是@EqualsAndHashCode(callSuper=false) 这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性,无论父类属性访问权限是否开放。
建议:
基于小编个人的感受来说:
业务项目类:
当我们项目中,在处理大量业务类型的 POJO 对象的时候,还是比较建议使用 Lombok的。因为业务类型的 POJO 的字段是变动还是比较频繁的,如果每次都手动进行生成 get set 这类的方法,太繁琐了,而且容易出错,遗漏和忘记。字段如果很多的话,代码还极其不优雅,效率低下。
核心类,框架类:
如果是对于项目核心的类库,还有就是自身项目的框架,小编还是不太建议使用 Lombok,因为对于核心的类库,一般都有着很强的结构设计和代码设计。Lombok 容易破坏这种结构设计。