• Lombok详解


    1. Lombok简介

    lombok官网说明:https://projectlombok.org/

    官方说明:
    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.

    Project Lombok是一个java库,它可以自动插入到您的编辑器和构建工具中,为您的java增添趣味。
    永远不要再写另一个getter或equals方法了,只要有一个注释,你的类就有了一个功能齐全的构建器,自动化你的日志变量,等等。

    Lombok简单理解就是一个Java类库,通过注解的形式帮助开发减少一些结构化代码的开发工作,提高开发效率,比如通过@Data注解,class在编译的时候会自动生成get,set,equals,hash,toString等方法,避免写大量的代码,减少了代码量,也使代码看起来更加简洁。尤其是一些对象属性需要改动的时候,每次改动都需要重新生成get,set,equals,hash,toString等方法,而使用注解则可以避免此问题。

    2. Lombok使用说明

    2.1 使用说明
    如果要使用lombok,首先开发工具IntelliJ IDEA或者Eclipse需要先安装插件支持,其次需要引入依赖。

    2.2 安装插件
    1.IDEA在线安装Lombok插件
    File > Settings > Plugins >Marketplace,搜索Lombok,点击install,弹窗Accept,然后安装好后Restart IDEA。
    在这里插入图片描述2.IDEA离线安装Lombok插件
    首先下载离线插件,这里要选择idea对应的版本,否则不兼容。
    下载地址:https://plugins.jetbrains.com/plugin/6317-lombok/versions
    在这里插入图片描述
    2.3 引入依赖
    Maven项目可以在pom.xml中配置依赖坐标即可。

    <dependencies>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.24version>
            <scope>providedscope>
        dependency>
    dependencies>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注意事项:
    1.provided表示该包只在编译和测试的时候用,项目真正打成包时不会将Lombok包打进去。
    2.Lombok还支持其他构建方法,比如Ant、Gradle、Kobalt,有需要的可以参考官网的Install菜单下的Build Tools,其他使用方法也可以参考Install菜单。

    3. Lombok功能说明

    lombok官方API文档https://projectlombok.org/api/

    1.@NonNull该注解用在属性或构造器上,Lombok会生成一个非空的声明,可用于校验参数,能帮助避免空指针。

    
        /**
         * 1.@NonNull该注解用在属性或构造器上,Lombok会生成一个非空的声明,可用于校验参数,能帮助避免空指针。
         * 参数User为null时产生异常:NullPointerException
         */
        public static String getName(@NonNull User user) {
            return user.getName();
        }
    
        /**
         * 等价@NonNull
         */
        public static String getName2(User user) {
            if (user == null) {throw new NullPointerException("user is marked non-null but is null");}
            return user.getName();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.@Getter 和@Setter 注解在类或字段,注解在类时为所有字段生成getter,setter方法,注解在字段上时只为该字段生成getter,setter方法。

    /**
     * student
     *
     * @author zrj
     * @since 2022/11/28
     **/
    @ToString(exclude = {"phone"})
    public class Student {
        @Getter
        @Setter
        private String name;
    
        /**
         * 只生成set方法,且作用范围 修饰符PROTECTED
         */
        @Setter(AccessLevel.PROTECTED)
        private int age;
        /**
         * 只生成get方法,且作用范围 修饰符PUBLIC
         */
        @Getter(AccessLevel.PUBLIC)
        private String address;
    
        @Getter
        @Setter
        private String phone;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    3.@Cleanup这个注解用在变量前面,可以保证此变量代表的资源会被自动关闭,默认是调用资源的close()方法,如果该资源有其它关闭方法,可使用@Cleanup(“methodName”)来指定要调用的方法。

    public static void main(String[] args) throws IOException {
         @Cleanup 
         InputStream in = new FileInputStream(args[0]);
         @Cleanup 
         OutputStream out = new FileOutputStream(args[1]);
         byte[] b = new byte[1024];
         while (true) {
           int r = in.read(b);
           if (r == -1) break;
           out.write(b, 0, r);
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    4.@ToString 注解在类,添加toString方法。@ToString在JavaBean或类JavaBean中使用,使用此注解会自动重写对应的toStirng方法,默认情况下,会输出类名、所有属性(会按照属性定义顺序),用逗号来分割,通过callSuper参数来指定是否引用父类,includeFieldNames参数设为true,就能明确的输出toString()属性。

    @ToString(exclude=”column”)
    意义:排除column列所对应的元素,即在生成toString方法时不包含column参数;
    
    @ToString(exclude={“column1″,”column2″})
    意义:排除多个column列所对应的元素,其中间用英文状态下的逗号进行分割,即在生成toString方法时不包含多个column参数;
    
    @ToString(of=”column”)
    意义:只生成包含column列所对应的元素的参数的toString方法,即在生成toString方法时只包含column参数;;
    
    @ToString(of={“column1″,”column2”})
    意义:只生成包含多个column列所对应的元素的参数的toString方法,其中间用英文状态下的逗号进行分割,即在生成toString方法时只包含多个column参数;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    5.@EqualsAndHashCode 注解在类,生成hashCode和equals方法。@EqualsAndHashCode默认情况下,会使用所有非静态(non-static)和非瞬态(non-transient)属性来生成equals和hasCode,也能通过exclude注解来排除一些属性。

    @EqualsAndHashCode(exclude={"id", "shape"})
    public class EqualsAndHashCodeExample {
      private transient int transientVar = 10;
      private String name;
      private double score;
      private Shape shape = new Square(5, 10);
      private String[] tags;
      private int id;
      
      public String getName() {
        return this.name;
      }
      
      @EqualsAndHashCode(callSuper=true)
      public static class Square extends Shape {
        private final int width, height;
        
        public Square(int width, int height) {
          this.width = width;
          this.height = height;
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    6.@NoArgsConstructor、@RequiredArgsConstructor和@AllArgsConstructor
    这三个注解都是用在类上的,NoArgsConstructor 注解在类生成无参的构造方法。@RequiredArgsConstructor 注解在类,为类中需要特殊处理的字段生成构造方法,比如final和被@NonNull注解的字段。@AllArgsConstructor 注解在类,生成包含类中所有字段的构造方法。

    三个注解都可以指定生成的构造方法的访问权限,同时,第二个注解还可以用@RequiredArgsConstructor(staticName=”methodName”)的形式生成一个指定名称的静态方法,返回一个调用相应的构造方法产生的对象。

    @RequiredArgsConstructor(staticName = "myShape")
    @AllArgsConstructor(access = AccessLevel.PROTECTED)
    @NoArgsConstructor
    public class Shape {
        private int x;
        @NonNull
        private double y;
        @NonNull
        private String name;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    7.@Data 注解在类,生成setter/getter、equals、canEqual、hashCode、toString方法,如为final属性,则不会为该属性生成setter方法。
    特别注意:Lombok的@Data注解生成的EqualsAndHashCode默认不支持父类在使用Lombok过程中,如果对于各种注解的底层原理不理解的话,很容易产生意想不到的结果。举一个简单的例子:我们知道,当我们使用@Data定义一个类的时候,会自动帮我们生成equals()方法 。但是如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的话,会默认是@EqualsAndHashCode(callSuper=false),这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性,无论父类属性访问权限是否开放,这就可能得到意想不到的结果。以下是测试验证。
    User,UserCustomer,UserEmployee

    /**
     * User
     *
     * @author zrj
     * @since 2022/11/28
     **/
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class User {
        private String name;
    }
    @Data
    public class UserCustomer extends User {
        private String customerId;
    }
    @Data
    @EqualsAndHashCode(callSuper = true)
    public class UserEmployee extends User{
        private String empId;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    LombokDataTest

    /**
     * Lombok的@Data注解生成的EqualsAndHashCode默认不支持父类
     * 在使用Lombok过程中,如果对于各种注解的底层原理不理解的话,很容易产生意想不到的结果。
     * 举一个简单的例子:我们知道,当我们使用@Data定义一个类的时候,会自动帮我们生成equals()方法 。
     * 但是如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的话,会默认是@EqualsAndHashCode(callSuper=false),
     * 这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性,无论父类属性访问权限是否开放,
     * 这就可能得到意想不到的结果。
     *
     * @author zrj
     * @since 2022/11/28
     **/
    public class LombokDataTest {
        public static void main(String[] args) {
            //1.@Data默认@EqualsAndHashCode(callSuper = false)
            compareUserCustomerWithCallSuperFalse();
    
            System.out.println("--------------------------------");
    
            //2.@Data指定@EqualsAndHashCode(callSuper = true)
            compareUserEmployeeWithCallSuperFalse();
        }
    
        /**
         * 2.@Data指定@EqualsAndHashCode(callSuper = true)
         */
        private static void compareUserEmployeeWithCallSuperFalse() {
            UserEmployee userEmployee = new UserEmployee();
            userEmployee.setName("jerry");
            userEmployee.setEmpId("123");
    
            UserEmployee userEmployee2 = new UserEmployee();
            userEmployee2.setName("jerry");
            userEmployee2.setEmpId("123456");
    
            UserEmployee userEmployee3 = new UserEmployee();
            userEmployee3.setName("jerry2");
            userEmployee3.setEmpId("123");
    
            //false,父类中的Name相同,子类中的EmpId不同,可以校验出来
            System.out.println("【userEmployee】:" + userEmployee.toString());
            System.out.println("【userEmployee2】:" + userEmployee2.toString());
            System.out.println("【userEmployee3】:" + userEmployee3.toString());
    
            System.out.println("【userEmployee & userEmployee2】:" + userEmployee.equals(userEmployee2));
            //false,父类中的Name不同,子类中的EmpId相同,可以校验出来
            System.out.println("【userEmployee & userEmployee3】:" + userEmployee.equals(userEmployee3));
        }
    
        /**
         * 1.@Data默认@EqualsAndHashCode(callSuper = false)
         */
        private static void compareUserCustomerWithCallSuperFalse() {
            UserCustomer userCustomer = new UserCustomer();
            userCustomer.setName("jerry");
            userCustomer.setCustomerId("123");
    
            UserCustomer userCustomer2 = new UserCustomer();
            userCustomer2.setName("jerry");
            userCustomer2.setCustomerId("123456");
    
            UserCustomer userCustomer3 = new UserCustomer();
            userCustomer3.setName("jerry2");
            userCustomer3.setCustomerId("123");
    
            System.out.println("【userCustomer】:" + userCustomer.toString());
            System.out.println("【userCustomer2】:" + userCustomer2.toString());
            System.out.println("【userCustomer3】:" + userCustomer3.toString());
    
            //false,父类中的Name相同,子类中的customerId不同,可以校验出来
            System.out.println("【userCustomer & userCustomer2】:" + userCustomer.equals(userCustomer2));
            //true,父类中的Name不同,子类中的customerId相同,无法校验出来
            System.out.println("【userCustomer & userCustomer3】:" + userCustomer.equals(userCustomer3));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74

    8.@SneakyThrows这个注解用在方法上,可以将方法中的代码用try-catch语句包裹起来,捕获异常并在catch中用Lombok.sneakyThrow(e)把异常抛出,可以使用.

    public class SneakyThrows implements Runnable {
        @SneakyThrows(UnsupportedEncodingException.class)
        public String utf8ToString(byte[] bytes) {
            return new String(bytes, "UTF-8");
        }
     
        @SneakyThrows
        public void run() {
            throw new Throwable();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    9.@Synchronized这个注解用在类方法或者实例方法上,效果和synchronized关键字相同,区别在于锁对象不同,对于类方法和实例方法,synchronized关键字的锁对象分别是类的class对象和this对象,而@Synchronized得锁对象分别是私有静态final对象LOCK和私有final对象lock,当然,也可以自己指定锁对象

    10.@Slf4j 注解在类,生成log变量,严格意义来说是常量。private static final Logger log = LoggerFactory.getLogger(UserController.class);

    4. Lombok原理分析

    Lombok核心之处就是对于注解的解析上。JDK5引入了注解的同时,也提供了两种解析方式。

    运行时解析
    运行时能够解析的注解,必须将@Retention设置为RUNTIME,这样就可以通过反射拿到该注解。java.lang.reflect反射包中提供了一个接口AnnotatedElement,该接口定义了获取注解信息的几个方法,Class、Constructor、Field、Method、Package等都实现了该接口,对反射熟悉的朋友应该都会很熟悉这种解析方式。

    编译时解析
    编译时解析有两种机制,分别简单描述下:
    1)Annotation Processing Tool
    apt自JDK5产生,JDK7已标记为过期,不推荐使用,JDK8中已彻底删除,自JDK6开始,可以使用Pluggable Annotation Processing API来替换它,apt被替换主要有2点原因:
    api都在com.sun.mirror非标准包下
    没有集成到javac中,需要额外运行

    2)Pluggable Annotation Processing API
    JSR 269自JDK6加入,作为apt的替代方案,它解决了apt的两个问题,javac在执行的时候会调用实现了该API的程序,这样我们就可以对编译器做一些增强,javac执行的过程如下:
    在这里插入图片描述Lombok本质上就是一个实现了“JSR 269 API”的程序。在使用javac的过程中,它产生作用的具体流程如下:
    1.javac对源代码进行分析,生成了一棵抽象语法树(AST)
    2.运行过程中调用实现了“JSR 269 API”的Lombok程序
    3.此时Lombok就对第一步骤得到的AST进行处理,找到@Data注解所在类对应的语法树(AST),然后修改该语法树(AST),增加getter和setter方法定义的相应树节点
    4.javac使用修改后的抽象语法树(AST)生成字节码文件,即给class增加新的节点(代码块)
    通过读Lombok源码,发现对应注解的实现都在HandleXXX中,比如@Getter注解的实现在HandleGetter.handle()。还有一些其它类库使用这种方式实现,比如Google Auto、Dagger等等。

    5. 为什么不推荐使用Lombok

    Lombok的优点显而易见,可以帮助我们省去很多冗余代码,实际上,从我个人角度来看,Java开发项目中,并不推荐使用Lombok,下面我们来看一下为什么不推荐使用Lombok,它都有哪些缺点?

    1) 高侵入性,强迫队友
    Lombok插件的使用,要求开发者一定要在IDE中安装对应的插件。不仅自己要安装,任何和你协同开发的人都要安装。如果有谁未安装插件的话,使用IDE打开一个基于Lombok的项目的话会提示找不到方法等错误,导致项目编译失败。更重要的是,如果我们定义的一个jar包中使用了Lombok,那么就要求所有依赖这个jar包的所有应用都必须安装插件,这种侵入性是很高的。

    2)代码可调试性降低
    Lombok确实可以帮忙减少很多代码,因为Lombok会帮忙自动生成很多代码。但是,这些代码是要在编译阶段才会生成的,所以在开发的过程中,其实很多代码其实是缺失的。这就给代码调试带来一定的问题,我们想要知道某个类中的某个属性的getter方法都被哪些类引用的话,就没那么简单了。

    3) 影响版本升级
    Lombok对于代码有很强的侵入性,就可能带来一个比较大的问题,那就是会影响我们对JDK的升级。按照如今JDK的升级频率,每半年都会推出一个新的版本,但是Lombok作为一个第三方工具,并且是由开源团队维护的,那么他的迭代速度是无法保证的。所以,如果我们需要升级到某个新版本的JDK的时候,若其中的特性在Lombok中不支持的话就会受到影响。还有一个可能带来的问题,就是Lombok自身的升级也会受到限制。因为一个应用可能依赖了多个jar包,而每个jar包可能又要依赖不同版本的Lombok,这就导致在应用中需要做版本仲裁,而我们知道,jar包版本仲裁是没那么容易的,而且发生问题的概率也很高。

    4)注解使用有风险
    在使用Lombok过程中,如果对于各种注解的底层原理不理解的话,很容易产生意想不到的结果。举一个简单的例子:我们知道,当我们使用@Data定义一个类的时候,会自动帮我们生成equals()方法 。但是如果只使用了@Data,而不使用@EqualsAndHashCode(callSuper=true)的话,会默认是@EqualsAndHashCode(callSuper=false),这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性,无论父类属性访问权限是否开放,这就可能得到意想不到的结果。

    5)可能会破坏封装性
    使用过程中如果不小心,在一定程度上就会破坏代码的封装性。举个简单的例子,我们定义一个购物车类,并且使用了@Data注解:

    @Data
    public class ShoppingCart { 
        //商品数目
        private int itemsCount; 
        //总价格
        private double totalPrice; 
        //商品明细
        private List items = new ArrayList<>();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们知道,购物车中商品数目、商品明细以及总价格三者之前其实是有关联关系的,如果需要修改的话是要一起修改的。但是,我们使用了Lombok的@Data注解,对于itemsCount 和 totalPrice这两个属性,虽然我们将它们定义成 private 类型,但是提供了 public 的 getter、setter 方法。

    外部可以通过 setter 方法随意地修改这两个属性的值,我们可以随意调用 setter 方法,来重新设置 itemsCount、totalPrice 属性的值,这也会导致其跟 items 属性的值不一致。

    而面向对象封装的定义是:通过访问权限控制,隐藏内部数据,外部仅能通过类提供的有限的接口访问、修改内部数据。所以,暴露不应该暴露的 setter 方法,明显违反了面向对象的封装特性。

    好的做法应该是不提供getter/setter,而是只提供一个public的addItem方法,同时取修改itemsCount、totalPrice以及items三个属性。

    因此,在此种情况下,就不适合使用Lombok,或者只用@Getter不用@Setter,而别直接使用@Data,在使用过程中,需要多多小心。

    Lombok虽好,但缺点也不少,如果你在公司团队开发中被强X了,你就只能使用,如果新项目开发,能不用就尽量别用了,否则坑也不少的!

  • 相关阅读:
    Java多线程详解
    Java中常见锁的分类及概念分析
    HotSpot JVM 中的应用程序/动态类数据共享
    亚马逊云科技 云技能孵化营——我的云技能之旅
    loguru日志二次封装
    (Java)数据类型与变量
    thymeleaf-extras-shiro(根据当前用户的权限显示对应的标签) 与 shiro的加盐加密
    动态规划之四边形不等式
    基于蜜蜂算法的函数寻优及TSP搜索算法
    ChatGPT从⼊⻔到精通
  • 原文地址:https://blog.csdn.net/m0_37583655/article/details/128081759