Lombok:一个让你的Java代码更简洁和优雅的工具

  |   0 评论   |   0 浏览

如果你是一个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