Lombok之详细分析底层原理

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 ?

  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 是怎么实现的?

以下内容参考和引用: 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源码编译时调用定义的注解。

具体的执行流程:

  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 自定义处理器的全限定类名。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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 的优缺点,同时更应该结合自己项目的使用情况来决定是否使用。

优点:

  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 容易破坏这种结构设计。

你可能感兴趣的:(java)