从零开始 Spring Boot 35:Lombok

news2024/11/28 2:28:54

从零开始 Spring Boot 35:Lombok

spring boot

图源:简书 (jianshu.com)

Lombok是一个java项目,旨在帮助开发者减少一些“模板代码”。其具体方式是在Java代码生成字节码(class文件)时,根据你添加的相关Lombok注解或类来“自动”添加和生成相应的字节码,以补完代码所需的“模板代码”。

实际上 Lombok 和 Spring 并没有关联关系,你开发任何Java应用都可以选择使用 Lombok,只不过日常的 Spring 开发中很容易看到 Lombok 的使用,所以这里就归类到这个系列博客。

为什么要使用 Lombok

我们先看一个Spring 开发中很常见的 POJO 类是什么样的:

public class Book {

    private Long id;
    private String name;
    private Long userId;
    private Long publisherId;

    public Book() {
    }

    public Book(String name, Long userId, Long publisherId) {
        this.name = name;
        this.userId = userId;
        this.publisherId = publisherId;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }


    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public Long getPublisherId() {
        return publisherId;
    }

    public void setPublisherId(Long publisherId) {
        this.publisherId = publisherId;
    }
}

实际上这种有一个空构造器和Getter/Setter的 Java 类,被称作 Java Bean,最早是为了开发 Java桌面应用提出的标准,不过目前已经被第三方 Java 框架广泛采纳和使用。

为了能让框架获取或修改我们的自定义类中的属性,我们需要提供Getter/Setter,以及可能需要的包含各种参数的构造器。显然为了让一个类变成 Java Bean所添加的代码,都是“模板代码”,是可以通过自动化手段取代的,这里我们就是 Lombok 的用武之地了。

如果上边的示例中 Lombok 改写,会变成这样:

@NoArgsConstructor
@Setter
@Getter
public class Book {

    private Long id;
    private String name;
    private Long userId;
    private Long publisherId;

    public Book(String name, Long userId, Long publisherId) {
        this.name = name;
        this.userId = userId;
        this.publisherId = publisherId;
    }
}

这样做的好处有:

  • 减少了不必要的模板代码,提高效率,以及让代码更简洁。
  • 如果新添加了属性,无需手动添加相应的Getter/Setter。

当然,要使用 Lombok,需要在项目中添加相应的依赖:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

下面一一介绍Lombok的功能。

var

在 Python3 或者 Go 这类“新语言”中,“自动类型推断”是一个很常见的语言级别功能,这个功能或多或少都会让你的编码工作更顺畅一些。Java 自 Java 10 起,也支持类似的功能:

  • JEP 286: Local-Variable Type Inference (openjdk.org)

直接看示例:

package com.example.lombok;

// ...
@SpringBootApplication
public class LombokApplication {

    public static void main(String[] args) {
        SpringApplication.run(LombokApplication.class, args);
        testVar();
    }

    private static void testVar() {
        var names = new ArrayList<String>();
        names.add("Li Lei");
        System.out.println(names.get(0));
        var students = new HashMap<Integer, String>();
        students.put(12, "Li Lei");
        students.put(20, "Han Meimei");
        for (var s : students.entrySet()) {
            System.out.println("number is %d, name is %s.".formatted(s.getKey(), s.getValue()));
        }
        students = new HashMap<>();
    }

}

在示例中,局部变量和for的条件语句中都用var取代了具体类型。

代码编译成字节码后,var会被相应的具体类型取代:

    // ...
        private static void testVar() {
        ArrayList<String> names = new ArrayList();
        names.add("Li Lei");
        System.out.println((String)names.get(0));
        HashMap<Integer, String> students = new HashMap();
        students.put(12, "Li Lei");
        students.put(20, "Han Meimei");
        Iterator var2 = students.entrySet().iterator();

        while(var2.hasNext()) {
            Entry<Integer, String> s = (Entry)var2.next();
            System.out.println("number is %d, name is %s.".formatted(new Object[]{s.getKey(), s.getValue()}));
        }

        new HashMap();
    }
    // ...

val

val是 Lombok 引入的一个类型,其功能相当于 final var

import lombok.val;
// ...
private static void testVal() {
    val names = new ArrayList<String>();
    names.add("Li Lei");
    System.out.println(names.get(0));
    val students = new HashMap<Integer, String>();
    students.put(12, "Li Lei");
    students.put(20, "Han Meimei");
    for (var s : students.entrySet()) {
        System.out.println("number is %d, name is %s.".formatted(s.getKey(), s.getValue()));
    }
}
//...

注意,要通过import lombok.val导入val类型到当前命名空间,否则就要用lombok.val声明变量。

var的区别是,这里用val声明的局部变量都是final的,因此不能被重新赋值。此外,val是 Lombok 的类型,因此,即使是 Java10以下的版本,也可以使用。

@NonNull

在从零开始 Spring Boot 33:Null-safety - 红茶的个人站点 (icexmoon.cn)中,我讨论过Spring框架对Null安全的支持,但那些支持都不是强制性的,仅能借助IDE的相关工具在编码阶段提供一些警告信息。

相比之下,可以借助Lombok的@NonNull注解,实现对属性或方法参数的强制性检查:

@NoArgsConstructor
@Setter
@Getter
public class Book {

    private Long id;
    private String name;
    @NonNull
    private Long userId;
    private Long publisherId;

    public Book(@NonNull String name, Long userId, Long publisherId) {
        this.name = name;
        this.userId = userId;
        this.publisherId = publisherId;
    }
}

注意,这里使用的是lombok.NonNull,而非Spring框架或者别的库的NonNull注解。

观察对应的字节码:

public class Book {
    // ...
    @NonNull
    private Long userId;
    // ...
	public Book(@NonNull String name, Long userId, Long publisherId) {
        if (name == null) {
            throw new NullPointerException("name is marked non-null but is null");
        } else {
            this.name = name;
            this.userId = userId;
            this.publisherId = publisherId;
        }
    }
    // ...
    public void setUserId(@NonNull final Long userId) {
        if (userId == null) {
            throw new NullPointerException("userId is marked non-null but is null");
        } else {
            this.userId = userId;
        }
    }
    // ...
    @NonNull
    public Long getUserId() {
        return this.userId;
    }
}

可以看到,使用@NonNull注解标记参数的方法体中被自动添加了if语句检查相应的参数是否为null,如果是就抛出NullPointerException异常。

如果用@NonNull标记属性,则相应由 Lombok 自动生成的方法(这里是setUserId)中会添加对该属性的null检查语句。

对于Getter,仅会用@NonNull标记,表示返回的是一个非Null值,不会添加其他的语句。

和 Spring 框架的@NonNull不同,Lombok 的@NonNull主要用于标记方法参数和属性,但如果用于方法也不会报错,只不过不会自动生成任何语句。

@Cleanup

在使用外部资源时,我们往往需要在最后手动关闭(这通常是使用try...catch...finally语句实现)。但是有时候我们会因为忘记添加关闭语句而导致bug。而 Lombok 提供一个@Cleanup注解,可以帮助我们。

go语言在语言层级提供关键字以关闭相应的资源。

直接看示例:

private static void testCleanUp() throws IOException {
    ClassPathResource classPathResource = new ClassPathResource("application.properties");
    @Cleanup InputStream inputStream = classPathResource.getInputStream();
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
    do {
        var line = bufferedReader.readLine();
        if (line == null){
            break;
        }
        System.out.println(line);
    }
    while (true);
}

这里通过Spring的Resource获取了class:application.properties文件对应的InputStream,并且逐行读取后输出。最后并没有显式调用inputStream.close(),这是因为我们用@Cleanup标记了inputStream变量。所以 Lombok 会自动添加上相应的关闭语句,字节码可以说明这一点:

private static void testCleanUp() throws IOException {
    ClassPathResource classPathResource = new ClassPathResource("application.properties");
    InputStream inputStream = classPathResource.getInputStream();

    try {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));

        while(true) {
            String line = bufferedReader.readLine();
            if (line == null) {
                return;
            }

            System.out.println(line);
        }
    } finally {
        if (Collections.singletonList(inputStream).get(0) != null) {
            inputStream.close();
        }

    }
}

我不清楚为什么finally中使用了Collections.singletonList而非直接的inputStream,有清楚的朋友可以在下面留言。

一般来说,资源的关闭方法都会使用close命名,但如果不是,我们也可以通过@Cleanupvalue属性进行指定。

假设我们自定义一个关闭方法是destroyBufferedReader

public class MyBufferedReader {
    private BufferedReader bufferedReader;

    public MyBufferedReader(Reader reader) {
        bufferedReader = new BufferedReader(reader);
    }

    public void destroy() throws IOException {
        bufferedReader.close();
    }

    public String readLine() throws IOException {
        return bufferedReader.readLine();
    }
}

@Cleanup来关闭相应的资源:

@Cleanup("destroy") MyBufferedReader bufferedReader = new MyBufferedReader(new InputStreamReader(inputStream));

生成的字节码:

bufferedReader.destroy();

@Cleanup存在一个潜在问题——如果字节码中的try块中出现异常,且finally中(对应关闭方法)也出现异常,那么前边的异常会被后边的异常“吞掉”。

比如上边的示例,我们强制让readLinedestroy都抛出异常:

public class MyBufferedReader {
	// ...
    public void destroy() throws IOException {
        throw new RuntimeException("destory is called");
    }

    public String readLine() throws IOException {
        throw new RuntimeException("readLine is called");
    }
}

最后我们只会得到destory调用时产生的异常,readLine调用时产生的异常被“吞掉”了。

这可能与使用@Cleanup的一般性预期不符,但目前因为Java语义的关系无法解决,相应的详细说明可以看@Cleanup (projectlombok.org)。

@Getter 和 @Setter

可以借助 Lombok 的@Getter@Setter注解生成属性的 getter 和 setter。

最简单的方式是直接在属性上使用,生成对应的 getter 和 setter:

public class User {
    @Getter
    @Setter
    private Long id;
    @Getter
    @Setter
    private String name;
    @Getter
    @Setter
    private Boolean isAdmin;
    @Getter
    @Setter
    private boolean delFlag;
}

对应的字节码:

public class User {
    private Long id;
    private String name;
    private Boolean isAdmin;
    private boolean delFlag;

    public User() {
    }

    public Long getId() {
        return this.id;
    }

    public void setId(final Long id) {
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public void setName(final String name) {
        this.name = name;
    }

    public Boolean getIsAdmin() {
        return this.isAdmin;
    }

    public void setIsAdmin(final Boolean isAdmin) {
        this.isAdmin = isAdmin;
    }

    public boolean isDelFlag() {
        return this.delFlag;
    }

    public void setDelFlag(final boolean delFlag) {
        this.delFlag = delFlag;
    }
}

注意,对一般性的属性,生成的getter命名是getXXX,但如果其类型是boolean(不是Boolean),其命名是isXXX

修改访问权限

默认情况下生成的 Getter 和 Setter 的访问标识符都是public,可以通过@Getter@Settervalue属性修改:

public class User {
    @Getter
    @Setter(AccessLevel.NONE)
    private Long id;
    @Getter
    @Setter(AccessLevel.PRIVATE)
    private String name;
    @Getter
    @Setter(AccessLevel.PACKAGE)
    private Boolean isAdmin;
    @Getter
    @Setter(AccessLevel.PROTECTED)
    private boolean delFlag;
}

对应的字节码:

public class User {
	// ...
    private void setName(final String name) {
        this.name = name;
    }
	// ...
    void setIsAdmin(final Boolean isAdmin) {
        this.isAdmin = isAdmin;
    }
	// ...
    protected void setDelFlag(final boolean delFlag) {
        this.delFlag = delFlag;
    }
}

AccessLevel枚举对应的用途:

  • AccessLevel.NONE:不会生成对应的 Getter 或 Setter。
  • AccessLevel.PRIVATE:生成的 Getter 或 Setter 对应的访问修饰符是private
  • AccessLevel.PACKAGE:生成的 Getter 或 Setter 对应拥有包访问权限(即没有访问修饰符)。
  • AccessLevel.PROTECTED:生成的 Getter 或 Setter 对应的访问修饰符是protected

可以在类上使用@Getter@Setter,相当于对所有属性都使用。比如上边的示例可以改写为:

@Getter
@Setter
public class User {
    @Setter(AccessLevel.NONE)
    private Long id;
    @Setter(AccessLevel.PRIVATE)
    private String name;
    @Setter(AccessLevel.PACKAGE)
    private Boolean isAdmin;
    @Setter(AccessLevel.PROTECTED)
    private boolean delFlag;
}

对应的字节码与之前的示例完全一致。

可以看到,在类上使用的@Getter@Setter可以被属性上使用的@Getter@Setter的设置覆盖。

Setter 的级联调用

默认情况下用@Setter生成的 Setter 返回的是void,所以不能用于“级联调用”,如果需要,可以用@Accessors注解来实现Setter的级联调用:

@Getter
@Setter
@Accessors(chain = true)
public class User {
    private Long id;
    private String name;
    private Boolean isAdmin;
    private boolean delFlag;
}

这里设置了@Accessors的属性chain=true,现在生成的字节码中 Setter 将返回this,而不是void

public class User {
	// ...
    public User setId(final Long id) {
        this.id = var1;
        return this;
    }
    // ...
}

所以可以用级联调用的方式使用 Setter:

User user = new User()
        .setId(1L)
        .setDelFlag(false)
        .setIsAdmin(true)
        .setName("icexmoon");
System.out.println(user.getName());

fluent

默认情况下 Lombok 生成的 Setter 命名都是setXXX, 生成的 Getter 命名都是getXXXisXXX,如果想要更简洁的命名,比如直接用属性名,可以这样:

@Getter
@Setter
@Accessors(fluent = true, chain = true)
public class Publisher {
    private Long id;
    private String name;
    private LocalDate createDate;
}

通过设置@Accessors的属性fluent=ture,可以让 Lombok 生成的Setter 和 Getter 使用简洁的命名。

对应的字节码:

public class Publisher {
	// ...
    public Long id() {
        return this.id;
    }

    public Publisher id(final Long id) {
        this.id = id;
        return this;
    }
	// ...
}

相应的调用示例:

private static void testAccessor2() {
    Publisher publisher = new Publisher()
            .id(1L)
            .name("海南出版社")
            .createDate(LocalDate.of(1991, 10, 1));
    System.out.println(publisher.name());
}

boolean 属性

从很早以前我学习 Java 开始,我就习惯于将boolean属性命名为isXXX,但如果使用 Lombok,这就可能会产生一些潜在问题,比如:

@Getter
public class BoolExample {
    private boolean isVal1;
}

这里的属性名为isVal1,类型是boolean,按照前边所说,生成的Getter应该是isIsVal1(),这样命名多少有些古怪,实际上 Lombok 会考虑这样的问题,所以生成的真实的字节码是:

public class BoolExample {
    private boolean isVal1;

    public BoolExample() {
    }

    public boolean isVal1() {
        return this.isVal1;
    }
}

可以看到,对于命名为isXXXboolean属性,Lombok 生成的 Getter 会命名为isXXX

乍一看这样并没有说明问题,但如果这样:

@Getter
public class BoolExample {
    private boolean isVal1;
    private boolean val1;
}

按照已经说过的规则,isVal1对应的Getter应该是isVal1,但val1对应的Getter也应该命名为isVal1,这无疑会产生冲突,实际上最后生成的字节码是:

public class BoolExample {
    private boolean isVal1;
    private boolean val1;

    public BoolExample() {
    }

    public boolean isVal1() {
        return this.isVal1;
    }
}

可以看到,val1属性的Getter并没有生成。

所以,最好在Java中不要将boolBoolean类型的属性命名为isXXX

@ToString

使用@ToString可以让 Lombok 自动生成toString方法:

@Getter
@Setter
@Accessors(fluent = true, chain = true)
@ToString
public class Publisher {
    private Long id;
    private String name;
    private LocalDate createDate;
}
System.out.println(publisher);

输出:

Publisher(id=1, name=海南出版社, createDate=1991-10-01)

默认的输出包含类名、属性名和属性值。

exclude

如果不需要输出属性名,可以:

@ToString(includeFieldNames = false)

输出:

Publisher(1, 海南出版社, 1991-10-01)

如果你不希望打印某些属性,可以:

@ToString(includeFieldNames = false, exclude = {"id"})

输出:

Publisher(海南出版社, 1991-10-01)

也可以在不希望输出的属性上使用@ToString.Exclude注解,效果和上边的等同。比如:

@ToString(includeFieldNames = false)
public class Publisher {
    @ToString.Exclude
    private Long id;
	// ...
}

include

如果你只希望输出某些属性,可以:

@ToString(includeFieldNames = false, onlyExplicitlyIncluded = true)
public class Publisher {
    private Long id;
    @ToString.Include
    private String name;
    @ToString.Include
    private LocalDate createDate;
}

现在toString方法只会输出namecreateDate属性,如果有新加入的属性,也不会输出。

callSuper

默认情况下 Lombok 生成的toString方法并不会调用父类的toString方法,比如:

@Setter
@Getter
@Accessors(chain = true, fluent = true)
@ToString
public class SpecialPublisher extends Publisher{
    private String admin;
}

测试:

private static void testToString() {
    Publisher publisher = new SpecialPublisher()
        .admin("icexmoon")
        .name("海南出版社")
        .id(1L)
        .createDate(LocalDate.of(1991, 10, 1));
    System.out.println(publisher);
}

输出:

SpecialPublisher(admin=icexmoon)

输出只包含了子类SpecialPublisher中的属性。

如果需要包含父类的输出,可以:

@ToString(callSuper = true)
public class SpecialPublisher extends Publisher{
    private String admin;
}

输出:

SpecialPublisher(super=Publisher(海南出版社, 1991-10-01), admin=icexmoon)

输出方法返回值

如果希望 Lombok 生成的toString方法输出中包含某些方法的返回值,可以:

@ToString(callSuper = true)
public class SpecialPublisher extends Publisher {
    private String admin;

    @ToString.Include
    private String hello() {
        return "欢迎来到" + this.name() + "出版社";
    }
}

输出:

SpecialPublisher(super=Publisher(海南出版社, 1991-10-01), admin=icexmoon, hello=欢迎来到海南出版社出版社)

最后的输出中包含了@ToString.Include标记的方法的返回值。需要注意的是,用于输出的方法不能是静态(static)的,且不能包含任何参数(空参数列表)。

属性展示名称

可以用@ToString.Includename属性修改toString输出时的属性名称:

@ToString
public class Publisher {
    @ToString.Include(name = "编号")
    private Long id;
    @ToString.Include(name = "出版社名称")
    private String name;
    @ToString.Include(name = "创建时间")
    private LocalDate createDate;
}

输出:

Publisher(编号=1, 出版社名称=海南出版社, 创建时间=1991-10-01)

排序

可以用@ToString.Includerank属性修改toString输出属性的顺序:

@ToString
public class Publisher {
    @ToString.Include(name = "编号")
    private Long id;
    @ToString.Include(name = "出版社名称", rank = 100)
    private String name;
    @ToString.Include(name = "创建时间", rank = 99)
    private LocalDate createDate;
}

输出:

Publisher(出版社名称=海南出版社, 创建时间=1991-10-01, 编号=1)

rank越大,在输出时越靠前。默认情况下rank0,且rank可以为负数。

@EqualsAndHashCode

可用注解@EqualsAndHashCode生成equalshashCode方法:

@EqualsAndHashCode
public class Publisher {
    @ToString.Include(name = "编号")
    private Long id;
    @ToString.Include(name = "出版社名称", rank = 100)
    private String name;
    @ToString.Include(name = "创建时间", rank = 99)
    private LocalDate createDate;
}

生成的字节码:

public class Publisher {
	// ...
    public boolean equals(final Object o) {
    	// ...
    }
    public int hashCode() {
    	// ...
    }
}

equalshashCode的详细代码可以下载文末的完整示例后自己编译查看。

Include

默认情况下生成的equalshashCode会使用所有的非static属性,换言之,调用equals方法进行比较时,所有属性都相等才能返回true

有时候我们仅希望比较某些作为“主键”的属性,比如:

@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class Publisher {
    @EqualsAndHashCode.Include
    @ToString.Include(name = "编号")
    private Long id;
    @ToString.Include(name = "出版社名称", rank = 100)
    private String name;
    @ToString.Include(name = "创建时间", rank = 99)
    private LocalDate createDate;
}

现在只要id属性相等,两个Publisher对象就相等(equals返回true)。

Exclude

@ToString类似,也可以使用“排除模式”:

@EqualsAndHashCode
public class Publisher {
    @ToString.Include(name = "编号")
    private Long id;
    @ToString.Include(name = "出版社名称", rank = 100)
    @EqualsAndHashCode.Exclude
    private String name;
    @ToString.Include(name = "创建时间", rank = 99)
    @EqualsAndHashCode.Exclude
    private LocalDate createDate;
}

callSuper

如果要将@EqualsAndHashCode应用于子类,通常需要考虑父类的equalshashCode方法,这可以用@EqualsAndHashCodecallSuper属性实现:

@EqualsAndHashCode(callSuper = true)
public class SpecialPublisher extends Publisher {
    @EqualsAndHashCode.Exclude
    private String admin;

    @ToString.Include
    private String hello() {
        return "欢迎来到" + this.name() + "出版社";
    }
}

在这个示例中,我们仅希望用Publisher.id这个属性来作为比较和生成哈希值的依据,所以子类的admin属性也被我们排除了。

生成构造器

Lombok 提供一些注解用于自动生成构造器:

  • @NoArgsConstructor
  • @RequiredArgsConstructor
  • @AllArgsConstructor

@NoArgsConstructor

@NoArgsConstructor可以生成一个空的构造器:

@NoArgsConstructor
public class User {
    private Long id;
    private String name;
    private Boolean isAdmin;
    private boolean delFlag;
}

字节码:

public class User {
	public User() {
    }
}

如果有final属性,这样做会导致一个编译错误:

@NoArgsConstructor
public class User {
    private final Long id;
    private String name;
    private Boolean isAdmin;
    private boolean delFlag;
}

错误信息:

java: 可能尚未初始化变量id

这时候可以:

@NoArgsConstructor(force = true)

Lombok 生成的字节码中会将final属性用零值强制初始化:

public class User {
    private final Long id = null;
	// ...
}

不过这样做似乎没有什么意义,且可能造成潜在bug,所以尽量还是不要这么做。

@RequiredArgsConstructor

@RequiredArgsConstructor可以为“需要的属性”生成一个用于初始化的构造器。

这里“需要的属性”,指用final@NonNull修饰且没有被初始化的属性。

示例:

@RequiredArgsConstructor
public class User {
    private final Long id;
    @NonNull
    private String name;
    @NonNull
    private Boolean isAdmin = false;
    private boolean delFlag;
}

字节码:

public class User {
	// ...
    public User(final Long id, @NonNull final String name) {
        if (name == null) {
            throw new NullPointerException("name is marked non-null but is null");
        } else {
            this.id = id;
            this.name = name;
        }
    }
}

构造器中也会加入对@NonNull字段null检查的if语句,这点在之前的@NonNull中有过介绍。

@AllArgsConstructor

@AllArgsConstructor会为所有属性生成一个构造器:

@AllArgsConstructor
public class User {
    private final Long id;
    @NonNull
    private String name;
    @NonNull
    private Boolean isAdmin = false;
    private boolean delFlag;
}

字节码:

public class User {
	// ...
    public User(final Long id, @NonNull final String name, @NonNull final Boolean isAdmin, final boolean delFlag) {
        if (name == null) {
            throw new NullPointerException("name is marked non-null but is null");
        } else if (isAdmin == null) {
            throw new NullPointerException("isAdmin is marked non-null but is null");
        } else {
            this.id = id;
            this.name = name;
            this.isAdmin = isAdmin;
            this.delFlag = delFlag;
        }
    }
}

staticName

上边的构造器都提供另外一种形式——将构造器本身定义为private,并提供一个static方法进行调用。

比如下面的示例:

@AllArgsConstructor(staticName = "of")
public class User {
    private final Long id;
    @NonNull
    private String name;
    @NonNull
    private Boolean isAdmin = false;
    private boolean delFlag;
}

对应的字节码:

public class User {
	// ...
    private User(final Long id, @NonNull final String name, @NonNull final Boolean isAdmin, final boolean delFlag) {
    	// ...
    }

    public static User of(final Long id, @NonNull final String name, @NonNull final Boolean isAdmin, final boolean delFlag) {
        return new User(id, name, isAdmin, delFlag);
    }
}

@Data

@Data注解相当于同时使用了以下注解:

  • @Setter
  • @Getter
  • @RequiredArgsConstructor
  • @ToString
  • @EqualsAndHashCode

比如下面的示例:

@Setter
@Getter
@RequiredArgsConstructor
@ToString
@EqualsAndHashCode
public class Employee {
    private final Long id;
    @NonNull
    private String name;
    @NonNull
    private Boolean delFlag;
}

和下面的是等效的:

@Data
public class Employee {
    private final Long id;
    @NonNull
    private String name;
    @NonNull
    private Boolean delFlag;
}

实际上@Data通常用来为实体类(POJO)提供基本的构造器、Setter和Getter,以及equalshashCode方法。

如果我们需要为某个注解提供更详细的设置,比如将Employeeid视作主键,用于比较和生成哈希值,以及输出的toString方法不包含键名和delFlag,可以在使用@Data注解的基础上使用对应的注解来设置:

@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString(includeFieldNames = false)
public class Employee {
    @EqualsAndHashCode.Include
    private final Long id;
    @NonNull
    private String name;
    @NonNull
    @ToString.Exclude
    private Boolean delFlag;
}

staticConstructor

类似于@AllArgsConstructor等,@Data同样可以将构造器设置为私有的,同时提供一个static方法用于调用构造器,比如:

@Data
public class Student<T> {
    private final Long id;
    @NonNull
    private String name;
    @NonNull
    private Integer age;
    @NonNull
    private T something;
}

需要用以下方式创建对象:

Student<String> s = new Student<>(1L, "icexmoon", 20, "hello");

可以修改为:

@Data(staticConstructor = "of")
public class Student<T> {
    private final Long id;
    @NonNull
    private String name;
    @NonNull
    private Integer age;
    @NonNull
    private T something;
}

此时这样调用:

Student<String> s = Student.of(1L, "icexmoon", 20, "hello");

因为静态方法会通过传入参数的类型来确定泛型参数,所以在使用Student.of时并不需要指定方法的泛型参数。

@Value

@Value可以创建一些“只读”性质的类型:

@Value
public class BookCategory {
    Long id;
    String name;
    String desc;
}

对应的字节码:

public final class BookCategory {
    private final Long id;
    private final String name;
    private final String desc;

    public BookCategory(final Long id, final String name, final String desc) {
        this.id = id;
        this.name = name;
        this.desc = desc;
    }

    public Long getId() {
        return this.id;
    }

    public String getName() {
        return this.name;
    }

    public String getDesc() {
        return this.desc;
    }

    public boolean equals(final Object o) {
        // ...
    }

    public int hashCode() {
       	// ...
    }

    public String toString() {
        // ...
    }
}

可以看到,在字节码中,BookCategory的非static属性被private final修饰,且只生成了Getter,没有生成Setter。所以BookCategory的属性只能在生成的构造器中被初始化,且不能通过其他方式修改。

此外,BookCategory本身也被final修饰,也就是说被@Value标记的类不能被继承。

所有上边这些特性,都标识——被@Value标记的类可以作为一个只读的“数据类”来使用。

等效写法

实际上@Value相当于下面的写法:

@ToString
@EqualsAndHashCode
@AllArgsConstructor
@Getter
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public final class BookCategory {
    Long id;
    String name;
    String desc;
}

示例

通常,在进行Web编程时,我们可以利用@Value来创建DTO,因为这些DTO类用于传递数据,他们的属性在初始化后就不应该被修改。

比如下面这个示例:

@Value
public class BookCategory {
    Long id;
    String name;
    String desc;

    public static BookCategory newInstance(BookCategoryController.AddCategoryDTO dto) {
        return new BookCategory(null, dto.getName(), dto.getDesc());
    }
}

@Value
public class Result<T> {
    boolean success;
    String errorCode;
    String errorMsg;
    T data;

    public static <T> Result<T> success(T data) {
        return new Result<T>(true, "", "", data);
    }

    public static Result<Object> success() {
        return success(null);
    }
}

@RestController
@RequestMapping("/book/category")
public class BookCategoryController {
    @Value
    public static class AddCategoryDTO {
        @NotBlank String name;
        @NotBlank String desc;
    }

    @PostMapping
    public Result<Object> addCategory(@Validated @RequestBody AddCategoryDTO addCategoryDTO) {
        System.out.println(addCategoryDTO);
        //用DTO生成POJO
        BookCategory bookCategory = BookCategory.newInstance(addCategoryDTO);
        System.out.println(bookCategory);
        //用POJO在持久层添加新的图书类别
        //这里省略持久层调用
        return Result.success();
    }
}

这里充当POJO的BookCategory、充当DTO的AddCategoryDTO,以及用于标准化返回的Result,都用@Value标识。因为这些类实际上都充当了传递数据的角色,并不涉及会改变内部属性的复杂业务逻辑。

事实上这些用于简单传递数据的类,从Java 10开始,可以用标准库的Record来实现,这点在之后的文章说明。

@Builder

利用@Builder可以为类创建一个“创建器”,利用这个创建器可以创建对象。

比如下面这个示例:

@Builder
@Getter
@ToString
public class Person {
    private final String name;
    private final String city;
    private final String job;
}

生成的字节码如下:

public class Person {
    private final String name;
    private final String city;
    private final String job;

    Person(final String name, final String city, final String job) {
        this.name = name;
        this.city = city;
        this.job = job;
    }

    public static Person.PersonBuilder builder() {
        return new Person.PersonBuilder();
    }

    // ... 这里是一些Getter 和 toString ...

    public static class PersonBuilder {
        private String name;
        private String city;
        private String job;

        PersonBuilder() {
        }

        public Person.PersonBuilder name(final String name) {
            this.name = name;
            return this;
        }

        public Person.PersonBuilder city(final String city) {
            this.city = city;
            return this;
        }

        public Person.PersonBuilder job(final String job) {
            this.job = job;
            return this;
        }

        public Person build() {
            return new Person(this.name, this.city, this.job);
        }

        public String toString() {
            return "Person.PersonBuilder(name=" + this.name + ", city=" + this.city + ", job=" + this.job + ")";
        }
    }
}

@Builder会为类创建一个包含所有非静态属性的构造器和一个静态的内嵌类xxxBuilder,这个内嵌类包含所有外部类的非静态属性,并且可以利用这个内嵌类的一系列方法来一步步生成外部类的对象。

比如下面这样:

private static void testBuilder() {
    var p = Person.builder().name("icexmoon")
    .city("NanJin")
    .job("Programmer")
    .build();
    System.out.println(p);
}

这样做的好处在于,虽然外部类Person的属性都是final的,并且只有Getter没有Setter,但是我们可以借助内部类PersonBuilder来灵活地设置属性和生成对象。这在我们想用一个“只读”类,但是又不想用死板的构造器一次性初始化的情况下会格外有用。

要注意,生成的内嵌类xxxBuilder仅会为外部类未初始化的属性添加对应的内嵌类属性,并生成对应的内嵌类的Setter方法,并最终用于构建外部类对象。外部类被显式初始化的属性不在此列。

@Singular

默认情况下容器类型的属性的处理与其他属性一致,比如:

@Builder
@Getter
@ToString
public class Person {
    private final String name;
    private final String city;
    private final String job;
    private final List<String> hobbies;
}

调用示例:

private static void testBuilder() {
    var p = Person.builder().name("icexmoon")
        .city("NanJin")
        .job("Programmer")
        .hobbies(List.of("play games", "travel"))
        .build();
    System.out.println(p);
}

这里PersonBuilder.hobbies仅是简单地用传入的List作为最终的外部类对象的hobbies属性。

@Builder还提供一种模式:

@Builder
@Getter
@ToString
public class Person {
    private final String name;
    private final String city;
    private final String job;
    @Singular
    private List<String> hobbies;
}

注意,这里的hobbies没有被final修饰,实际测试时如果有final,就无法生成PersonBuilder.hobbies等相关的Setter方法,不知道是不是Bug。

生成的字节码:

// ...
public class Person {
    // ...
    public static class PersonBuilder {
		// ...
        public Person.PersonBuilder hobby(final String hobby) {
            if (this.hobbies == null) {
                this.hobbies = new ArrayList();
            }

            this.hobbies.add(hobby);
            return this;
        }

        public Person.PersonBuilder hobbies(final Collection<? extends String> hobbies) {
            if (hobbies == null) {
                throw new NullPointerException("hobbies cannot be null");
            } else {
                if (this.hobbies == null) {
                    this.hobbies = new ArrayList();
                }

                this.hobbies.addAll(hobbies);
                return this;
            }
        }
		// ...
    }
}

可以看到,PersonBuilder.hobbies的行为改变了,变成用传入的List与已有List合并,此外还有一个新的PersonBuilder.hobby方法,可以用这个方法逐一向List添加元素。

调用示例:

private static void testBuilder() {
    var p = Person.builder().name("icexmoon")
        .city("NanJin")
        .job("Programmer")
        .hobbies(List.of("play games", "travel"))
        .hobbies(List.of("draw"))
        .hobby("music")
        .hobby("movie")
        .build();
    System.out.println(p);
}

@Value

@Builder可以和@Value一同使用,比如之前的示例可以改写为:

@Builder
@Value
public class Person {
    String name;
    String city;
    String job;
    @Singular
    List<String> hobbies;
}

要注意的是,@Value会生成一个包含了所有属性的public构造器,而@Builder会生成一个包含所有属性的包访问权限的构造器,两者会发生冲突,此时后者会产生而前者不会。

对应的字节码和调用示例与之前的几乎一致,这里不再展示。

奇怪的是这里@Value会让hobbies变成final的,但是依然可以正常生成PersonBuilder.hobbies。只能认为之前的是个Bug。

@Builder.Default

可以用@Builder.DefaultBuilder构建外部类时提供默认值(如果没有设置相应的值的话):

@Builder
@Value
public class Person {
    String name;
    String city;
    String job;
    @Singular
    List<String> hobbies;
    @Builder.Default
    LocalDateTime createTime = LocalDateTime.now();
}

对应的字节码:

public final class Person {
	// ...
    private final LocalDateTime createTime;

    private static LocalDateTime $default$createTime() {
        return LocalDateTime.now();
    }
    // ...
    public static class PersonBuilder {
		// ...
        private boolean createTime$set;
        private LocalDateTime createTime$value;
        
        public Person.PersonBuilder createTime(final LocalDateTime createTime) {
            this.createTime$value = createTime;
            this.createTime$set = true;
            return this;
        }
        
        public Person build() {
         	// ...
            LocalDateTime createTime$value = this.createTime$value;
            if (!this.createTime$set) {
                createTime$value = Person.$default$createTime();
            }
            return new Person(this.name, this.city, this.job, hobbies, createTime$value);
        }
    }
}

@SneakyThrows

如果我们的代码中包含方法声明中有throws指明会抛出一个“被检查异常”的代码,那我们只有两种解决方式:

  • 在当前方法声明中添加throws语句,指明当前方法也可能抛出该类型的“被检查异常”。
  • try...catch捕获该异常,并处理(通常是将其包装成一个RuntimeException并抛出。

之所以Java会这样设计,是因为早期Java的设计者认为某些异常必须要被调用放显式处理才行。但实际运用中,一层层调用过程中都要抛出一个“被检查异常”是相当繁琐的,且必须在每一层方法声明中都添加对应的throws语句,所以将异常转化成RuntimeException并抛出的解决方案使用频率反而更多。

但是,这种方式需要我们编写一些额外代码(try...catch语句),因此 Lombok 提供一个@SneakyThrows注解,可以帮助我们更简单的实现一个替代解决方案,并只需要添加一个注解。

看下面这个示例:

private static void callTestThrow() throws IOException{
    testThrow();
}

private static void testThrow() throws IOException{
    @Cleanup InputStream inputStream = new ClassPathResource("application.properties").getInputStream();
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
    while (true){
        String line = bufferedReader.readLine();
        if (line == null){
            break;
        }
        System.out.println(line);
    }
}

创建输入流的相关代码可能会产生一个被检查异常IOException,因此我们需要在testThrow方法声明中添加throws语句,这是Java语法强制要求的。并且,调用该方法的其他方法,比如callTestThrow,同样需要处理这个被检查异常。

当然我们可以利用try...catch将其转换为“非检查异常”:

private static void callTestThrow() {
    testThrow();
}

private static void testThrow() {
    try {
        @Cleanup InputStream inputStream = new ClassPathResource("application.properties").getInputStream();
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        while (true) {
            String line = bufferedReader.readLine();
            if (line == null) {
                break;
            }
            System.out.println(line);
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

@SneakyThrows给了我们第三种选择:

private static void callTestThrow() {
    testThrow();
}

@SneakyThrows(IOException.class)
private static void testThrow() {
    @Cleanup InputStream inputStream = new ClassPathResource("application.properties").getInputStream();
    BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
    while (true) {
        String line = bufferedReader.readLine();
        if (line == null) {
            break;
        }
        System.out.println(line);
    }
}

可以看到,@SneakyThrows(IOException.class)的效果与使用try...catch转换异常是一样的,调用方同样不需要显式处理异常。

我们看对应的字节码:

private static void callTestThrow() {
    testThrow();
}

private static void testThrow() {
    try {
        // ...
    } catch (IOException var7) {
        throw var7;
    }
}

这样看起来很奇怪,testThrow捕获了IOException异常并原样抛出,并且callTestThrow中也没有处理这个“被检查异常”,这样并不符合Java语法。

Lombok 官方文档对此的说法是 Lombok 通过某种方式在JVM层面“欺骗”了编译器,所以可以实现类似的效果。

  • 官方文档中的示例对应的字节码实现与这里我实际测试中产生的字节码有出入(throw Lombok.sneakyThrow(t)),原因不明。
  • Sneaky 一词在英语中有“悄悄地”意思,因此@SneakyThrows的用途可以被理解为“悄悄地抛出一个被检查异常”。
  • 关于Java异常的更多内容,可以阅读Java编程笔记10:异常 - 红茶的个人站点 (icexmoon.cn)。

@Synchronized

在Java中,可以通过synchronized给方法调用“加锁”,并且这种方式可以和用synchronized语句块用this作为临界区的写法是可以协同工作的,比如:

public class ShareData {
    public synchronized void func1() {
        for (int i = 0; i < 5; i++) {
            System.out.println("func1() is called.");
            Thread.yield();
        }
    }

    public void func2() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println("func2() is called.");
                Thread.yield();
            }
        }
    }
}

@SpringBootApplication
public class LombokApplication {
    // ...
    private static void testSyncronize() {
        var sd = new ShareData();
        new Thread(() -> sd.func1()).start();
        new Thread(() -> sd.func2()).start();
    }
}

输出:

func1() is called.
func1() is called.
func1() is called.
func1() is called.
func1() is called.
func2() is called.
func2() is called.
func2() is called.
func2() is called.
func2() is called.

这是一种特性,但有时候你或许不希望使用它。比如你可能担心某些用this作为synchronized(...){}语句临界区的代码其本意并非是与synchronized方法互斥。

这种问题可以通过使用@Synchronized注解来解决,比如:

public class ShareData {
    @Synchronized
    public void func1() {
        for (int i = 0; i < 5; i++) {
            System.out.println("func1() is called.");
            Thread.yield();
        }
    }

    public void func2() {
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println("func2() is called.");
                Thread.yield();
            }
        }
    }
}

输出:

func1() is called.
func2() is called.
func2() is called.
func2() is called.
func2() is called.
func1() is called.
func1() is called.
func1() is called.
func1() is called.
func2() is called.

可以看到func1func2的相关代码实际上是并行的,并非互斥。

对应的字节码:

public class ShareData {
    private final Object $lock = new Object[0];

    public ShareData() {
    }

    public void func1() {
        synchronized(this.$lock) {
            for(int i = 0; i < 5; ++i) {
                System.out.println("func1() is called.");
                Thread.yield();
            }
        }
    }

    public void func2() {
        synchronized(this) {
            for(int i = 0; i < 5; ++i) {
                System.out.println("func2() is called.");
                Thread.yield();
            }
        }
    }
}

可以看到,实际上@Synchronized同样是使用synchronized(...){}语句实现的,不过临界快并非使用的this,而是 Lombok 自己添加的静态属性$lock。因此,和使用this作为临界区的synchronized块并不互斥。

更多synchronized和并发内容可以阅读Java学习笔记21:并发(1) - 红茶的个人站点 (icexmoon.cn)。

如果对静态方法使用@Synchronized,Lombok 会创建一个$LOCK属性作为临界区:

public class ShareData {
	// ...
	@Synchronized
    public static void func3(){
        for (int i = 0; i < 5; i++) {
            System.out.println("func3() is called.");
            Thread.yield();
        }
    }
}

对应的字节码:

public class ShareData {
	// ...
	private static final Object $LOCK = new Object[0];
	// ...
    public static void func3() {
        synchronized($LOCK) {
            for(int i = 0; i < 5; ++i) {
                System.out.println("func3() is called.");
                Thread.yield();
            }
        }
    }
}
  • 值得注意的是,作为临界区的$lock$LOCK都是new Object[0](即一个元素类型为Object且长度为0的数组),而不是一般会使用的new Object。这样做的好处是前者是可以序列化的,而后者不行。而序列化的时候必须确保所有属性都可以被序列化,因此前者不会阻止所在的类变成一个可序列化的类(implements Serializable),而后者会,所以使用前者会更好一些。
  • 关于序列化的更多内容可以阅读Java编程笔记18:I/O(续) - 红茶的个人站点 (icexmoon.cn)中的序列化部分。

指定临界区

使用@Synchronized时也可以自己指定一个属性作为临界区,比如:

public class ShareData {
    private final Object lock1 = new Object[0];
    private static final Object lock2 = new Object[0];

    @Synchronized("lock1")
    public void func1() {
        //...
    }
    //...
    @Synchronized("lock2")
    public static void func3(){
        //...
    }
}

对应的字节码:

public class ShareData {
    private final Object lock1 = new Object[0];
    private static final Object lock2 = new Object[0];
    public void func1() {
        synchronized(this.lock1) {
			// ...
        }
    }
    // ...
    public static void func3() {
        synchronized(lock2) {
            // ...
        }
    }
}

可以看到,此时 Lombok 不会再添加$lock$LOCK,而是使用指定的属性作为synchronized块的临界区。

@With

有时候虽然是一个“只读”的类,我们依然希望修改其中的某个属性,比如:

@Value
public class Dog {
    String name;
    Integer age;
}

Dog的属性都是private final的,显然它也不可能有setter。所以正常情况下我们是没法修改其中的属性的,但是我们可以选择创建一个新的和对象,只不过该对象中的属性都与原来对象一致,除了一个我们想变更的属性。比如下面的示例:

private static void testWith() {
    Dog dog = new Dog("audi",11);
    Dog dog2 = new Dog(dog.getName(), 2);
    System.out.println(dog);
    System.out.println(dog2);
}

不过上面的写法多少有点冗余,这时候自然是 Lombok 派上用场的时候了:

@Value
@With
public class Dog {
    String name;
    Integer age;
}

对应的字节码:

public final class Dog {
    // ...
	public Dog withName(final String name) {
        return this.name == name ? this : new Dog(name, this.age);
    }

    public Dog withAge(final Integer age) {
        return this.age == age ? this : new Dog(this.name, age);
    }
}
  • 要注意的是,Lombok 生成的withXXX方法的处理逻辑是用==(不是equals)比较属性值,如果与原始值一样,就返回原始对象(this),否则创建新对象。
  • withXXX方法是用构造器创建新对象,因此@With标记的类必须有一个包含所有属性的构造器(可以用@AllArgsConstructor创建)。

此时上面的调用示例就可以改写为:

private static void testWith() {
    Dog dog = new Dog("audi",11);
    Dog dog2 = dog.withAge(2);
    System.out.println(dog);
    System.out.println(dog2);
}

只有两个属性的Dog并不能说明便利性,但假如属性很多,使用@With就会省很多事。

特定属性

可以只对特定属性生成withXXX方法而非所有属性:

@Value
public class Dog {
    String name;
    @With
    Integer age;
}

生成的字节码中只会有withAge方法,而不会有withName方法。

访问权限

默认情况下@With生成的withXXX方法的访问权限是public,也可以指定其他访问权限,比如:

@Value
public class Dog {
    String name;
    @With(AccessLevel.PACKAGE)
    Integer age;
}

此时生成的withAge是包访问权限。

@Getter(lazy=true)

有时候,对于final属性,会在声明时进行一些复杂(消耗时间)的初始化工作,比如:

@Getter
public class LazyExample {
    private final long bigFibnacci = fibonacci(30);

    private static long fibonacci(int n) {
        if (n <= 2) {
            return 1;
        }
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

如果对这个属性的使用并不是在对象创建后立即进行,我们可以将这种初始化动作延后,以减少对象创建时所消耗的时间。比如:

public class LazyExample {

    private Long bigFibnacci;

    private static long fibonacci(int n) {
        if (n <= 2) {
            return 1;
        }
        return fibonacci(n - 1) + fibonacci(n - 2);
    }

    public Long getBigFibnacci() {
        if (bigFibnacci == null){
            bigFibnacci = fibonacci(30);
        }
        return bigFibnacci;
    }
}

这里的优化方案实际上并没有考虑到多线程调用的情况,因此是线程不安全的。

实际上 Lombok 的@Getter(lazy=true)可以帮助我们更容易地实现类似的代码:

public class LazyExample {
    @Getter(lazy = true)
    private final Long bigFibnacci = fibonacci(30);

    private static long fibonacci(int n) {
        if (n <= 2) {
            return 1;
        }
        return fibonacci(n - 1) + fibonacci(n - 2);
    }
}

对应的字节码:

public class LazyExample {
    private final AtomicReference<Object> bigFibnacci = new AtomicReference();

    public LazyExample() {
    }

    private static long fibonacci(int n) {
        return n <= 2 ? 1L : fibonacci(n - 1) + fibonacci(n - 2);
    }

    public Long getBigFibnacci() {
        Object value = this.bigFibnacci.get();
        if (value == null) {
            synchronized(this.bigFibnacci) {
                value = this.bigFibnacci.get();
                if (value == null) {
                    Long actualValue = fibonacci(30);
                    value = actualValue == null ? this.bigFibnacci : actualValue;
                    this.bigFibnacci.set(value);
                }
            }
        }

        return (Long)(value == this.bigFibnacci ? null : value);
    }
}

可以看到,Lombok 自动帮助我们实现了类似的代码,且使用了原子操作的相关类AtomicReference,以及synchronized语句,所以用@Getter(lazy=true)实现的类似优化(延迟初始化)是可以用于多线程的,是线程安全的。

潜在问题

就像我们看到的,如果你用了@Getter(lazy=true),那么在类中调用该字段时就必须用getXXX获取属性值,否则你获取到的就是一个AtomicReference<Object>类型的对象,并且该对象还没有进行过“初始化”。

@Log

使用@Log注解可以更方便地输出调试信息,比如:

@RestController
@RequestMapping("/book/category")
@Log
public class BookCategoryController {
	// ...
    @PostMapping
    public Result<Object> addCategory(@Validated @RequestBody AddCategoryDTO addCategoryDTO) {
        System.out.println(addCategoryDTO);
        log.log(Level.INFO, addCategoryDTO.toString());
        // ...
    }
}

输出:

// ...
BookCategoryController.AddCategoryDTO(name=文学, desc=包括中国文学,外国文学等)
2023-06-04T10:42:03.928+08:00  INFO 17536 --- [nio-8080-exec-1] c.e.l.controller.BookCategoryController  : BookCategoryController.AddCategoryDTO(name=文学, desc=包括中国文学,外国文学等)
// ...

可以看到,通过日志输出比起直接通过System.out输出会显示更多信息,比如时间、线程编号和名称、日志级别等。

除了以上好处外,还包括:

  • 可以通过设置方便地输出到文件。
  • 可以通过设置让不同的运行环境(开发环境、测试环境等)输出不同包下不同的日志级别的日志。

当然,具体使用时还和你的Java应用使用的框架以及日志模块相关,比如 Spring Boot 默认使用 logback 作为日志模块,且支持多种方式的日志调用API,实际上上边的@Log就是导入了java.util.logging的相关日志API。这点在字节码中有体现:

package com.example.lombok.controller;

// ...
import java.util.logging.Logger;
// ...
public class BookCategoryController {
    private static final Logger log = Logger.getLogger(BookCategoryController.class.getName());
	// ...
}

当然也可以使用别的API,比如l4j2的:

@Log4j2
public class BookCategoryController {
	// ...
    @PostMapping
    public Result<Object> addCategory(@Validated @RequestBody AddCategoryDTO addCategoryDTO) {
        System.out.println(addCategoryDTO);
        log.debug(addCategoryDTO);
        // ...
    }
}

对应的字节码:

package com.example.lombok.controller;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
// ...
public class BookCategoryController {
    private static final Logger log = LogManager.getLogger(BookCategoryController.class);
    // ...
}

Spring Boot 默认不输出 DEBUG 级别日志,所以这里还需要在配置文件中添加logging.level.com.example.lombok=debug

最后,总结一下,通过使用 Lombok 日志相关注解,可以更方便的引入和调用不同日志模块的API。

  • 如果想了解其它日志模块对应的 Lombok 注解,可以阅读@Log (and friends) (projectlombok.org)。
  • 如果想了解 Spring Boot 中的日志使用,可以阅读从零开始 Spring Boot 10:日志 - 红茶的个人站点 (icexmoon.cn)和从零开始 Spring Boot 34:日志 II - 红茶的个人站点 (icexmoon.cn)。

如果想了解 Lombok 的更多用法和说明,可以前往官方文档。

The End,谢谢阅读。

本文的完整示例可以从这里获取。

参考资料

  • JEP 286: Local-Variable Type Inference (openjdk.org)
  • var (projectlombok.org)
  • @NonNull (projectlombok.org)
  • Introduction to Project Lombok
  • @Cleanup (projectlombok.org)
  • @Getter and @Setter (projectlombok.org)
  • @ToString (projectlombok.org)
  • @EqualsAndHashCode (projectlombok.org)
  • @Data (projectlombok.org)
  • Introduction to Project Lombok | Baeldung
  • @Value (projectlombok.org)
  • @Builder (projectlombok.org)
  • @Synchronized (projectlombok.org)
  • @With (projectlombok.org)
  • @Getter(lazy=true)(lazy=true) (projectlombok.org)
  • 从零开始 Spring Boot 10:日志 - 红茶的个人站点 (icexmoon.cn)
  • 从零开始 Spring Boot 34:日志 II - 红茶的个人站点 (icexmoon.cn)
  • @Log (and friends) (projectlombok.org)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/607699.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

冈萨雷斯DIP第9章知识点

文章目录 9.1 预备知识9.2 腐蚀和膨胀9.3 开运算与闭运算9.5 一些基本的形态学算法9.8 灰度级形态学 9.1 预备知识 形态学运算是用 集合 来定义的 。 在图像处理中 使用两类像素集合的形态学&#xff1a;目标元素 和 结构元 SE 。 通常目标定义为前景像素集合&#xff0c;结构…

计算机基础--->操作系统(2)【线程和进程、死锁】

文章目录 线程和进程线程和进程的区别&#xff1f;有了进程为什么还需要线程&#xff1f;为什么使用多线程&#xff1f;线程间的同步方式进程控制块&#xff08;PCB&#xff09;进程有哪几种状态&#xff1f;进程间的通讯方式有哪些&#xff1f;进程常见的调度算法有哪些&#…

冈萨雷斯DIP第2章知识点

文章目录 2.1 视觉感知要素2.2 光和电磁波谱2.3 图像感知与获取2.4 图像取样和量化2.4.1 取样和量化的基本概念2.4.2 数字图像表示2.4.4 空间分辨率和灰度分辨率2.4.5 图像内插 (image interpolation)2.5 像素间的一些基本关系 2.6 数字图像处理所用的基本数学工具介绍2.6.3 算…

基础查询 — 投影、选择

准备 数据查询操作均采用的是MySQL。示例数据库采用的是northwind 示例数据库。数据库导入手册 关于northwind 示例数据库 查询数据库中的表 show tables;查询表的表属性 desc xxx(表名);投影操作 1.语法 投影运算&#xff1a;选择表中的全部或者部分列。 语法 select 字段…

Jeston Orin Nano 离线烧写系统到NVME存储

大家好&#xff0c;我是虎哥&#xff0c;Jeston Orin nano 8G模块&#xff0c;我自己也玩了一段时间&#xff0c;在Orin 系列&#xff0c;官方提供了一种新的烧写方式&#xff0c;也就是离线烧写&#xff0c;就是你在主机&#xff0c;挂载存储后&#xff0c;直接烧写系统到这个…

Maven高级——继承与聚合——聚合实现

为什么要聚合 分模块开发之后一个项目会被拆分成多个模块。多个模块之间还会有依赖关系。 在一些大型项目中模块比较多&#xff0c;模块之间的依赖关系也会变得错综复杂。 并且在打包的时候还会有一个新的问题&#xff0c;在打包的时候我们要打包的是Springboot项目&#xf…

黑马Redis视频教程高级篇(一)

目录 分布式缓存 一、Redis持久化 1.1、RDB持久化 1.1.1、执行时机 1.1.2、RDB原理 1.1.3、小结 1.2、OF持久化 1.2.1、AOF原理 1.2.2、OF配置 1.2.3、AOF文件重写 1.3、RDB与AOF对比 二、Redis主从 2.1、搭建主从架构 2.1.1、集群结构 2.1.2、准备实例和配置 …

CBCGPRibbonBar 设置整个界面字体大小

在CMainFrame.h中添加成员变量&#xff1a;CFont m_fontCustom; 在onCreat()方法结束之前的任一个位置写下下测方法即可 方法1&#xff1a; { // Create custom font: LOGFONT lf; globalData.fontRegular.GetLogFont(&lf); lf.lfItalic TRUE; …

详解Spring Cloud版本问题

目录 1.让人头疼的多版本号体系 2.目录关系 3.为什么会有多个版本号体系 1.让人头疼的多版本号体系 由于历史原因&#xff0c;spring cloud分为了Alibaba和Netflix两个体系。 想要了解原因以及整个spring cloud体系的来龙去脉的同学可以去看我的另一篇文章&#xff1a; S…

MySQL脏读、不可重复读、幻读的区别与注意事项

目录 一、引入二、事务并发执行会遇到的问题1. 区别2. 注意 三、隔离级别四、参考资料 一、引入 MySQL的架构是 C/S 架构&#xff08;即 客户端/服务器 架构&#xff09;&#xff0c;一个服务器可能有多个客户端与之相连接&#xff0c;每个连接称之为会话&#xff08;Session&…

chatgpt赋能python:Python去掉分隔符:优化SEO效果的一种方法

Python去掉分隔符&#xff1a;优化SEO效果的一种方法 在现代的数字化时代&#xff0c;SEO已经成为了许多企业、个人和网站运营者最为关心的问题之一。SEO指的是搜索引擎优化&#xff0c;通过各种技术手段和优化方式&#xff0c;提高网站在搜索引擎结果页面上显示的排名。Pytho…

PageHelper使用

PageHelper &#xff1a; mybatis中的分页插件 文档 &#xff1a; https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md springboot使用PageHelper <dependency><groupId>com.github.pagehelper</groupId><artifactId…

利用DocsGPT快速搭建一个问答式的文档检索服务

docsGPT 示例 基于企业内部独有的知识库&#xff0c;进行智能的客服问答&#xff0c;毫无疑问是 ChatGPT 出圈以后&#xff0c;所有公司想要融入 ChatGPT 技术时的第一反应。可惜 ChatGPT 实际上是一个基于大语言模型实现的&#xff0c;包括很多其他功能的&#xff0c;完整的聊…

电力电子技术的论文

电力电子技术的论文范文一&#xff1a;Matlab电力电子技术应用 【文章摘要】信息技术的快速发展推动许多学科进一步完善&#xff0c;以电力电子技术为例&#xff0c;其本身具有较强的理论性、实践性等特征&#xff0c;涉及的波形图、电路图也较多&#xff0c;相关设计人员需掌握…

Hitcon 2016 SleepyHolder-fastbin_dup_consolidate.c

参考/题目下载&#xff1a; https://github.com/mehQQ/public_writeup/tree/master/hitcon2016/SleepyHolder https://blog.csdn.net/seaaseesa/article/details/105856878 1&#xff0c;三联 保护:基本都开了 功能&#xff1a; 0、唤醒功能&#xff1b; 1、创建-secret&#…

华为OD机试真题 Java 实现【猴子爬山】【2023 B卷 100分】,附详细解题思路

一、题目描述 一天一只顽猴想去从山脚爬到山顶&#xff0c;途中经过一个有个N个台阶的阶梯&#xff0c;但是这猴子有一个习惯&#xff1a; 每一次只能跳1步或跳3步&#xff0c;试问猴子通过这个阶梯有多少种不同的跳跃方式&#xff1f; 二、输入描述 输入只有一个整数N&…

Custom Frames插件:内置Web应用 | Obsidian实践

今天跟大家分享一个Obsidian插件&#xff1a;Custom Frames。 这个插件的美妙之处在于&#xff0c;可以将一个Obsidian外部的Web应用&#xff0c;内置到Obsidian内部来操作和使用。 这么说可能是有点儿抽象&#xff0c;就比方说吧&#xff0c;微信读书有个网页版&#xff08;后…

嵌入式开发——文件系统部署rz、sz命令

1、rz、sz命令源码下载 下载网址&#xff1a;https://ohse.de/uwe/software/lrzsz.html 2、源码编译 tar -zxvf lrzsz-0.12.20.tar.gz cd lrzsz-0.12.20 ./configure make CCaarch64-mix410-linux-gcc #指定芯片平台的交叉编译链3、源码编译遇到的问题 3.1、报错打印 (1)报错…

绝地求生可以这样制作 (Python 版)

一、概述 1.1 效果 总的来说&#xff0c;这种方式是通过图像识别来完成的&#xff0c;不侵入游戏&#xff0c;不读取内存&#xff0c;安全不被检测。 1.2 前置知识 游戏中有各种不同的枪械&#xff0c;不同的枪械后坐力不一样&#xff0c;射速也不同。相同的枪械&#xff0c;…

【Java】Java(四十八):日志

文章目录 1. 概述2. 日志体系结构和Log4J3. 入门案例4. 配置文件详解 1. 概述 程序中的日志可以用来记录程序在运行的时候点点滴滴。并可以进行永久存储。 日志与输出语句的区别 2. 日志体系结构和Log4J 体系结构 Log4J Log4j是Apache的一个开源项目。 通过使用Log4j&#…