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 的源码仓库
https://github.com/projectlombok/lombok
Lombok 的官网:
https://projectlombok.org/

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

  1. 如何使用Lombok ?
    引入 lombok 的 jar ,这里你可以通过 lombok 的源码库去获取最新的版本,或者到 maven 官方仓库寻找。
    在对应的类上添加注解即可,例如:
/**
 * 用户对象
 */
@Getter
@Setter
public class UserPO implements Serializable {
   /** 主键ID */
   private String id;
   /** 本系统用户账号 */
   private String account;
   /** 显示姓名 */
   private String username;
}
  1. 这里存在一个问题是:通过注解,在编译的时候,会帮我们自动生成 get 和 set 方法,但是没编译之前,idea 是认为它没有 get 和 set 方法的,所以当你创建一个对象的时候,你是获取不到它的 get 和 set 方法的,那怎么办 ?这其实也是 Lombok 的一个问题之一,不过现在主流的 Idea 中都提供了 Lombok 的插件用于解决这个问题。比如 Intellij Idea中,可以通过在插件市场下载安装 「Lombok plugin」 。
  2. 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注解(推荐)


    com.google.auto.service
    auto-service
    1.0.1


我们这里通过两个工程的方式来解决这个问题,其实就是方法2

3.2 工程 A utils
A. 添加依赖包:



  com.sun
    tools
    1.6.0
    system
    ${java.home}/../lib/tools.jar

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 getSupportedAnnotationTypes() {
        Set annotataions = new LinkedHashSet();
        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 annotations, RoundEnvironment roundEnv) {
        // 拿到被注解标注的所有的类
        Set 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 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 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 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 自定义处理器的全限定类名。
Lombok之轻描淡写_第1张图片
B. 在工程 B 中,我们定义一个测试类 TestBean 对象,并使用自定义注解 CustomGetSet

@CustomGetSet
public class TestBean {
    private String name;
}

3.4 测试编译后的 TestBean 是否生成 get 方法
你可以通过 javac 指令的方式编译,也可以通过 maven 的方式,我因为是以 maven 的方式构建的工程,因此就通过 maven 的方式进行操作

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

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

你可能感兴趣的:(java)