@Builder 注解用于在类上生成构建器模式的代码,允许通过链式调用的方式构建对象。
🏷 版本
@Builder 在 Lombok v0.12.0 中作为实验性功能引入。
@Builder 从 Lombok v1.16.0 开始获得 @Singular 支持并晋升为主 lombok 包。
@Builder.Default 在 Lombok v1.16.16 中添加了功能。
@Builder(builderMethodName = “”) 从 Lombok v1.18.8 开始被支持的(并且将抑制构建器方法的生成)。
@Builder(access = AccessLevel.PACKAGE) 从 Lombok v1.18.8 开始被支持的(并将生成具有指定访问级别的构建器类、构建器方法等)。
📋 概述
@Builder 注解会为你的类生成复杂的构建器 API。
@Builder 允许你自动生成所需的代码,以便使用以下代码实例化你的类:
Person.builder()
.name("Adam Savage")
.city("San Francisco")
.job("Mythbusters")
.job("Unchained Reaction")
.build();
@Builder 可以放在类、构造函数或方法上。
@Builder 添加在方法上会导致生成以下 7 件事:
- 一个名为 FooBuilder 的内部静态类,其类型参数与静态方法(称为生成器)相同。
- 在构建器中:方法的每个参数都有一个私有的非静态非最终字段。
- 在构建器中:一个私有无参构造函数。
- 在构建器中:方法的每个参数都类似“setter”方法:它与该参数具有相同的类型和相同的名称。它返回生成器本身,以便可以链式调用。
- 在构建器中:build() 方法返回的类型与希望得到的类型相同。
- 在构建器中:包含一个 toString 方法。
- 在当前类中:一个 builder() 方法,用于创建生成器的新实例。
如果某个列出的生成元素已存在,则该元素将被静默跳过。
@Builder 也支持集合类型的参数:
Person.builder()
.job("Mythbusters")
.job("Unchained Reaction")
.build();
参数 List<String> jobs
可以通过上面的代码添加两个元素值。不过想要支持此功能,需要在字段上添加注解 @Singular
。
在类上添加 @Builder 就好像在类上添加 @AllArgsConstructor(access = AccessLevel.PACKAGE)
并且在全参构造函数上添加 @Builder 注解一样。不过这需要你没有显示编写构造器的情况下,如果已经有构造函数了,建议将 @Builder 放在构造函数上,而不是类上。
如果将“@Value”和“@Builder”放在类上,则“@Builder”生成私有构造函数将有更高的优先级。
默认情况下, @Builder 生成的构建器将得到当前类的实例。你可以通过 @Builder(toBuilder = true) 来更改这一行为,此时可以在类中定义一个 toBuilder() 方法,构建器将首先调用该方法并得到想要被定义的类型。可以在字段、构造器或者方法上添加 @Builder.ObtainVia 注解来指定该字段的返回值。例如,可以指定要调用的方法: @Builder.ObtainVia(method = "calculateFoo")
。
如果有一个类名为 Foobar 的类添加 @Builder 注解后,得到的构建器为 FoobarBuilder。
如果有一个包含泛型的类名为 Foobar
如果 @Builder 应用于返回 void 的方法,则构建器将被命名为 VoidBuilder 。
构建器有以下几个可以配置的点:
- 构建器的类名:默认为 xxxBuilder
- build() 方法的名称:默认为 build
- builder() 方法的名称:默认为 builder()
- 是否需要 toBuilder() 方法:默认不需要
- 生成的方法访问修饰符:默认为 public
- 是否给 setter 方法添加 set 前缀:默认不添加。默认的调用形式为
Person.builder().name("Jane").build()
,如果添加前缀就变成了Person.builder().setName("Jane").build()
下面的例子将所有的配置都加上:
@Builder(builderClassName = "HelloWorldBuilder", buildMethodName = "execute", builderMethodName = "helloWorld", toBuilder = true, access = AccessLevel.PRIVATE, setterPrefix = "set")
public class MyHello{}
@Builder.Default
在使用构建器构造示例时,如果没有设置过某个字段,则得到的值为 0/null/false。如果在类上添加了 @Builder 注解,可以在字段上使用 @Builder.Default 来设置默认值。
使用 Lombok 生成的构造器也将触发默认值,不过如果是自定义的构造函数,将不会触发。
@Singular
当有 @Builder 注解存在时,在字段上发现了 @Singular 注解,该字段将会被认为是集合,此时生成的方法不再是 setter 而是 adder 方法,当然也会生成 clear 方法用于清除集合中的元素。
需要注意以下几点:
- 调用 build() 以后,生成的集合将是不可变的。
- 调用 build() 以后,之前生成集合将无法通过 clear 方法清除。
- 调用 build() 以后,再调用 adder 方法会生成新的集合;然后再调用 build() 会将之前所有 adder 的元素合成为一个新的集合。
@Singular 仅支持以下集合类型:
- java.util.Iterable / java.util.Collection / java.util.List
- java.util.Set / java.util.SortedSet / java.util.NavigableSet
- java.util.Map / java.util.SortedMap / java.util.NavigableMap
- com.google.common.collect.ImmutableCollection / com.google.common.collect.ImmutableList
- com.google.common.collect.ImmutableSet / com.google.common.collect.ImmutableSortedSet
- com.google.common.collect.ImmutableMap / com.google.common.collect.ImmutableBiMap / com.google.common.collect.ImmutableSortedMap
- com.google.common.collect.ImmutableTable
如果字段的名称为复数形式,则 adder 方法会采用其单数形式:例如有一个 statuses 集合,则 adder 方法将为 status 。当然,也使用通过参数来指定 adder 方法的名称:@Singular("axis") List<Line> axes;
。
如果 Lombok 无法识别字段的单数形式,则会生成错误并强制你显式指定单数名称。
如果还使用了参数 setterPrefix = "with"
,则生成的名称为 withName (添加 1 个名称)、 withNames (添加多个名称)和 clearNames (重置所有名称)。
通常情况下,生成的集合将进行非空检查,如果得到的集合为 null ,将会抛出 NullPointerException 异常。如果不希望抛出异常,可以通过参数来忽略 @Singular(ignoreNullCollections = true)
。
与 jackson
你可以自定义构建器的各个部分,例如,通过自己创建构建器类来向构建器类添加另一个方法,或者在构建器类上添加其他注解。Lombok 将帮助你生成所有需要的内容,并将其放入此构建器类中。例如,如果尝试将 jackson 配置为对集合使用特定的子类型,则可以编写如下内容:
@Value @Builder
@JsonDeserialize(builder = JacksonExample.JacksonExampleBuilder.class)
public class JacksonExample {
@Singular(nullBehavior = NullCollectionBehavior.IGNORE) private List<Foo> foos;
@JsonPOJOBuilder(withPrefix = "")
public static class JacksonExampleBuilder implements JacksonExampleBuilderMeta {
}
private interface JacksonExampleBuilderMeta {
@JsonDeserialize(contentAs = FooImpl.class)
JacksonExampleBuilder foos(List<? extends Foo> foos)
}
}
原生写法
import java.util.Set;
public class BuilderExample {
private long created;
private String name;
private int age;
private Set<String> occupations;
BuilderExample(String name, int age, Set<String> occupations) {
this.name = name;
this.age = age;
this.occupations = occupations;
}
private static long $default$created() {
return System.currentTimeMillis();
}
public static BuilderExampleBuilder builder() {
return new BuilderExampleBuilder();
}
public static class BuilderExampleBuilder {
private long created;
private boolean created$set;
private String name;
private int age;
private java.util.ArrayList<String> occupations;
BuilderExampleBuilder() {
}
public BuilderExampleBuilder created(long created) {
this.created = created;
this.created$set = true;
return this;
}
public BuilderExampleBuilder name(String name) {
this.name = name;
return this;
}
public BuilderExampleBuilder age(int age) {
this.age = age;
return this;
}
public BuilderExampleBuilder occupation(String occupation) {
if (this.occupations == null) {
this.occupations = new java.util.ArrayList<String>();
}
this.occupations.add(occupation);
return this;
}
public BuilderExampleBuilder occupations(Collection<? extends String> occupations) {
if (this.occupations == null) {
this.occupations = new java.util.ArrayList<String>();
}
this.occupations.addAll(occupations);
return this;
}
public BuilderExampleBuilder clearOccupations() {
if (this.occupations != null) {
this.occupations.clear();
}
return this;
}
public BuilderExample build() {
// complicated switch statement to produce a compact properly sized immutable set omitted.
Set<String> occupations = ...;
return new BuilderExample(created$set ? created : BuilderExample.$default$created(), name, age, occupations);
}
@java.lang.Override
public String toString() {
return "BuilderExample.BuilderExampleBuilder(created = " + this.created + ", name = " + this.name + ", age = " + this.age + ", occupations = " + this.occupations + ")";
}
}
}
Lombok 简化
import lombok.Builder;
import lombok.Singular;
import java.util.Set;
@Builder
public class BuilderExample {
@Builder.Default private long created = System.currentTimeMillis();
private String name;
private int age;
@Singular private Set<String> occupations;
}
🛠 配置
lombok.builder.flagUsage = [warning | error]
默认:未设置。如果配置的话,Lombok 会将使用@Builder
标记为警告或错误。
lombok.builder.className = [类名称]
默认值: *Builder。除非使用 builderClassName 参数显式指定构建器的类名,否则将选择此名称。
lombok.singular.useGuava = [true | false]
默认:false。如果 true ,Lombok 将使用 guava 的 ImmutableXxx 构建器和类型来实现 java.util 集合接口,而不是创建基于 Collections.unmodifiableXxx 的实现。如果使用此设置,则必须确保 guava 在类路径和构建路径上实际可用。
lombok.singular.auto = [ true | false ]
默认:true。Lombok 将会尝试识别集合字段的单数形式。如果设置为 false,则必须始终显式指定单数名称,如果不这样做,lombok 将生成错误。
🔔 说明
@Singular 支持 java.util.NavigableMap/Set
仅在使用 JDK1.8 或更高版本进行编译时才有效。
@Singular 不支持一次添加多个元素。
排序集合如SortedSet,SortedMap使用的是字段的自然顺序(实现了 java.util.Comparable 接口)。目前不能自己在构建时使用 Comparator 指定排序规则。
当字段类型属于 java.util 包中的类时,添加 @Singular 注解后,该字段的实现将采用 ArrayList ,即使该字段为 Set 或 Map。因为 Lombok 在存储集合时有会压缩动作,采用 ArrayList 更好处理一些,不过使用者不必关心这一行为,它属于内部实现细节。
带有 @Builder.Default 注解的字段的初始值的删除和存储都在静态方法中进行,以保证在生成中指定值时根本不会执行此初始值。这意味着初始化时不能引用 this、super 或非静态成员。如果 lombok 为你生成构造函数,它还会使用初始值设定项初始化此字段。
带有 @Builder.Default 注解的字段将通过 propertyName$value
来设置值;也会有一个布尔类型的 propertyName$set
字段来判断是否已经设置过值。一般来说,不应该通过这些 Lombok 内部生成的代码来设置字段值,如果要自定义字段值,应该使用 setter 方法。
构建器构造对象时,也会遵守常见的非空判断注解,这一特性与 @Setter 提供的能力相同。
在使用了 @Builder(builderMethodName = "")
以后,builder() 方法会被禁用,你可以自定义 toBuilder() 方法。此时,有关缺少 @Builder.Default 注解的任何警告都将消失。
@Builder 可用于复制构造函数: foo.toBuilder().build()
制作浅克隆。如果您只想使用此功能,可以考虑禁用 builder 方法:@Builder(toBuilder = true, builderMethodName = "")
。
由于 javac 处理静态导入的特殊方式,尝试对静态 builder() 方法进行非*
静态导入是行不通的。要么使用*
静态导入:import static TypeThatHasABuilder.*;
,要么不静态导入 builder 方法。
如果将访问级别设置为 PROTECTED ,则在构建器类中生成的所有方法实际上都生成为 public ; protected 关键字的含义在内部类内部是不同的,并且 PROTECTED 指示的精确行为(允许同一包中的任何源访问,以及外部类中的任何子类)标记为 @Builder 不可能,并且标记内部成员为 public 更合理些。
如果已经通过 lombok.config 的 lombok.addNullAnnotations 配置了非空注解,那么被 @Singular 标注的字段将会有非空检查。
📝总结
使用 Lombok 的 @Builder 注解可以显著减少样板代码,并使对象构建过程更加清晰和易于管理。然而,开发者应当注意注解的正确使用方式和配置选项,以确保生成的代码符合预期的设计。