上海网站建设做物流一,关于做营销型网站的建议,公司网站主页打不开,快速收录提交入口从零开始 Spring Boot 35#xff1a;Lombok 图源#xff1a;简书 (jianshu.com)
Lombok是一个java项目#xff0c;旨在帮助开发者减少一些“模板代码”。其具体方式是在Java代码生成字节码#xff08;class文件#xff09;时#xff0c;根据你添加的相关Lombok注解或类来…从零开始 Spring Boot 35Lombok 图源简书 (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需要在项目中添加相应的依赖
dependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdoptionaltrue/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 ArrayListString();names.add(Li Lei);System.out.println(names.get(0));var students new HashMapInteger, 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() {ArrayListString names new ArrayList();names.add(Li Lei);System.out.println((String)names.get(0));HashMapInteger, String students new HashMap();students.put(12, Li Lei);students.put(20, Han Meimei);Iterator var2 students.entrySet().iterator();while(var2.hasNext()) {EntryInteger, 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 ArrayListString();names.add(Li Lei);System.out.println(names.get(0));val students new HashMapInteger, 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 33Null-safety - 红茶的个人站点 (icexmoon.cn)中我讨论过Spring框架对Null安全的支持但那些支持都不是强制性的仅能借助IDE的相关工具在编码阶段提供一些警告信息。
相比之下可以借助Lombok的NonNull注解实现对属性或方法参数的强制性检查
NoArgsConstructor
Setter
Getter
public class Book {private Long id;private String name;NonNullprivate 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 {// ...NonNullprivate 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;}}// ...NonNullpublic 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命名但如果不是我们也可以通过Cleanup的value属性进行指定。
假设我们自定义一个关闭方法是destroy的BufferedReader
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中对应关闭方法也出现异常那么前边的异常会被后边的异常“吞掉”。
比如上边的示例我们强制让readLine和destroy都抛出异常
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 {GetterSetterprivate Long id;GetterSetterprivate String name;GetterSetterprivate Boolean isAdmin;GetterSetterprivate 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或Setter的value属性修改
public class User {GetterSetter(AccessLevel.NONE)private Long id;GetterSetter(AccessLevel.PRIVATE)private String name;GetterSetter(AccessLevel.PACKAGE)private Boolean isAdmin;GetterSetter(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的属性chaintrue现在生成的字节码中 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 命名都是getXXX或isXXX如果想要更简洁的命名比如直接用属性名可以这样
Getter
Setter
Accessors(fluent true, chain true)
public class Publisher {private Long id;private String name;private LocalDate createDate;
}通过设置Accessors的属性fluentture可以让 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;}
}可以看到对于命名为isXXX的boolean属性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中不要将bool或Boolean类型的属性命名为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(id1, name海南出版社, createDate1991-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.Excludeprivate Long id;// ...
}include
如果你只希望输出某些属性可以
ToString(includeFieldNames false, onlyExplicitlyIncluded true)
public class Publisher {private Long id;ToString.Includeprivate String name;ToString.Includeprivate LocalDate createDate;
}现在toString方法只会输出name和createDate属性如果有新加入的属性也不会输出。
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(adminicexmoon)输出只包含了子类SpecialPublisher中的属性。
如果需要包含父类的输出可以
ToString(callSuper true)
public class SpecialPublisher extends Publisher{private String admin;
}输出
SpecialPublisher(superPublisher(海南出版社, 1991-10-01), adminicexmoon)输出方法返回值
如果希望 Lombok 生成的toString方法输出中包含某些方法的返回值可以
ToString(callSuper true)
public class SpecialPublisher extends Publisher {private String admin;ToString.Includeprivate String hello() {return 欢迎来到 this.name() 出版社;}
}输出
SpecialPublisher(superPublisher(海南出版社, 1991-10-01), adminicexmoon, hello欢迎来到海南出版社出版社)最后的输出中包含了ToString.Include标记的方法的返回值。需要注意的是用于输出的方法不能是静态static的且不能包含任何参数空参数列表。
属性展示名称
可以用ToString.Include的name属性修改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.Include的rank属性修改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越大在输出时越靠前。默认情况下rank是0且rank可以为负数。
EqualsAndHashCode
可用注解EqualsAndHashCode生成equals和hashCode方法
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() {// ...}
}equals和hashCode的详细代码可以下载文末的完整示例后自己编译查看。 Include
默认情况下生成的equals和hashCode会使用所有的非static属性换言之调用equals方法进行比较时所有属性都相等才能返回true。
有时候我们仅希望比较某些作为“主键”的属性比如
EqualsAndHashCode(onlyExplicitlyIncluded true)
public class Publisher {EqualsAndHashCode.IncludeToString.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.Excludeprivate String name;ToString.Include(name 创建时间, rank 99)EqualsAndHashCode.Excludeprivate LocalDate createDate;
}callSuper
如果要将EqualsAndHashCode应用于子类通常需要考虑父类的equals和hashCode方法这可以用EqualsAndHashCode的callSuper属性实现
EqualsAndHashCode(callSuper true)
public class SpecialPublisher extends Publisher {EqualsAndHashCode.Excludeprivate String admin;ToString.Includeprivate String hello() {return 欢迎来到 this.name() 出版社;}
}在这个示例中我们仅希望用Publisher.id这个属性来作为比较和生成哈希值的依据所以子类的admin属性也被我们排除了。
生成构造器
Lombok 提供一些注解用于自动生成构造器
NoArgsConstructorRequiredArgsConstructorAllArgsConstructor
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;NonNullprivate String name;NonNullprivate 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;NonNullprivate String name;NonNullprivate 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;NonNullprivate String name;NonNullprivate 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注解相当于同时使用了以下注解
SetterGetterRequiredArgsConstructorToStringEqualsAndHashCode
比如下面的示例
Setter
Getter
RequiredArgsConstructor
ToString
EqualsAndHashCode
public class Employee {private final Long id;NonNullprivate String name;NonNullprivate Boolean delFlag;
}和下面的是等效的
Data
public class Employee {private final Long id;NonNullprivate String name;NonNullprivate Boolean delFlag;
}实际上Data通常用来为实体类POJO提供基本的构造器、Setter和Getter以及equals和hashCode方法。
如果我们需要为某个注解提供更详细的设置比如将Employee的id视作主键用于比较和生成哈希值以及输出的toString方法不包含键名和delFlag可以在使用Data注解的基础上使用对应的注解来设置
Data
EqualsAndHashCode(onlyExplicitlyIncluded true)
ToString(includeFieldNames false)
public class Employee {EqualsAndHashCode.Includeprivate final Long id;NonNullprivate String name;NonNullToString.Excludeprivate Boolean delFlag;
}staticConstructor
类似于AllArgsConstructor等Data同样可以将构造器设置为私有的同时提供一个static方法用于调用构造器比如
Data
public class StudentT {private final Long id;NonNullprivate String name;NonNullprivate Integer age;NonNullprivate T something;
}需要用以下方式创建对象
StudentString s new Student(1L, icexmoon, 20, hello);可以修改为
Data(staticConstructor of)
public class StudentT {private final Long id;NonNullprivate String name;NonNullprivate Integer age;NonNullprivate T something;
}此时这样调用
StudentString 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 ResultT {boolean success;String errorCode;String errorMsg;T data;public static T ResultT success(T data) {return new ResultT(true, , , data);}public static ResultObject success() {return success(null);}
}RestController
RequestMapping(/book/category)
public class BookCategoryController {Valuepublic static class AddCategoryDTO {NotBlank String name;NotBlank String desc;}PostMappingpublic ResultObject addCategory(Validated RequestBody AddCategoryDTO addCategoryDTO) {System.out.println(addCategoryDTO);//用DTO生成POJOBookCategory 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 ListString 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;Singularprivate ListString 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;SingularListString hobbies;
}要注意的是Value会生成一个包含了所有属性的public构造器而Builder会生成一个包含所有属性的包访问权限的构造器两者会发生冲突此时后者会产生而前者不会。
对应的字节码和调用示例与之前的几乎一致这里不再展示。 奇怪的是这里Value会让hobbies变成final的但是依然可以正常生成PersonBuilder.hobbies。只能认为之前的是个Bug。 Builder.Default
可以用Builder.Default为Builder构建外部类时提供默认值如果没有设置相应的值的话
Builder
Value
public class Person {String name;String city;String job;SingularListString hobbies;Builder.DefaultLocalDateTime 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 {Synchronizedpublic 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.可以看到func1和func2的相关代码实际上是并行的并非互斥。
对应的字节码
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)。 如果对静态方法使用SynchronizedLombok 会创建一个$LOCK属性作为临界区
public class ShareData {// ...Synchronizedpublic 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编程笔记18I/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;WithInteger age;
}生成的字节码中只会有withAge方法而不会有withName方法。
访问权限
默认情况下With生成的withXXX方法的访问权限是public也可以指定其他访问权限比如
Value
public class Dog {String name;With(AccessLevel.PACKAGE)Integer age;
}此时生成的withAge是包访问权限。
Getter(lazytrue)
有时候对于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(lazytrue)可以帮助我们更容易地实现类似的代码
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 AtomicReferenceObject 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(lazytrue)实现的类似优化延迟初始化是可以用于多线程的是线程安全的。
潜在问题
就像我们看到的如果你用了Getter(lazytrue)那么在类中调用该字段时就必须用getXXX获取属性值否则你获取到的就是一个AtomicReferenceObject类型的对象并且该对象还没有进行过“初始化”。
Log
使用Log注解可以更方便地输出调试信息比如
RestController
RequestMapping(/book/category)
Log
public class BookCategoryController {// ...PostMappingpublic ResultObject addCategory(Validated RequestBody AddCategoryDTO addCategoryDTO) {System.out.println(addCategoryDTO);log.log(Level.INFO, addCategoryDTO.toString());// ...}
}输出
// ...
BookCategoryController.AddCategoryDTO(name文学, desc包括中国文学外国文学等)
2023-06-04T10:42:03.92808: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 {// ...PostMappingpublic ResultObject 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.lombokdebug。 最后总结一下通过使用 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 LombokCleanup (projectlombok.org)Getter and Setter (projectlombok.org)ToString (projectlombok.org)EqualsAndHashCode (projectlombok.org)Data (projectlombok.org)Introduction to Project Lombok | BaeldungValue (projectlombok.org)Builder (projectlombok.org)Synchronized (projectlombok.org)With (projectlombok.org)Getter(lazytrue)lazytrue (projectlombok.org)从零开始 Spring Boot 10日志 - 红茶的个人站点 (icexmoon.cn)从零开始 Spring Boot 34日志 II - 红茶的个人站点 (icexmoon.cn)Log (and friends) (projectlombok.org)