Java 开发插件 Lombok 的使用方法

442 阅读13分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

1. 简单介绍

官网地址

API 文档

Lombok 是一款 Java 开发插件,使得 Java 开发者可以通过其定义的一些注解来消除业务工程中冗长和繁琐的代码,尤其对于简单的 Java 模型对象(POJO)。在开发环境中使用 Lombok 插件后,Java 开发人员可以节省出重复构建,诸如 hashCode 和 equals 这样的方法以及各种业务对象模型的 accessor 和 toString 等方法的大量时间。对于这些方法,它能够在编译源代码期间自动帮我们生成这些方法,并不会像反射那样降低程序的性能。

2. 准备工作

2.1. 安装插件

我使用的开发工具是IntelliJ IDEA 2021.3版本,直接在File--->Settings--->Plugins里面搜索lombok,会发现该版本 IDEA 已经捆绑了 Lombok 插件,且已启用。

image.png

如果你的 IDEA 版本较低,可能需要手动安装下该插件。下面是IntelliJ IDEA 2019.2版本的插件安装方法,直接在File--->Settings--->Plugins里面搜索lombok,会看到下图所示的插件列表,Marketplace 显示的是市场上的相关插件列表,Installed 显示的是本地已安装的插件列表。

在这里插入图片描述

点击 Install 按钮安装,安装完毕该按钮会变为 Installed,上图为已安装完的状态。接下来会在 Installed 列表中看到已安装的 Lombok 插件。(安装完插件需重启 IDEA 才会生效)

在这里插入图片描述

2.2. 引入依赖

对于 Maven 项目,直接在 pom.xml 中引入 Lombok 的依赖。

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.22</version>
</dependency>

下面测试注解使用过程中,我们每次修改完毕代码后,需要对当前 Java 文件进行编译,如果没有设置自动编译,则直接使用快捷键Ctrl+Shift+F9进行手动编译。

重新编译完毕,找到对应的 class 文件,可能是没有更新的,这时可以直接在对应 class 文件上或者对应包上点击鼠标右键,选择Synchronize 'xxxxx'进行同步即可更新为最新的 class 文件。

IDEA 默认集成了反编译插件,我们可以很方便的查看我们 class 文件的反编译结果。使用的时候只需要找到你的 class 文件(Maven 项目直接在 target 下找对应目录的 class 文件),直接双击打开即可。

当使用 2.6.2 版本的spring-boot-starter-parent时,发现 Lombok 的版本已经在spring-boot-dependencies中被默认指定了。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.2</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-dependencies</artifactId>
  <version>2.6.2</version>
</parent>
<lombok.version>1.18.22</lombok.version>
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>${lombok.version}</version>
</dependency>

3. 注解使用

3.1. @Getter@Setter

使用方法

@Getter@Setter注解为成员变量生成gettersetter方法。可分开使用也可一起使用,下面以一起使用进行讲解。

(1)注解在类或成员变量上。

注解在类上时为所有成员变量生成 getter 和 setter 方法。

注解在成员变量上时只为该字段生成 getter 和 setter 方法。

(2)不会对 final 修饰的成员变量生成 setter 方法,但是会对该成员变量生成 getter 方法。

(3)对于 boolean 类型的成员变量,生成的 getter 方法遵循布尔属性的约定,例如对于成员变量 boolean sex 生成的 getter 方法为 isSex,而不是 getSex。

(4)如果使用该注解的成员变量所在的类包含与要生成的 getter 或 setter 名称相同的方法且形式参数列表相同,则不会生成相应的方法。

(5)这两个注解生成的 getter 和 setter 方法默认是 public 修饰的。源码:

lombok.AccessLevel value() default lombok.AccessLevel.PUBLIC;

通过使用可选参数 AccessLevel 可以指定生成的方法的访问级别。一共有六种级别:

AccessLevel描述
PUBLIC生成 public 修饰的 getter 或 setter 方法。
MODULE生成没有修饰符修饰的 getter 或 setter 方法。
PROTECTED生成 protected 修饰的 getter 或 setter 方法。
PACKAGE生成没有修饰符修饰的 getter 或 setter 方法。
PRIVATE生成 private 修饰的 getter 或 setter 方法。
NONE不生成 getter 或 setter 方法。

示例代码

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

public class UserInfo {
    //测试正常用法
    @Getter@Setter
    private Long userId;

    //测试 final 修饰成员变量
    @Getter @Setter
    private final String constant = "常量";

    //测试布尔属性成员变量	
    @Getter@Setter
    private boolean sex1;
    @Getter@Setter
    private Boolean sex2;

    //测试生成方法级别
    @Setter(value = AccessLevel.PUBLIC)
    private String ac1;
    @Setter(value = AccessLevel.MODULE)
    private String ac2;
    @Setter(value = AccessLevel.PROTECTED)
    private String ac3;
    @Setter(value = AccessLevel.PACKAGE)
    private String ac4;
    @Setter(value = AccessLevel.PRIVATE)
    private String ac5;
    @Setter(value = AccessLevel.NONE)
    private String ac6;

    //测试同名方法存在的情况
    @Getter@Setter
    private String same1;
    @Getter@Setter
    private String same2;
    @Getter@Setter
    private String same3;

    //返回类型和形参都相同
    public String getSame1() {
        return "same test1";
    }
    public void setSame1(String same1) {}
    //返回类型相同,形参不同
    public String getSame2(String param) {
        return "same test2";
    }
    public void setSame2() {}
    //返回类型不同,形参相同
    public void getSame3() {}
    public String setSame3(String same3) {
        return "same test3";
    }
}

生成代码

public class UserInfo {
    private Long userId;
    private final String constant = "常量";
    private boolean sex1;
    private Boolean sex2;
    private String ac1;
    private String ac2;
    private String ac3;
    private String ac4;
    private String ac5;
    private String ac6;
    private String same1;
    private String same2;
    private String same3;

    public UserInfo() {}
    
	//提供的三种类型的同名方法
    public String getSame1() {
        return "same test1";
    }
    public void setSame1(String same1) {}
    public String getSame2(String param) {
        return "same test2";
    }
    public void setSame2() {}
    public void getSame3() {}
    public String setSame3(String same3) {
        return "same test3";
    }

	//正常使用生成的 getter、setter 方法
    public Long getUserId() {
        return this.userId;
    }
    public void setUserId(final Long userId) {
        this.userId = userId;
    }

	//final 修饰的成员变量只生成了 getter 方法,没生成 setter 方法
    public String getConstant() {
        this.getClass();
        return "常量";
    }

	//布尔属性 boolean 修饰的成员变量生成的 getter 方法为 is 开头的
	//生成的 setter 方法和其他一样
    public boolean isSex1() {
        return this.sex1;
    }
    public void setSex1(final boolean sex1) {
        this.sex1 = sex1;
    }
    //布尔包装属性 Boolean 修饰的成员变量生成的 getter、setter 方法和其他一样
    public Boolean getSex2() {
        return this.sex2;
    }
    public void setSex2(final Boolean sex2) {
        this.sex2 = sex2;
    }

	//测试生成方法级别
    public void setAc1(final String ac1) {
        this.ac1 = ac1;
    }
    void setAc2(final String ac2) {
        this.ac2 = ac2;
    }
    protected void setAc3(final String ac3) {
        this.ac3 = ac3;
    }
    void setAc4(final String ac4) {
        this.ac4 = ac4;
    }
    private void setAc5(final String ac5) {
        this.ac5 = ac5;
    }

	//存在返回类型相同的同名方法会生成新的 getter、setter 方法
	//只有存在形式参数列表相同的同名方法,才不会生成新的 getter、setter 方法
    public String getSame2() {
        return this.same2;
    }
    public void setSame2(final String same2) {
        this.same2 = same2;
    }
}

3.2. @ToString

使用方法

类上使用该注解会自动生成toString方法。默认情况下,任何非静态字段都将以名称-值对的形式包含在 toString 方法的输出中。该注解有几个可选属性,可相应控制 toString 的输出内容。

属性描述
includeFieldNames该属性设置为 false 表示输出没有属性名和等号,只有属性值,多个属性值用逗号隔开。
exclude该属性中列出的字段都不会在出现在生成的 toString 方法中,与 of 属性互斥。
of该属性中列出的字段是要打印的字段,与 exclude 属性互斥。
callSuper该属性设置为 true,表示输出中会包含父类的 toString 方法的输出结果,默认为 false。
doNotUseGetters通常都是通过字段的 getter 方法获取字段值,如果没有 getter 方法,才在通过直接访问字段来获取值。该属性设置为 true,表示输出的字段值不通过 getter 方法获取,而是直接访问字段,默认为 false。
onlyExplicitlyIncluded该属性设置为 true,不输出任何字段信息,只输出了构造方法的名字,默认为 false。

示例代码

public class Info {
    private String remark;
}
import lombok.ToString;

@ToString(callSuper = true, of = {"userName"})
public class UserInfo extends Info{
    private Long userId;
    private String userName;
}

生成代码

public class UserInfo extends Info {
    private Long userId;
    private String userName;

    public UserInfo() {
    }

	//生成的方法只包含 userName 字段,还包含了父类的 toSring 方法的输出结果
    public String toString() {
        return "UserInfo(super=" + super.toString() + ", userName=" + this.userName + ")";
    }
}

3.3. @EqualsAndHashCode

使用方法

该注解用在类上会同时生成equalshashCode方法,因为这两个方法本质上是由hashCode契约绑定在一起的。默认情况下,这两种方法都会考虑类中不是静态或瞬态的任何字段。该注解也有几个可选属性,可相应控制参与 hashCode 计算的字段。

属性描述
exclude在生成的 equals 和 hashCode 方法中不会考虑这里列出的所有字段,与 of 属性互斥。
of在生成的 equals 和 hashCode 方法中只会考虑该属性中列出的字段,与 exclude 属性互斥。
callSuper该属性设置为 true,表示父类的 equals 和 hashCode 方法的计算结果会在生成的 equals 和 hashCode 方法中参与计算,默认为 false。
doNotUseGetters通常都是通过字段的 getter 方法获取字段值,如果没有 getter 方法,才会通过直接访问字段来获取值。该属性设置为 true,表示参与计算的字段值不通过 getter 方法获取,而是直接访问字段,默认为 false。

这里引用一段话,对equals()hashCode()方法的解释:

hashCode() 方法和 equals() 方法的作用其实一样,在 Java 里都是用来对比两个对象是否相等一致。重写的 equals() 里一般比较的全面且复杂,这样效率就比较低,而利用 hashCode() 进行对比,则只要生成一个 hash 值进行比较就可以了,效率很高。但是,hashCode() 并不是完全可靠的,有时候不同的对象它们生成的 hash 值也会一样(生成 hash 值的公式可能存在的问题),所以 hashCode() 只能说是大多数时候可靠,并不是绝对可靠,所以我们可以得出:

(1)equal() 相等的两个对象它们的 hashCode() 肯定相等,也就是用 equals() 对比是绝对可靠的。

(2)hashCode() 相等的两个对象它们的 equals() 不一定相等,也就是 hashCode() 不是绝对可靠的。

因此,我们可以得出以下结论:

所有对于需要大量并且快速的对比的情况如果都用 equals() 去做显然效率太低,所以解决方式是,每当需要对比的时候,首先用 hashCode() 去对比,如果 hashCode() 不一样,则表示这两个对象肯定不相等(也就不必再调用 equals() 去对比了),如果 hashCode() 相同,这时再对比它们的 equals(),如果 equals() 也相同,则表示这两个对象是真的相同了,这样既能大大提高效率也保证了对比的绝对正确性!

示例代码

public class Info {
    private String remark;
}
//提供了每个字段的 getter 方法
@Getter
//父类的 equals 和 hashCode 方法结果参与计算
//只有 "userName"、"userAge" 参与计算
//不通过 getter 方法获取字段值
@EqualsAndHashCode(callSuper = true, of = {"userName", "userAge"}, doNotUseGetters = true)
public class UserInfo extends Info{
    private Long userId;
    private String userName;
    private Integer userAge;
}

生成代码

public class UserInfo extends Info {
    private Long userId;
    private String userName;
    private Integer userAge;

    public UserInfo() {
    }

    public Long getUserId() {
        return this.userId;
    }
    public String getUserName() {
        return this.userName;
    }
    public Integer getUserAge() {
        return this.userAge;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof UserInfo)) {
            return false;
        } else {
            UserInfo other = (UserInfo)o;
            if (!other.canEqual(this)) {
                return false;
                //调用父类的 eauqls 方法
            } else if (!super.equals(o)) {
                return false;
            } else {
            	//通过直接访问字段来获取字段值
                Object this$userName = this.userName;
                Object other$userName = other.userName;
                if (this$userName == null) {
                    if (other$userName != null) {
                        return false;
                    }
                } else if (!this$userName.equals(other$userName)) {
                    return false;
                }
				//通过直接访问字段来获取字段值
                Object this$userAge = this.userAge;
                Object other$userAge = other.userAge;
                if (this$userAge == null) {
                    if (other$userAge != null) {
                        return false;
                    }
                } else if (!this$userAge.equals(other$userAge)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(Object other) {
        return other instanceof UserInfo;
    }

    public int hashCode() {
        int PRIME = true;
        //调用父类的 hashCode 方法
        int result = super.hashCode();
        //通过直接访问字段来获取字段值
        Object $userName = this.userName;
        result = result * 59 + ($userName == null ? 43 : $userName.hashCode());
        Object $userAge = this.userAge;
        result = result * 59 + ($userAge == null ? 43 : $userAge.hashCode());
        return result;
    }
}

3.4. @NoArgsConstructor

使用方法

该注解使用在类上,生成无参数的构造方法。

(1)使用 @NoArgsConstructor 会生成没有参数的构造函数,但如果存在 final 修饰的成员变量字段,会编译出错,除非使用 @NoArgsConstructor(force=true),那么所有的 final 字段会根据其类型被初始化为 0,false,null 等值。

一个普通类中默认会存在一个无参构造函数。 如果一个类中有无参构造方法,则该类中 final 修饰的成员变量必须被初始化。 否则必须存在一个含参构造方法,参数为 final 修饰的成员变量,并且不能有无参构造方法。

(2)使用无参数的构造函数构造出来的实例的成员变量值是 null,如果存在 @NonNull 修饰的成员字段,那么就矛盾了。所以如果有 @NonNull 修饰的成员变量就不要用 @NoArgsConstructor 修饰类了。(但是你这么做的话也不报错)。

属性描述
staticName(1)如果设置该属性,则生成的构造函数将会变为私有的,另外还会生成一个静态“构造函数”。该静态函数内部会调用私有的构造函数。
(2)如果该属性没有被指定值,则不起作用。
access(1)设置生成的构造函数的访问级别。默认情况下,生成的构造函数是 public。
(2)如果同时使用了 staticName 属性,则构造函数私有,所以 access 属性控制的是 staticName 属性生成的静态函数的访问级别。
force如果设置为 true,会将所有 final 字段初始化为 0 / null / false。否则,编译时出现错误。

示例代码

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(staticName = "staticMethod", access = AccessLevel.PRIVATE, force = true)
public class UserInfo{
    private final Long userId;
    private String userName;
    private Integer userAge;
}

生成代码

public class UserInfo{
	//final字段被强制初始化为null了
    private final Long userId = null;
    private String userName;
    private Integer userAge;
	//生成的无参构造方法被 staticName 属性设置为了私有的
    private UserInfo() {
    }
	//staticName 属性生成的静态方法,该方法被 access 属性指定为了私有的。
    private static UserInfo staticMethod() {
        return new UserInfo();
    }
}

3.5. @RequiredArgsConstructor

使用方法

注解使用在类上,生成具有必需参数的构造函数。必需参数包括 final 修饰的字段和具有 @NonNull 注解的字段。

属性描述
staticName(1)如果设置该属性,则生成的构造函数将会变为私有的,另外还会生成一个静态“构造函数”。该静态函数内部会调用私有的构造函数。
(2)如果该属性没有被指定值,则不起作用。
access(1)设置生成的构造函数的访问级别。默认情况下,生成的构造函数是 public。
(2)如果同时使用了 staticName 属性,则构造函数私有,所以 access 属性控制的是 staticName 属性生成的静态函数的访问级别。
onConstructor = @__(@Autowired)自动生成包含所有 final 修饰或和具有 @NonNull 注解的字段的构造函数并在构造函数上添加 @Autowired 注解。

示例代码

import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor(staticName = "staticMethod", access = AccessLevel.PRIVATE)
public class UserInfo{
    private final Long userId;
    @NonNull
    private String userName;
    private Integer userAge;
}

生成代码

import lombok.NonNull;

public class UserInfo {
    private final Long userId;
    @NonNull
    private String userName;
    private Integer userAge;
	//final 字段和 @NonNull 字段作为了构造函数的参数
	//生成的含参构造方法被 staticName 属性设置为了私有的
    private UserInfo(Long userId, @NonNull String userName) {
    	//这里还对 @NonNull 字段进行了 null 值检查
        if (userName == null) {
            throw new NullPointerException("userName is marked non-null but is null");
        } else {
            this.userId = userId;
            this.userName = userName;
        }
    }
	//staticName 属性生成的静态方法,该方法被 access 属性指定为了私有的
    private static UserInfo staticMethod(Long userId, @NonNull String userName) {
        return new UserInfo(userId, userName);
    }
}

@RequiredArgsConstructor(onConstructor = @__(@Autowired))

在 Controller 中注入 Service 的时候,IDEA 会报警告:不建议使用字段注入,请使用构造方法注入代替。要想解决这个报黄,有以下几种方法:

方法①:可以使用 @Resource 替代 @Autowired。

方法②:使用构造器注入,手写构造方法。

方法③:在类上添加 @RequiredArgsConstructor 或者 @RequiredArgsConstructor(onConstructor = @__(@Autowired))

其实就是生成构造方法,并在生成的构造方法上添加 @Autowired 注解;查看编译后生成的 class 文件可以发现自动生成了构造方法并添加了 @Autowired 注解。需要注意的是,字段需要加上 final 关键字或者 @NonNull 注解。

实际使用中,加上 final 关键字或者 @NonNull 注解的字段,直接使用 @RequiredArgsConstructor 注解就能生成对应的构造函数,@RequiredArgsConstructor(onConstructor = @__(@Autowired)) 这种用法好像就多了个在生成的构造方法上添加 @Autowired 注解而已。

示例代码

@Controller
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TestController{
    private final TestService testService;
}

生成代码

@Controller
public class TestController{
    private final TestService testService;
    
    @Autowired
    public TestController(final TestService testService){
        this.testService = testService;
    }
}

示例代码

@Controller
@RequiredArgsConstructor
public class TestController{
    private final TestService testService;
}

生成代码

@Controller
public class TestController{
    private final TestService testService;
    
    public TestController(final TestService testService){
        this.testService = testService;
    }
}

3.6. @AllArgsConstructor

使用方法

注解使用在类上,生成参数包含类中所有字段的构造方法。

属性描述
staticName(1)如果设置该属性,则生成的构造函数将会变为私有的,另外还会生成一个静态“构造函数”。该静态函数内部会调用私有的构造函数。
(2)如果该属性没有被指定值,则不起作用。
access(1)设置生成的构造函数的访问级别。默认情况下,生成的构造函数是 public。
(2)如果同时使用了 staticName 属性,则构造函数私有,所以 access 属性控制的是 staticName 属性生成的静态函数的访问级别。

这三个处理构造函数的注解,相同之处:

(1)都只能修饰类。

(2)都能通过staticName属性创建静态工厂方法。

(3)都能使用access属性控制生成的构造函数的访问级别。

(4)如果用于枚举上,则生成的构造方法都为私有的。

不同之处在于:

(1)@NoArgsConstructor所有的成员变量都不会纳入到构造函数。

(2)@RequiredArgsConstructor只会把final@NonNull修饰的成员变量纳入。

(3)@AllArgsConstructor会把所有的成员变量都纳入到构造函数中。

示例代码

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.NonNull;

@AllArgsConstructor(staticName = "staticMethod", access = AccessLevel.PRIVATE)
public class UserInfo{
    private final Long userId;
    @NonNull
    private String userName;
    private Integer userAge;
}

生成代码

import lombok.NonNull;

public class UserInfo {
    private final Long userId;
    @NonNull
    private String userName;
    private Integer userAge;
	//生成的全参构造方法被 staticName 属性设置为了私有的
    private UserInfo(Long userId, @NonNull String userName, Integer userAge) {
    	//这里还对 @NonNull 字段进行了 null 值检查
        if (userName == null) {
            throw new NullPointerException("userName is marked non-null but is null");
        } else {
            this.userId = userId;
            this.userName = userName;
            this.userAge = userAge;
        }
    }
	//staticName 属性生成的静态方法,该方法被 access 属性指定为了私有的
    private static UserInfo staticMethod(Long userId, @NonNull String userName, Integer userAge) {
        return new UserInfo(userId, userName, userAge);
    }
}

3.7. @Data

使用方法

该注解使用在类上。应该是 Lombok 项目中使用最频繁的注解了。它综合了@RequiredArgsConstructor,@ToString,@EqualsAndHashCode,@Getter 和 @Setter 这五个注解的功能。

(1)虽然 @Data 非常有用,但它不能提供与其他 Lombok 注解相同的控制粒度。为了覆盖默认的方法生成行为,请使用其他 Lombok 注解对类、字段或方法进行注释,并指定必要的参数值,以达到预期的效果。

(3)提供了一个参数选项 staticConstructor,可以用来生成静态工厂方法。该属性会将构造函数设置为私有,并生成一个公开的的静态工厂方法,方法名称就是该属性值指定的方法名。

示例代码

import lombok.Data;
import lombok.NonNull;
@Data(staticConstructor = "getInstance")
public class UserInfo{
    private final Long userId;
    @NonNull
    private String userName;
    private Integer userAge;
}

生成代码

import lombok.NonNull;

public class UserInfo {
    private final Long userId;
    @NonNull
    private String userName;
    private Integer userAge;
	
	//生成的构造方法被 staticConstructor 属性设置为私有的了
    private UserInfo(final Long userId, @NonNull final String userName) {
        if (userName == null) {
            throw new NullPointerException("userName is marked non-null but is null");
        } else {
            this.userId = userId;
            this.userName = userName;
        }
    }

	//staticConstructor 属性生成的静态工厂方法,方法名是该属性指定的值
    public static UserInfo getInstance(final Long userId, @NonNull final String userName) {
        return new UserInfo(userId, userName);
    }

    public Long getUserId() {
        return this.userId;
    }

    @NonNull
    public String getUserName() {
        return this.userName;
    }

    public Integer getUserAge() {
        return this.userAge;
    }

    public void setUserName(@NonNull final String userName) {
        if (userName == null) {
            throw new NullPointerException("userName is marked non-null but is null");
        } else {
            this.userName = userName;
        }
    }

    public void setUserAge(final Integer userAge) {
        this.userAge = userAge;
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof UserInfo)) {
            return false;
        } else {
            UserInfo other = (UserInfo)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                label47: {
                    Object this$userId = this.getUserId();
                    Object other$userId = other.getUserId();
                    if (this$userId == null) {
                        if (other$userId == null) {
                            break label47;
                        }
                    } else if (this$userId.equals(other$userId)) {
                        break label47;
                    }

                    return false;
                }

                Object this$userName = this.getUserName();
                Object other$userName = other.getUserName();
                if (this$userName == null) {
                    if (other$userName != null) {
                        return false;
                    }
                } else if (!this$userName.equals(other$userName)) {
                    return false;
                }

                Object this$userAge = this.getUserAge();
                Object other$userAge = other.getUserAge();
                if (this$userAge == null) {
                    if (other$userAge != null) {
                        return false;
                    }
                } else if (!this$userAge.equals(other$userAge)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof UserInfo;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $userId = this.getUserId();
        int result = result * 59 + ($userId == null ? 43 : $userId.hashCode());
        Object $userName = this.getUserName();
        result = result * 59 + ($userName == null ? 43 : $userName.hashCode());
        Object $userAge = this.getUserAge();
        result = result * 59 + ($userAge == null ? 43 : $userAge.hashCode());
        return result;
    }

    public String toString() {
        return "UserInfo(userId=" + this.getUserId() + ", userName=" + this.getUserName() + ", userAge=" + this.getUserAge() + ")";
    }
}

3.8. @NonNull

使用方法

@NonNull注解用于成员变量上,表示对该成员变量进行null检查。

(1)当放置在使用 @Setter 注解的成员变量上时,将在生成的 setter 方法中进行 null 检查。

(2)如果使用 @RequiredArgsConstructor 注解为所属类生成构造函数,则使用了该注解的成员变量将被添加到生成的构造函数的形式参数列表中,并且在生成的构造函数中对该参数进行 null 检查。

示例代码

import lombok.*;

@RequiredArgsConstructor
public class UserInfo {
    @Setter@NonNull
    private Long userId;
}

生成代码

import lombok.NonNull;

public class UserInfo {
    @NonNull
    private Long userId;

    public UserInfo(@NonNull final Long userId) {
    	//构造方法中生成的null检查
        if (userId == null) {
            throw new NullPointerException("userId is marked non-null but is null");
        } else {
            this.userId = userId;
        }
    }

    public void setUserId(@NonNull final Long userId) {
    	//setter方法中生成的null检查
        if (userId == null) {
            throw new NullPointerException("userId is marked non-null but is null");
        } else {
            this.userId = userId;
        }
    }
}

3.9. @Builder

使用方法

该注解可以使用在类上、构造方法、普通方法上。

(1)如果使用在类上,则会生成一个包括该类所有参数的私有的构造方法(类似于在类上使用@AllArgsConstructor(access = AccessLevel.PRIVATE) )。注意,只有在没有编写任何构造函数也没有使用任意@XArgsConstructor注解的情况下才会生成此构造函数。

(2)该注解的作用是生成一个名为 TBuilder 的内部类和一个私有的构造函数,以及一个静态工厂方法 builder()。

(3)通过 Builder 构造的方式,比直接使用构造函数的方式更加具备可读性,比频繁使用 set 方法的方式更加简洁。

属性描述
builderMethodName该属性用于指定生成的静态工厂方法的名称。(1)不使用该属性的情况下,生成的静态工厂方法默认名称为 builder。(2)使用该属性,如果值为空串,则不会生成静态工厂方法
buildMethodName该属性用于指定静态内部类中用于获取外部使用@Builder注解的类实例的方法名称。(1)不使用该属性的情况下,生成的方法默认名称为 build。(2)使用该属性,如果值为空串,编译会报错。
builderClassName该属性用于指定生成的静态内部类的名称。如果值为空串,则相当于未使用该属性
toBuilder默认为 false。如果为 true,则生成一个实例方法,以获取使用该实例的值初始化的生成器。仅当 @Builder 用于构造函数、类型本身或用于返回的静态方法时才是合法的声明类型的实例。(使用方法见下方 toBuilder 属性使用示例
access设置生成的生成器类(静态内部类)的访问级别。默认情况下,生成的构建器类是 public 的。注意:如果是自己写的 builder 类,则不会改变它的访问级别。

示例代码

import lombok.Builder;

@Builder
public class UserInfo{
    private String userName;
    private Integer userAge;
}
UserInfo userInfo = UserInfo.builder().userName("zhangsan").userAge(14).build();
System.out.println(userInfo);
String str = UserInfo.builder().userName("zhangsan").userAge(14).toString();
System.out.println(str);

执行结果

cn.udesk.kun.UserInfo@3d04a311
UserInfo.UserInfoBuilder(userName=zhangsan, userAge=14)

生成代码

public class UserInfo {
    private String userName;
    private Integer userAge;
	
	//生成的全参数构造方法
    UserInfo(final String userName, final Integer userAge) {
        this.userName = userName;
        this.userAge = userAge;
    }

	//生成的静态工厂方法,用于构造内部类实例
    public static UserInfo.UserInfoBuilder builder() {
        return new UserInfo.UserInfoBuilder();
    }
	//生成的静态内部类
    public static class UserInfoBuilder {
        private String userName;
        private Integer userAge;

        UserInfoBuilder() {
        }

        public UserInfo.UserInfoBuilder userName(final String userName) {
            this.userName = userName;
            return this;
        }

        public UserInfo.UserInfoBuilder userAge(final Integer userAge) {
            this.userAge = userAge;
            return this;
        }
		//该方法用于返回外部类的完整实例
        public UserInfo build() {
            return new UserInfo(this.userName, this.userAge);
        }

        public String toString() {
            return "UserInfo.UserInfoBuilder(userName=" + this.userName + ", userAge=" + this.userAge + ")";
        }
    }
}

toBuilder 属性使用示例

import lombok.Builder;
import lombok.ToString;

@ToString
@Builder(toBuilder = true)
public class UserInfo{
    private String userName;
    private Integer userAge;
}
UserInfo userInfo = UserInfo.builder().userName("zhangsan").userAge(14).build();
System.out.println(userInfo);
//修改默写属性,并生成新对象
UserInfo userInfo1 = userInfo.toBuilder().userAge(34).build();
System.out.println(userInfo1);
UserInfo(userName=zhangsan, userAge=14)
UserInfo(userName=zhangsan, userAge=34)
public class UserInfo {
    private String userName;
    private Integer userAge;

    UserInfo(final String userName, final Integer userAge) {
        this.userName = userName;
        this.userAge = userAge;
    }

    public static UserInfo.UserInfoBuilder builder() {
        return new UserInfo.UserInfoBuilder();
    }
	//toBuilder 属性生成的方法,用来生成新的实例
    public UserInfo.UserInfoBuilder toBuilder() {
        return (new UserInfo.UserInfoBuilder()).userName(this.userName).userAge(this.userAge);
    }
	//ToString 注解生成的方法
    public String toString() {
        return "UserInfo(userName=" + this.userName + ", userAge=" + this.userAge + ")";
    }

    public static class UserInfoBuilder {
        private String userName;
        private Integer userAge;

        UserInfoBuilder() {
        }

        public UserInfo.UserInfoBuilder userName(final String userName) {
            this.userName = userName;
            return this;
        }

        public UserInfo.UserInfoBuilder userAge(final Integer userAge) {
            this.userAge = userAge;
            return this;
        }

        public UserInfo build() {
            return new UserInfo(this.userName, this.userAge);
        }

        public String toString() {
            return "UserInfo.UserInfoBuilder(userName=" + this.userName + ", userAge=" + this.userAge + ")";
        }
    }
}

3.10. @SuperBuilder

使用方法

(1)该注解类似于 @Builder 注解,只在类上使用,主要用于有继承结构的情况下构建父类的参数。并且层次结构中的所有类都必须使用 @SuperBuilder 进行注释。

(2)使用 @Builder 或 @SuperBuilder 注解时,不会默认创建无参构造函数,如果有额外使用无参构造函数的需求,需要在子类和父类都加上 @NoArgsConstructor 注解。

属性描述
builderMethodName该属性用于指定生成的静态工厂方法的名称。(1)不使用该属性的情况下,生成的静态工厂方法默认名称为 builder。(2)使用该属性,如果值为空串,则不会生成静态工厂方法
buildMethodName该属性用于指定静态内部类中用于获取外部使用@Builder注解的类实例的方法名称。(1)不使用该属性的情况下,生成的方法默认名称为 build。(2)使用该属性,如果值为空串,编译会报错。
toBuilder(1)默认为 false。如果为 true,则生成一个实例方法,以获取使用该实例的值初始化的生成器。(2)toBuilder 属性默认关闭,如果开启,则所有的父类应该也要开启(使用方法见下方 toBuilder 属性使用示例

示例代码

import lombok.experimental.SuperBuilder;

@SuperBuilder
public class Info {
    private String remark;
}
import lombok.experimental.SuperBuilder;

@SuperBuilder
public class UserInfo extends Info{
    private String userName;
    private Integer userAge;
}

生成代码

import cn.udesk.kun.Info.1;

public class Info {
    private String remark;

    protected Info(final cn.udesk.kun.Info.InfoBuilder<?, ?> b) {
        this.remark = cn.udesk.kun.Info.InfoBuilder.access$000(b);
    }

    public static cn.udesk.kun.Info.InfoBuilder<?, ?> builder() {
        return new cn.udesk.kun.Info.InfoBuilderImpl((1)null);
    }
}
import cn.udesk.kun.UserInfo.1;

public class UserInfo extends Info {
    private String userName;
    private Integer userAge;

    protected UserInfo(final UserInfo.UserInfoBuilder<?, ?> b) {
        super(b);
        this.userName = UserInfo.UserInfoBuilder.access$000(b);
        this.userAge = UserInfo.UserInfoBuilder.access$100(b);
    }

    public static UserInfo.UserInfoBuilder<?, ?> builder() {
        return new cn.udesk.kun.UserInfo.UserInfoBuilderImpl((1)null);
    }

    public static class UserInfoBuilder {
        private String userName;
        private Integer userAge;

        UserInfoBuilder() {
        }

        public UserInfo.UserInfoBuilder userName(final String userName) {
            this.userName = userName;
            return this;
        }

        public UserInfo.UserInfoBuilder userAge(final Integer userAge) {
            this.userAge = userAge;
            return this;
        }

        public UserInfo build() {
            return new UserInfo(this.userName, this.userAge);
        }

        public String toString() {
            return "UserInfo.UserInfoBuilder(userName=" + this.userName + ", userAge=" + this.userAge + ")";
        }
    }
}

toBuilder 属性使用示例

@ToString
@SuperBuilder(toBuilder = true)
public class Info {
    private String remark;
}
@ToString(callSuper = true)
@SuperBuilder(toBuilder = true)
public class UserInfo extends Info{
    private String userName;
    private Integer userAge;
}
UserInfo userInfo = UserInfo.builder().userName("zhangsan").userAge(14).remark("老对象").build();
System.out.println(userInfo);
//修改默写属性,并生成新对象
UserInfo userInfo1 = userInfo.toBuilder().userAge(34).remark("新对象").build();
System.out.println(userInfo1);
UserInfo(super=Info(remark=老对象), userName=zhangsan, userAge=14)
UserInfo(super=Info(remark=新对象), userName=zhangsan, userAge=34)

3.11. @Log

使用方法

将 @Log 的变体(任何一个适用于你使用的日志系统的注解)放在你的类上,然后会生成一个static final log字段,按照你使用的日志框架通常规定的方式进行初始化,然后你就可以使用它来编写日志语句。有以下几种可供选择:

@CommonsLog

private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog(LogExample.class);

@Flogger

v1.16.24 添加,使用谷歌的 FluentLogger 框架时使用。

private static final com.google.common.flogger.FluentLogger log = com.google.common.flogger.FluentLogger.forEnclosingClass();

@JBossLog

private static final org.jboss.logging.Logger log = org.jboss.logging.Logger.getLogger(LogExample.class);

@Log

private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogExample.class.getName());

@Log4j

使用 Log4j 框架时使用

private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(LogExample.class);

@Log4j2

使用 Log4j2 框架时使用。

private static final org.apache.logging.log4j.Logger log = org.apache.logging.log4j.LogManager.getLogger(LogExample.class);

@Slf4j

使用 Logback 框架时使用。

private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExample.class);

@XSlf4j

private static final org.slf4j.ext.XLogger log = org.slf4j.ext.XLoggerFactory.getXLogger(LogExample.class);

@CustomLog

v1.18.10 添加,自定义日志。@CustomLog 的主要目的是支持内部的、私有的日志框架。

private static final com.foo.your.Logger log = com.foo.your.LoggerFactory.createYourLogger(LogExample.class);

示例代码

import lombok.extern.java.Log;
import lombok.extern.slf4j.Slf4j;

@Log
public class LogExample {
  
  public static void main(String... args) {
    log.severe("Something's wrong here");
  }
}

@Slf4j
public class LogExampleOther {
  
  public static void main(String... args) {
    log.error("Something else is wrong here");
  }
}

@CommonsLog(topic="CounterLog")
public class LogExampleCategory {

  public static void main(String... args) {
    log.error("Calling the 'CounterLog' with a message");
  }
}

生成代码

public class LogExample {
  private static final java.util.logging.Logger log = java.util.logging.Logger.getLogger(LogExample.class.getName());
  
  public static void main(String... args) {
    log.severe("Something's wrong here");
  }
}

public class LogExampleOther {
  private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogExampleOther.class);
  
  public static void main(String... args) {
    log.error("Something else is wrong here");
  }
}

public class LogExampleCategory {
  private static final org.apache.commons.logging.Log log = org.apache.commons.logging.LogFactory.getLog("CounterLog");

  public static void main(String... args) {
    log.error("Calling the 'CounterLog' with a message");
  }
}

3.12. @Cleanup

使用方法

@Cleanup 注释可用于确保已分配的资源被释放。当用 @Cleanup 注释局部变量时,任何后续代码都封装在try/finally块中,该块保证在当前范围的末尾处调用清理方法。默认情况下 @Cleanup 生成的清理方法名为 close,与输入和输出流一样。可以通过 value 参数提供一个不同的方法名。注意方法名必须是该变量可以使用的方法。不然会报错。在使用 @Cleanup 注释时还需要注意一点。如果清理方法抛出异常,它将抢占方法主体中抛出的任何异常(覆盖了实际异常)。这可能导致问题的实际原因被掩盖,在选择使用 Lombok 的资源管理时应该考虑这种情况。此外,随着Java 7中出现了自动资源管理,以后会很少需要使用该注解。

示例代码

public void testCleanUp() {
    try {
        @Cleanup ByteArrayOutputStream baos = new ByteArrayOutputStream();
        baos.write(new byte[] {'Y','e','s'});
        System.out.println(baos.toString());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

生成代码

public void testCleanUp() {
    try {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        try {
            baos.write(new byte[]{89, 101, 115});
            System.out.println(baos.toString());
        } finally {
        	//进行了资源关闭
            if (Collections.singletonList(baos).get(0) != null) {
                baos.close();
            }
        }
    } catch (IOException var6) {
        var6.printStackTrace();
    }
}

3.13. @Synchronized

使用方法

使用 @Synchronized 注释实例方法将使该方法变为一个同步方法,生成一个名为$lock的私有锁定字段,该方法在执行之前会锁定该字段。类似地,以同样的方式注释静态方法将生成一个私有静态对象$LOCK,以便静态方法以相同的方式使用。可以通过向注释的值参数提供字段名来指定不同的锁定对象。当提供字段名时,开发人员必须确保Lombok不会生成该字段。

示例代码

private DateFormat format = new SimpleDateFormat("yyyy-MM-dd");

@Synchronized
public String synchronizedFormat(Date date) {
    return format.format(date);
}

private static DateFormat format1 = new SimpleDateFormat("yyyy-MM-dd");

@Synchronized
public static String synchronizedFormat1(Date date) {
    return format1.format(date);
}

生成代码

private DateFormat format = new SimpleDateFormat("yyyy-MM-dd");

private final Object $lock = new Object[0];

public String synchronizedFormat(Date date) {
    synchronized(this.$lock) {
        return this.format.format(date);
    }
}

private static DateFormat format1 = new SimpleDateFormat("yyyy-MM-dd");

private static final Object $LOCK = new Object[0];

public static String synchronizedFormat1(Date date) {
    synchronized($LOCK) {
        return format1.format(date);
    }
}

3.14. @SneakyThrows

使用方法

如果一个类里面抛出一个Exception,但是类上没进行抛出;或者父类抛出了一个异常,但是子类没有进行处理,这种情况都会产生编译期错误,会提示有一个“未处理的异常”错误。当在类上使用 @SneakyThrows 注释时,错误将消失。默认情况下,@SneakyThrows 将允许抛出任何检查过的异常,而不需要在throw子句中声明。通过向注释的值参数提供一个可抛出类(Class)数组,可以将此限制为一组特定的异常。

示例代码

@SneakyThrows
public void testSneakyThrows() {
    throw new IllegalAccessException();
}

等效代码

public void testSneakyThrows() {
    try {
        throw new IllegalAccessException();
    } catch (Throwable var2) {
        throw var2;
    }
}

3.15. @Accessors

使用方法

@Accessors 注解用来配置 Lombok 如何生成 getter 和 setter 方法,可以用在类上和字段上。单独使用这个注解是没有任何作用的,必须和一个可以生成 getter 和 setter 方法的注解一起使用,例如 @Setter、@Getter 或 @Data 注解。

该注解有三个属性可进行设置:

属性描述
fluent如果为 true 则生成的 getter/setter 方法没有 set/get 前缀,默认为 false。
如果该属性为 true,并且 chain 未设置,则 chain 会被默认设置为 true。
chain如果为 true 则生成的 setter 方法返回 this,默认为 false,生成的 setter 方法返回是 void。
如果没有显式设置该属性为 false,则当 fluent 为 true 时,chain 会被默认设置为 true。
prefix该属性可以指定一系列前缀,生成 getter/setter 方法时会去掉指定的前缀。注意:
(1)只有字段中前缀的下一个字符不是小写字母或者前缀的最后一个字符不是字母(例如是下划线)时,前缀才算合法。
(2)如果去掉前缀时多个字段都变成相同的名称,将生成一个错误。

(1)对于前两个属性,经测试,发现@Accessors(fluent = true)@Accessors(fluent = true, chain = true)效果是一样的,@Accessors(fluent = true)@Accessors(fluent = true, chain = false)效果是不同的。

(2)第三个属性,如果设置的前缀不合法,会出现下面的警告,并且不会为该字段或该类的所有字段生成 getter/setter 方法。

Warning:(15, 21) java: Not generating getter for this field: It does not fit your @Accessors prefix list.

如果去掉前缀时多个字段都变成相同的名称,则只会给这几个字段中第一个字段(由上到下数)生成对应的 setter/getter 方法,这几个字段中的其他字段则不会生成 setter/getter 方法。

示例代码

package com.wangbo.cto.lombok;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;

@Getter@Setter
public class UserInfo{
    @Accessors(fluent = true, chain = false)
    private Long userId;
    @Accessors(chain = true)
    private String userName;
    @Accessors(prefix = {"user"})
    private Integer userAge;
}

等效代码

package com.wangbo.cto.lombok;

public class UserInfo {
    private Long userId;
    private String userName;
    private Integer userAge;

    public UserInfo() {}
    
	//fluent = true,生成的 getter 和 setter 方法没有 set/get 前缀
    public Long userId() {
        return this.userId;
    }
	public void userId(Long userId) {
        this.userId = userId;
    }
    
    public String getUserName() {
        return this.userName;
    }
	//chain = true,生成的 setter 方法返回 this,而不是 void
	public UserInfo setUserName(String userName) {
        this.userName = userName;
        return this;
    }
    
	//prefix = {"user"},去掉了字段的 user 前缀
    public Integer getAge() {
        return this.userAge;
    }
    public void setAge(Integer userAge) {
        this.userAge = userAge;
    }
}

3.16. @Tolerate

使用 @Builder 对一个 DTO 实现一个构造器,但是在做 Json 反序列化的时候会发生错误,原因就是缺少公共的无参构造函数,而手动写一个无参构造函数的时候会编译错误,就是和 @Builder 冲突了,这时候使用 @Tolerate 就能实现对冲突的兼容。

@Builder
public class UserInfo {
    @Tolerate
    public UserInfo () {}
}

4. 问题记录

4.1. 循环依赖

2021年06月21日:

目前Lombok使用的越来越多,感觉真的特别好用,今天遇到了一个问题,这里记录一下。

问题是这样的,项目中用到两个类,TestA 类是 TestB 类的父类,然后 TestA 类中有个成员变量是 TestB 类型的,TestB 类中也有个成员变量是 TestA 类型的,然后都使用了@Data注解,如下所示:

@Data
public class TestA {
    private TestB testB;
}
@Data
public class TestB extends TestA {
    private TestA testA;
}

因为程序中的一个框架默认会调用 TestA 类的hashCode()方法,这时就出问题了,造成了循环依赖,最后一直压栈,导致了 java.lang.StackOverflowError的错误。

原因出在@Data注解上,通过反编译class文件,可以看到 TestA 的hashCode()方法中调用了 TestB 的hashCode()方法,TestB 的hashCode()方法中调用了 TestA 的hashCode()方法,于是便无限递归了。

一种解决方法是使用@Setter@Getter代替@Data

还有一种解决方法是在使用@Data时加上@EqualsAndHashCode(exclude = {})注解。根据项目情况,我们使用了该种解决办法。

@Data
@EqualsAndHashCode(exclude = {"testB"})
public class TestA {
    private TestB testB;
}
@Data
@EqualsAndHashCode(exclude = {"testA"})
public class TestB extends TestA{
    private TestA testA;
}

4.2. 对象比较

@Data相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这 5 个注解的合集。其中的@EqualsAndHashCode使用默认配置。@EqualsAndHashCode注解会生成equals(Object other)hashCode()方法。默认仅使用该类中定义的属性且不调用父类的方法,可通过callSuper=true让其生成的方法中调用父类的方法。

比如,有多个类有相同的部分属性,把它们定义到父类中,恰好 id 属性(数据库主键)也在父类中,那么就会存在部分对象在比较时,它们并不相等,却因为 lombok 自动生成的 equals(Object other) 和 hashCode() 方法判定为相等,从而导致出错。

一种解决办法是在使用 @Getter @Setter @ToString 代替 @Data 并且自定义 equals(Object other) 和 hashCode() 方法,比如有些类只需要判断主键 id 相等即代表相等。

另一种解决方法就是在使用 @Data 时加上 @EqualsAndHashCode(callSuper=true) 注解。

4.3. @Builder 注意事项

4.3.1. @Data 和 @Builder 导致无参构造方法丢失

单独使用 @Data 注解,是会生成指定参数参数的构造方法。

单独使用 @Builder 注解,发现生成了全参数的构造方法。

@Data 和 @Builder 一起用,发现没有了默认的构造方法。如果手动添加无参数构造方法或者用 @NoArgsConstructor 注解都会报错!

两种解决方法:

1、公共构造方法上加 @Tolerate 注解,让 lombok 假装它不存在(不感知)。

@Data
@Builder
public class TestLombok {
    @Tolerate
    TestLombok() {}
    ......
}    

2、直接加上下面的四个注解

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TestLombok {
    ......
}    

4.3.2. @Builder 注解导致默认值无效

@Builder 注解会把对象的默认值清掉,需要在有(显式)默认值的字段上面加上 @Builder.Default 注解。

@Data
@Builder
public class TestLombok {
    @Builder.Default
    private String aa = "123";
}    

总结: 如果想要使用@Builder,最简单的方法就是直接写上这4个注解,有默认值的话再加上@Builder.Default,正常情况下就没啥问题了!

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TestLombok {

    @Builder.Default
    private String aa = "123";
}