Lombok

408 阅读8分钟

Lombok

1. 什么是 Lombok ?

简单介绍一下: Lombok 是一个 Java 语言的开发工具库。在面对对象的编程语言中,我们常常会需要构建大量的 POJO 对象,同时维护对应的 get set 方法。我相信使用过 Java 的开发者们能都能体会到那种重复编写 get set 方法的痛苦(即使IDE有自动生成的功能,当你补充或者删除一个属性的时候,你还得从新生成)。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.

Lombok 的源码仓库

github.com/projectlomb…

Lombok 的官网: projectlombok.org/

Project Lombok最初是由Reinier Zwitserloot(在Twitter上被称为@surial)和Roel Spilker在2009年之前创建的。名称“Lombok”从位于Java东部的同名印度尼西亚岛屿汲取灵感。此外,“Lombok”是印尼语中的“辣椒”的意思,与该库的标语“Spicing up your Java”相一致。

2. 如何使用Lombok ?

  1. 引入 lombok 的 jar ,这里你可以通过 lombok 的源码库去获取最新的版本,或者到 maven 官方仓库寻找。

  2. 在对应的类上添加注解即可,例如:

    /**
     * 用户对象
     */
    @Getter
    @Setter
    public class UserPO implements Serializable {
        /** 主键ID */
        private String id;
        /** 本系统用户账号 */
        private String account;
        /** 显示姓名 */
        private String username;
    }
    
  3. 这里存在一个问题是:通过注解,在编译的时候,会帮我们自动生成 get 和 set 方法,但是没编译之前,idea 是认为它没有 get 和 set 方法的,所以当你创建一个 UserPO 对象的时候,你是获取不到它的 get 和 set 方法的,那怎么办 ?这其实也是 Lombok 的一个问题之一,不过现在主流的 Idea 中都提供了 Lombok 的插件用于解决这个问题。比如 Intellij Idea中,可以通过在插件市场下载安装 Lombok plugin

3. Lombok 是怎么实现的?

以下内容参考和引用: blog.csdn.net/w1014074794…

lombok 其实本质上就是在编译 java 源码到字节码的时候,通过识别注解,自动生成了对应的 get 和 set 方法等等。

Java 在编译的时候,还能动态生成生成和修改字节码?它是怎么做的呢?

一切的开始还得从 Java 6 说起,自 Java 6 起 ,javac 指令开始支持 JSR 269 Pluggable Annotation Processing API 规范,只要程序实现了该API,就能在java源码编译时调用定义的注解。

具体的执行流程:

  1. javac对源代码进行分析,生成一棵抽象语法树(AST)
  2. 运行过程中调用实现了 "JSR 269 API" 的程序
  3. 通过实现了 "JSR 269 API" 的程序,操作抽象语法树(AST)
  4. 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 自定义处理器的全限定类名。

Untitled.png

B. 在工程 B 中,我们定义一个测试类 TestBean 对象,并使用自定义注解 CustomGetSet

@CustomGetSet
public class TestBean {
    private String name;
}

3.4 测试编译后的 TestBean 是否生成 get 方法

你可以通过 javac 指令的方式编译,也可以通过 maven 的方式,我因为是以 maven 的方式构建的工程,因此就通过 maven 的方式进行操作

Untitled 1.png

Untitled 2.png

查看编译后的 TestBean 的字节码,的确是生成了 get 方法。至此操作就结束了。当然 Lombok 的实现复杂得多,但是总体的流程和思路就是如此。有兴趣的,大家可以去 github 上学习学习源码。地址小编上面已经给出来了。

4. Lombok我们应该使用?

想必坚持和反对的声音应该都不少吧,哈哈哈,说到该不该使用 Lombok,我觉得我们得先了解 Lombok 的优缺点,同时更应该结合自己项目的使用情况来决定是否使用。

优点:

  1. 可用来帮助开发人员消除Java的冗长代码,尤其是对于简单的Java对象(POJO)。
  2. 提高效率
  3. 简单,通过简单的一些注解就可以实现

缺点:

  1. 代码的可阅读性变差了,调试代码的时候不是很友好。【ps:尤其是在你IDEA没有安装Lombok插件的时候】

  2. 插件问题,如果未安装插件,使用IDE打开一个基于Lombok的项目的话会提示找不到方法等错误。导致项目编译失败。所以只要你负责的项目使用了Lombok,就必须安装插件。

  3. 不了解实现细节,引起的坑。比如:

    当我们使用@Data定义一个类的时候,会自动帮我们生成equals()方法 。
    但是如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的话,
    会默认是@EqualsAndHashCode(callSuper=false)
    这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性,无论父类属性访问权限是否开放。
    

建议:

基于小编个人的感受来说:

业务项目类:

当我们项目中,在处理大量业务类型的 POJO 对象的时候,还是比较建议使用 Lombok的。因为业务类型的 POJO 的字段是变动还是比较频繁的,如果每次都手动进行生成 get set 这类的方法,太繁琐了,而且容易出错,遗漏和忘记。字段如果很多的话,代码还极其不优雅,效率低下。

核心类,框架类:

如果是对于项目核心的类库,还有就是自身项目的框架,小编还是不太建议使用 Lombok,因为对于核心的类库,一般都有着很强的结构设计和代码设计。Lombok 容易破坏这种结构设计。


更多内容欢迎关注 [ 小巫编程室 ] 公众号,喜欢文章的话,也希望能给小编点个赞或者转发,你们的喜欢与支持是小编最大的鼓励,小巫编程室感谢您的关注与支持。good good study day day up

qrcode_for_xiaowu.jpg