Lombok:一个让你的Java代码更简洁和优雅的工具
如果你是一个Java开发者,你可能经常遇到这样的情况:为了遵循Java Bean规范或者框架的要求,你不得不为你的类写很多重复和冗余的代码,比如getter和setter方法、构造器、equals和hashCode方法等等。这些代码不仅占用了你的时间和空间,而且增加了你的维护成本和出错的风险。有没有一种方法可以让你省去这些繁琐的工作,让你的代码更简洁和优雅呢?
答案是有的,那就是Lombok。Lombok是一个Java库,它可以通过注解的方式自动为你生成这些常用的代码,从而减少你的编码量和提高你的效率。Lombok可以很容易地集成到你的IDE和构建工具中,让你在编写和编译时就能享受它带来的便利。
依赖配置
以maven为例
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
常用注解
注解 | 功能 |
---|---|
@Getter | 为属性生成getter方法 |
@Setter | 为属性生成setter方法 |
@NoArgsConstructor | 为类生成无参构造器 |
@RequiredArgsConstructor | 为类生成包含所有final或@NonNull属性作为参数的构造器 |
@AllArgsConstructor | 为类生成包含所有属性作为参数的构造器 |
@Data | 为类生成所有常用的方法,包括getter和setter、equals和hashCode、toString、构造器等等 |
@Builder | 为类生成一个构建器(builder)模式,让你可以用链式调用的方式创建对象 |
@Slf4j | 为这个类生成一个名为log的日志变量,使用org.slf4j.Logger作为日志框架。让可以方便地打印日志信息 |
@SneakyThrows | 让你在方法中抛出受检异常(checked exception),而不需要在方法签名中声明或者使用try-catch语句 |
@Value | 为类生成一个不可变(immutable)的对象,即所有属性都是final的,并且只有getter方法,没有setter方法 |
@Accessor | 为属性生成自定义的访问方法,让你可以控制方法的名称、修饰符、参数等等 |
@With | 为属性生成一个返回一个新对象的方法,让你可以用不可变(immutable)的方式修改对象的属性 |
@Singular | 为集合属性生成一个构建器(builder)模式,让你可以用链式调用的方式添加元素 |
@NonNull | 为方法或构造器的参数添加非空检查,如果参数为null,抛出NullPointerException |
@Cleanup | 为需要关闭的资源自动调用close方法,避免资源泄漏 |
@Synchronized | 为方法添加同步锁,避免多线程问题 |
@EqualsAndHashCode | 为类生成equals和hashCode方法,根据属性的值判断对象是否相等 |
@ToString | 为类生成toString方法,返回对象的字符串表示 |
@Delegate | 为类生成委托(delegate)方法,让你可以调用另一个对象的方法,而不需要自己编写 |
@Val | 表示一个不可变的局部变量,相当于使用final修饰符 |
@Var | 表示一个可变的局部变量,相当于省略了类型声明 |
@Getter和@Setter
@Getter和@Setter注解可以为类中的属性自动生成getter和setter方法。这样,你就不需要手动编写这些方法,也不需要使用IDE提供的自动生成功能。例如:
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class User {
private String name;
private int age;
}
这段代码相当于:
public class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
你可以在类上使用@Getter和@Setter注解,也可以在属性上使用。如果在类上使用,那么所有属性都会生成对应的方法;如果在属性上使用,那么只有该属性会生成对应的方法。另外,你还可以指定生成方法的访问修饰符(access modifier),默认是public。
@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor
@NoArgsConstructor, @RequiredArgsConstructor, @AllArgsConstructor注解可以为类生成不同类型的构造器。@NoArgsConstructor会生成一个无参构造器;@RequiredArgsConstructor会生成一个包含所有final或@NonNull属性作为参数的构造器;@AllArgsConstructor会生成一个包含所有属性作为参数的构造器。例如:
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
@NoArgsConstructor
@RequiredArgsConstructor
@AllArgsConstructor
public class User {
@NonNull private String name;
private int age;
}
这段代码相当于:
public class User implements Serializable {
private static final long serialVersionUID = -8054600833969507380L;
private String name;
private int age;
public User() {
}
public User(String name) {
this.name = name;
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
}
@Data
@Data注解是一个综合性的注解,它可以为类生成所有常用的方法,包括getter和setter、equals和hashCode、toString、构造器等等。例如:
import lombok.Data;
@Data
public class User {
private String name;
private int age;
}
这段代码相当于:
public class User {
private String name;
private int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
可以看到,使用@Data注解可以大大减少你的代码量,让你的类更简洁和清晰。当然,如果你不想生成所有的方法,你也可以使用其他的注解来选择性地生成你需要的方法。
@Builder
@Builder注解可以为类生成一个构建器(builder)模式,让你可以用链式调用的方式创建对象。这样,你就不需要写一个很长的构造器或者一个静态工厂方法,而且可以避免参数顺序或者数量的错误。例如:
import lombok.Builder;
@Builder
public class User {
private String name;
private int age;
}
这段代码相当于:
public class User {
private String name;
private int age;
private User(String name, int age) {
this.name = name;
this.age = age;
}
public static UserBuilder builder() {
return new UserBuilder();
}
public static class UserBuilder {
private String name;
private int age;
UserBuilder() {
}
public UserBuilder name(String name) {
this.name = name;
return this;
}
public UserBuilder age(int age) {
this.age = age;
return this;
}
public User build() {
return new User(name, age);
}
}
}
你可以看到,使用@Builder注解可以让你用下面的方式创建对象:
User user = User.builder()
.name("Alice")
.age(20)
.build();
@Builder 有以下几个参数:
- builderClassName: 表示构建器类的名字,默认为类名加上Builder后缀。
- builderMethodName: 表示构建器方法的名字,默认为builder。
- buildMethodName: 表示构建对象的方法的名字,默认为build。
- toBuilder: 表示是否生成一个toBuilder方法,可以从一个已有的对象创建一个构建器,默认为false。
- access: 表示构建器类和方法的访问级别,默认为public。
- setterPrefix:表示构建器类中的setter方法的前缀,默认为空字符串。如果指定了这个参数,那么构建器类中的setter方法会以这个前缀开头,而不是以字段名开头
import lombok.Builder;
@Builder(builderClassName = "UserBuilder", builderMethodName = "create",
buildMethodName = "done", toBuilder = true,
access = lombok.AccessLevel.PROTECTED,setterPrefix = "with")
public class User {
private String name;
private int age;
}
// 测试代码
// 使用自定义的构建器类和方法创建User对象
User user = User.create().withName("张三").withAge(18).done();
// 使用toBuilder方法从已有的对象创建一个新的对象
User user2 = user.toBuilder().withName("李四").done();
@Slf4j
@Slf4j注解可以为类生成一个日志对象(logger),让你可以方便地打印日志信息。这样,你就不需要手动创建和初始化一个日志对象,而且可以避免引入额外的日志库。例如:
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class UserService {
public void createUser(User user) {
// some logic
log.info("User created: {}", user);
}
}
这段代码相当于:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UserService {
private static final Logger log = LoggerFactory.getLogger(UserService.class);
public void createUser(User user) {
// some logic
log.info("User created: {}", user);
}
}
你可以看到,使用@Slf4j注解可以让你用log对象来打印不同级别的日志信息,而不需要自己创建和初始化一个Logger对象。Lombok还支持其他的日志框架,比如@Log4j, @Log4j2, @CommonsLog等等,你可以根据你的需要选择合适的注解。
@SneakyThrows
@SneakyThrows注解可以让你在方法中抛出受检异常(checked exception),而不需要在方法签名中声明或者使用try-catch语句。这样,你就可以避免写一些冗余的代码,也可以让你的方法更简洁和清晰。例如:
import lombok.SneakyThrows;
public class FileService {
@SneakyThrows
public void readFile(String fileName) {
// some logic
throw new IOException("File not found");
}
}
这段代码相当于:
public class FileService {
public void readFile(String fileName) {
// some logic
try {
throw new IOException("File not found");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
你可以看到,使用@SneakyThrows注解可以让你在方法中直接抛出IOException,而不需要在方法签名中声明或者使用try-catch语句。Lombok会在编译时将异常包装成一个运行时异常(runtime exception),从而绕过编译器的检查。
@Value
@Value注解可以为类生成一个不可变(immutable)的对象,即所有属性都是final的,并且只有getter方法,没有setter方法。这样,你就可以创建一个安全和稳定的对象,而不需要自己编写很多代码。例如:
import lombok.Value;
@Value
public class Point {
private int x;
private int y;
}
这段代码相当于:
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
// equals, hashCode, toString methods
}
你可以看到,使用@Value注解可以让你创建一个不可变的对象,即所有属性都是final的,并且只有getter方法,没有setter方法。Lombok还会为这个类生成一个全参构造器、equals、hashCode和toString方法。
@Accessors
@Accessors注解可以为属性生成自定义的访问方法,让你可以控制方法的名称、修饰符、参数等等。这样,你就可以根据你的需要或者习惯来定义你的访问方法,而不需要遵循Java Bean规范或者Lombok的默认规则。
@Accessors注解有以下几个属性,你可以根据你的需要选择合适的属性:
- prefix:指定要 去掉的属性前缀 ,比如m, f等等。例如:
import lombok.experimental.Accessors;
@Accessors(prefix = "m")
@Getter
@Setter
public class User {
private String mName;
private int mAge;
}
这段代码会生成以下的访问方法:
public String getName() {
return this.mName;
}
public void setName(String name) {
this.mName = name;
}
public int getAge() {
return this.mAge;
}
public void setAge(int age) {
this.mAge = age;
}
- fluent:指定是否使用 流式风格(fluent style )来生成访问方法,即 方法名和属性名相同,返回值为当前对象 。例如:
import lombok.experimental.Accessors;
@Accessors(fluent = true)
@Getter
@Setter
public class User {
private String name;
private int age;
}
这段代码会生成以下的访问方法:
public String name() {
return this.name;
}
public User name(String name) {
this.name = name;
return this;
}
public int age() {
return this.age;
}
public User age(int age) {
this.age = age;
return this;
}
- chain:指定是否使用链式风格(chain style)来生成访问方法,即返回值为当前对象。这个属性只对setter方法有效,如果fluent为true,则默认为true。例如:
import lombok.experimental.Accessors;
@Accessors(chain = true)
@Getter
@Setter
public class User {
private String name;
private int age;
}
这段代码会生成以下的访问方法:
public String getName() {
return this.name;
}
public User setName(String name) {
this.name = name;
return this;
}
public int getAge() {
return this.age;
}
public User setAge(int age) {
this.age = age;
return this;
}
- makeFinal:一个布尔值。如果为 true,那么生成的 getter、setter 都会是 final 的。默认值:false。
import lombok.experimental.Accessors;
@Accessors(makeFinal = true)
@Getter
@Setter
public class User {
private String name;
private int age;
}
这段代码会生成以下的访问方法:
public class User {
private String name;
private int age;
public final String getName() {
return this.name;
}
public final void setName(String name) {
this.name = name;
}
public final int getAge() {
return this.age;
}
public final void setAge(int age) {
this.age = age;
}
}
注意,生成的 getter 和 setter 都是 final 的,也就是说,你不能在子类中重写它们
@With
@With注解可以为属性生成一个返回一个新对象的方法,让你可以用不可变(immutable)的方式修改对象的属性。这样,你就可以创建一个安全和稳定的对象,而不需要自己编写很多代码。例如:
import lombok.With;
@With
@NoArgsConstructor
@AllArgsConstructor
public class User {
private String name;
private int age;
}
这段代码相当于:
public class User {
private String name;
private int age;
public User() {
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public User withName(String name) {
return this.name == name ? this : new User(name, this.age);
}
public User withAge(int age) {
return this.age == age ? this : new User(this.name, age);
}
}
你可以看到,使用@With注解可以让你为属性生成一个返回一个新对象的方法,比如withName, withAge等等。这些方法会创建一个新的对象,并用给定的参数替换原来的属性值。这样,你就可以用不可变的方式修改对象的属性,而不影响原来的对象。
@Value
public class Person {
String name;
@With
int age;
}
这段代码相当于:
public final class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public Person withAge(int age) {
return this.age == age ? this : new Person(this.name, age);
}
@Override
public boolean equals(Object o) {
// omitted for brevity
}
@Override
public int hashCode() {
// omitted for brevity
}
@Override
public String toString() {
// omitted for brevity
}
}
注意,@Value 注解会生成一个不可变的类,所有的字段都是 final 的,并且有一个全参构造器,一个 getter 方法,一个 equals 方法,一个 hashCode 方法和一个 toString 方法。@With 注解只会对指定的字段生成一个 with-er 方法,返回一个新的对象。
@Singular
@Singular: 这个注解可以用在@Builder注解的字段上,表示这个字段是一个集合类型,可以通过builder的方法添加单个元素或多个元素
import lombok.Builder;
import lombok.Singular;
import java.util.List;
@Builder
public class User {
private String name;
@Singular
private List<String> hobbies;
}
// 使用Builder创建User对象
User user = User.builder()
.name("李四")
.hobby("游泳")
.hobby("跑步")
.hobbies(List.of("唱歌", "跳舞")).build();
@NonNull
这个注解可以用在字段或参数上,表示这个字段或参数不能为空,否则会抛出空指针异常。例如,你可以这样写:
import lombok.NonNull;
public class User {
@NonNull
private String name;
private int age;
public User(@NonNull String name) {
this.name = name;
}
public void setName(@NonNull String name) {
this.name = name;
}
}
// 测试代码
User user1 = new User("王五"); // 正常
User user2 = new User(null); // 抛出空指针异常
user1.setName("赵六"); // 正常
user1.setName(null); // 抛出空指针异常
@Cleanup
这个注解可以用在局部变量上,表示这个变量需要在使用完毕后调用它的close()方法来释放资源。例如,你可以这样写:
import lombok.Cleanup;
import java.io.*;
public class FileUtil {
public static void copyFile(String src, String dest) throws IOException {
@Cleanup InputStream in = new FileInputStream(src);
@Cleanup OutputStream out = new FileOutputStream(dest);
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
}
}
// 测试代码
FileUtil.copyFile("test.txt", "copy.txt"); // 正常执行,并且自动关闭输入输出流
@Synchronized
import lombok.Synchronized;
public class Counter {
private int count;
@Synchronized
public void increment() {
count++;
}
@Synchronized
public int getCount() {
return count;
}
}
//测试代码
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount()); // 输出20000,没有出现线程安全问题
@EqualsAndHashCode
这个注解可以用在类上,表示为这个类生成equals和hashCode方法,可以根据需要指定哪些字段参与比较和计算。例如,你可以这样写:
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(exclude = "age")
public class User {
private String name;
private int age;
}
// 测试代码
User user1 = new User("张三", 18);
User user2 = new User("张三", 20);
System.out.println(user1.equals(user2)); // 输出true,因为只比较了name字段
System.out.println(user1.hashCode() == user2.hashCode()); // 输出true,因为只根据name字段计算了哈希值
@ToString
这个注解可以用在类上,表示为这个类生成toString方法,可以根据需要指定哪些字段包含或排除在输出中,以及是否调用父类的toString方法。参数和示例如下
- includeFieldNames: 表示是否在输出中包含字段名,默认为true。如果为false,只输出字段值,用逗号分隔。例如:
import lombok.ToString;
@AllArgsConstructor
@ToString(includeFieldNames = false)
public class User {
private String name;
private int age;
}
// 测试代码
User user = new User("张三", 18);
System.out.println(user); // 输出User(张三, 18),没有输出字段名
- callSuper: 表示是否调用父类的toString方法,默认为false。如果为true,会在输出中包含父类的toString方法的结果。例如:
import lombok.ToString;
@AllArgsConstructor
@ToString(callSuper = true)
public class User extends Person {
private String name;
private int age;
}
// 测试代码
User user = new User("李四", 18);
System.out.println(user); // 输出Person(User(name=李四, age=18)),包含了父类的toString方法的结果
- exclude: 表示要排除在输出中的字段名,可以指定多个。例如:
import lombok.ToString;
@AllArgsConstructor
@ToString(exclude = {"age", "password"})
public class User {
private String name;
private int age;
private String password;
}
// 测试代码
User user = new User("王五", 20, "123456");
System.out.println(user); // 输出User(name=王五),排除了age和password字段
- of: 表示要包含在输出中的字段名,可以指定多个。如果指定了这个参数,那么其他没有指定的字段都会被排除。例如:
import lombok.ToString;
@AllArgsConstructor
@ToString(of = {"name", "age"})
public class User {
private String name;
private int age;
private String password;
}
// 测试代码
User user = new User("赵六", 22, "654321");
System.out.println(user); // 输出User(name=赵六, age=22),只包含了name和age字段
@Delegate
这个注解可以用在字段上,表示将这个字段的所有方法委托给当前类,相当于实现了一个接口或继承了一个类,但是不需要显式地重写每个方法。例如,你可以这样写:
import lombok.experimental.Delegate;
import java.util.Collection;
import java.util.HashSet;
public class UserCollection<T> {
@Delegate
private Collection<T> users = new HashSet<>();
}
// 测试代码
UserCollection<String> userCollection = new UserCollection<>();
userCollection.add("张三");
userCollection.add("李四");
userCollection.add("王五");
userCollection.remove("李四");
System.out.println(userCollection.size()); // 输出2,因为委托了Collection接口的所有方法
@val
表示一个不可变的局部变量,相当于使用final修饰符
// 相当于 final List<String> list = new ArrayList<String>();
val list = new ArrayList<String>();
list.add("Hello");
list.add("World");
@var
表示一个可变的局部变量,相当于省略了类型声明
var name = "张三"; // 相当于 String name = "张三";
name = "李四"; // 可以重新赋值
// name = 123; // 编译错误,不能改变类型
Lombok的原理
Lombok利用了Java的注解处理器(Annotation Processor)机制,它可以在编译时扫描和处理注解,并生成额外的Java代码。Lombok通过实现一个自定义的注解处理器,来拦截和修改抽象语法树(AST),从而在类中添加相应的方法,字段,构造器等。Lombok还提供了一个插件,可以让IDE在编辑时也能识别和显示Lombok生成的代码,从而避免编译错误和提示信息的不一致。
Lombok的原理可以用以下几个步骤来概括:
- 定义一个注解,比如@Getter,用来标记需要生成getter方法的类或字段。
- 定义一个注解处理器,比如GetterProcessor,用来处理@Getter注解,并在类中生成相应的getter方法。
- 在注解处理器中,使用Lombok提供的API,比如JavacAST,JavacHandlerUtil等,来获取和修改AST。
- 在编译时,使用javac或者其他工具(比如maven,gradle等)来调用注解处理器,并传入源代码。
- 注解处理器扫描源代码中的注解,并根据注解的参数和目标来生成相应的代码,并添加到AST中。
- 编译器根据修改后的AST来生成字节码文件(.class文件)。
总结
Lombok是一个非常实用的Java库,它可以让我们的代码更加简洁,可读,健壮。Lombok有很多优点,但也有一些缺点,比如可能会影响代码的调试,测试,维护等。因此,在使用Lombok时,我们需要权衡利弊,根据自己的需求和喜好来选择合适的注解和配置。
原文链接https://zhuanlan.zhihu.com/p/623338642?utm_id=0
标题:Lombok:一个让你的Java代码更简洁和优雅的工具
作者:michael
地址:https://blog.junxworks.cn/articles/2024/01/05/1704437369629.html