使用 Lombok 进行 Java 开发


1. 简单介绍

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

2. 准备工作

2.1. 安装插件

我使用的开发工具是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.8</version>
</dependency>

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

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

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

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 属性生成的静态函数的访问级别。

示例代码

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

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 的变体(任何一个适用于你使用的日志系统的注解)放在你的类上,然后会生成一个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;
    }
}

4. 实现原理

Lombok这款插件是依靠可插件化的Java自定义注解处理API(JSR 269: Pluggable Annotation Processing API)来实现在javac编译阶段利用Annotation Processor(注解处理) 对自定义的注解进行预处理后生成真正在JVM上面执行的Class文件。大致执行原理图如下:
在这里插入图片描述
从上面的这个原理图上可以看出Annotation Processing是编译器在解析Java源代码和生成Class文件之间的一个步骤。其中Lombok插件具体的执行流程如下:
在这里插入图片描述
从上面的Lombok执行的流程图中可以看出,在javac解析成AST抽象语法树之后,Lombok根据自己编写的注解处理器,动态地修改AST,增加新的节点(即Lombok自定义注解所需要生成的代码),最终通过分析生成JVM可执行的字节码Class文件。使用Annotation Processing自定义注解是在编译阶段进行修改,而JDK的反射技术是在运行时动态修改,两者相比,反射虽然更加灵活一些,但是带来的性能损耗更加大。

5. 写在最后

(1)优点

  • 能通过注解的形式自动生成构造器、getter/setterequalshashcodetoString等方法,提高了一定的开发效率。
  • 让代码变得简洁,不用过多的去关注相应的方法。
  • 属性做修改时,也简化了维护为这些属性所生成的getter/setter方法等。

(2)缺点

  • 不支持多种参数构造器的重载。
  • 降低了源代码的可读性和完整性,降低了阅读源代码的舒适度。

(3)总结
Lombok虽然有很多优点,但Lombok更类似于一种IDE插件,项目也需要依赖相应的jar包。Lombok依赖jar包是因为编译时要用它的注解,为什么说它又类似插件?因为在使用时,EclipseIntelliJ IDEA都需要安装相应的插件,在编译器编译时通过操作AST(抽象语法树)改变字节码生成,变向的就是说它在改变 java语法。它不像spring的依赖注入或者mybatisORM一样是运行时的特性,而是编译时的特性。

这里引用下网上一些人对Lombok的看法:

这是一种低级趣味的插件,不建议使用。Java发展到今天,各种插件层出不穷,如何甄别各种插件的优劣?能从架构上优化你的设计的,能提高应用程序性能的,实现高度封装可扩展的…, 像Lombok这种,已经不仅仅是插件了,它改变了你如何编写源码,事实上,少去的代码你写上去又如何? 如果Java家族到处充斥着这样的东西,那只不过是一坨披着金属颜色的屎,迟早会被其它语言取代。

如果一个项目有非常多类似Lombok这样的插件,真的会极大的降低阅读源代码的舒适度。Lombok有它得天独厚的优点,也有它避之不及的缺点,在实战中需要灵活运用。其实我觉得在项目中适度使用还是可以的,用几个最简单基本的注解,能大大简化一些模板型代码的开发。比如我常用的是@Data@Slf4j@Setter@Getter@EqualsAndHashCode@ToString

6. 问题记录

6.1. 循环依赖

2021年06月21日:

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

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

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

因为程序中的一个框架默认会调用 A 类的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 {
    private TestA testA;
}

6.2. 对象比较

还有个问题可参见 lombok @EqualsAndHashCode 注解的影响

@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) 注解。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值