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.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

注意事项:
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();
    }

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;
}

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);
     }
 }

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参数;

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;
    }
  }
}

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;
}

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;
}

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));
    }
}

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();
    }
}

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<>();
}

我们知道,购物车中商品数目、商品明细以及总价格三者之前其实是有关联关系的,如果需要修改的话是要一起修改的。但是,我们使用了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了,你就只能使用,如果新项目开发,能不用就尽量别用了,否则坑也不少的!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值