Lombok 中的@Builder 注解

@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 注解后,得到的构建器为 FoobarBuilder

如果 @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 注解可以显著减少样板代码,并使对象构建过程更加清晰和易于管理。然而,开发者应当注意注解的正确使用方式和配置选项,以确保生成的代码符合预期的设计。

转载请注明出处:码谱记录 » Lombok 中的@Builder 注解