如何从 Lombok 构建器中排除 属性?

How to exclude property from Lombok builder?

我有一个名为 "XYZClientWrapper" 的 class ,它具有以下结构:

@Builder
XYZClientWrapper{
    String name;
    String domain;
    XYZClient client;
}

我想要的 属性 XYZClient client

没有生成构建函数

Lombok 是否支持这样的用例?

是的,您可以将 @Builder 放在构造函数或静态(工厂)方法上,只包含您想要的字段。

披露:我是 Lombok 开发人员。

或者,我发现将字段标记为 finalstaticstatic final 指示 @Builder 忽略此字段。

@Builder
public class MyClass {
   private String myField;

   private final String excludeThisField = "bar";
}

龙目岛 1.16.10

在代码中创建构建器并为您的 属性 添加私有 setter。

@Builder
XYZClientWrapper{
    String name;
    String domain;
    XYZClient client;

    public static class XYZClientWrapperBuilder {
        private XYZClientWrapperBuilder client(XYZClient client) { return this; }
    }
}

我发现我可以实现一个"shell"的static Builder class,添加我想用私有访问修饰符隐藏的方法,在建设者。同样,我也可以向构建器添加自定义方法。

package com.something;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import java.time.ZonedDateTime;

@Data
@Entity
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class MyClass{

    //The builder will generate a method for this property for us.
    private String anotherProperty;

    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "localDateTime", column = @Column(name = "some_date_local_date_time")),
            @AttributeOverride(name = "zoneId", column = @Column(name = "some__date_zone_id"))
    })
    @Getter(AccessLevel.PRIVATE)
    @Setter(AccessLevel.PRIVATE)
    private ZonedDateTimeEmbeddable someDateInternal;

    public ZonedDateTime getSomeDate() {
        return someDateInternal.toZonedDateTime();
    }

    public void setSomeDate(ZonedDateTime someDate) {
        someDateInternal = new ZonedDateTimeEmbeddable(someDate);
    }

    public static class MyClassBuilder {
        //Prevent direct access to the internal private field by pre-creating builder method with private access.
        private MyClassBuilder shipmentDateInternal(ZonedDateTimeEmbeddable zonedDateTimeEmbeddable) {
            return this;
        }

        //Add a builder method because we don't have a field for this Type
        public MyClassBuilder someDate(ZonedDateTime someDate) {
            someDateInternal = new ZonedDateTimeEmbeddable(someDate);
            return this;
        }
    }

}

这是我的首选解决方案。有了它,您可以在最后创建您的字段 client 并根据先前由构建器设置的其他字段。

XYZClientWrapper{
    String name;
    String domain;
    XYZClient client;
    
    @Builder
    public XYZClientWrapper(String name, String domain) {
        this.name = name;
        this.domain = domain;
        this.client = calculateClient();
    }
}

工厂静态方法示例

class Car {
   private String name;
   private String model;


   private Engine engine; // we want to ignore setting this
   
   @Builder
   private static Car of(String name, String model){
      Car car=new Car();
      car.name = name;
      car.model = model;
      constructEngine(car); // some static private method to construct engine internally
      return car;  
   }

   private static void constructEngine(Car car) {
       // car.engine = blabla...
       // construct engine internally
   }
}

那么你可以使用如下:

Car toyotaCorollaCar=Car.builder().name("Toyota").model("Corolla").build();
// You can see now that Car.builder().engine() is not available

注意 每当调用 build() 时都会调用静态方法 of,因此执行类似 Car.builder().name("Toyota") 的操作实际上不会设置值"Toyota"name 除非调用 build() 然后在构造函数静态方法中分配逻辑 of 被执行。

此外,注意 of 方法是私有访问的,因此 build 方法是调用者唯一可见的方法

我找到了另一种解决方案 您可以将您的字段包装到 initiated final 包装器或代理中。 将它包装成 AtomicReference 的最简单方法。

@Builder
public class Example {
    private String field1;
    private String field2;
    private final AtomicReference<String> excluded = new AtomicReference<>(null);
}

您可以在内部通过get和set方法与它交互,但它不会出现在builder中。

excluded.set("Some value");
excluded.get();

使用 Lombok @Builder 将所谓的 'partial builder' 添加到 class 可以提供帮助。诀窍是像这样添加一个内部部分构建器 class:

@Getter
@Builder
class Human {
    private final String name;
    private final String surname;
    private final Gender gender;
    private final String prefix; // Should be hidden, depends on gender

    // Partial builder to manage dependent fields, and hidden fields
    public static class HumanBuilder {

        public HumanBuilder gender(final Gender gender) {
            this.gender = gender;
            if (Gender.MALE == gender) {
                this.prefix = "Mr.";
            } else if (Gender.FEMALE == gender) {
                this.prefix = "Ms.";
            } else {
                this.prefix = "";
            }
            return this;
        }

        // This method hides the field from external set 
        private HumanBuilder prefix(final String prefix) {
            return this;
        }

    }

}

PS:@Builder 允许更改生成的构建器 class 名称。上面的示例假定使用默认构建器 class 名称。

我之前使用的一种方法是将实例字段分组为配置字段和会话字段。配置字段作为 class 实例并且对构建器可见,而会话字段进入 嵌套 private static class 并通过具体 final 访问实例字段(Builder 默认会忽略)。

像这样:

@Builder
class XYZClientWrapper{
    private String name;
    private String domain;
 
    private static class Session {
        XYZClient client;
    }

    private final Session session = new Session();

    private void initSession() {
        session.client = ...;
    }
 
    public void foo() {
        System.out.println("name: " + name);
        System.out.println("domain: " + domain;
        System.out.println("client: " + session.client);
    }
}

我有另一种使用 @DelegateInner Class 的方法,它支持排除字段的“计算值”。

首先,我们将要排除的字段移到 Inner Class 中,以避免 Lombok 将它们包含在构建器中。

然后,我们使用 @Delegate 公开 Getters/Setters 个构建器排除的字段。

示例:

@Builder
@Getter @Setter @ToString
class Person {

    private String name;
    private int value;
    /* ... More builder-included fields here */

    @Getter @Setter @ToString
    private class BuilderIgnored {

        private String position; // Not included in the Builder, and remain `null` until p.setPosition(...)
        private String nickname; // Lazy initialized as `name+value`, but we can use setter to set a new value
        /* ... More ignored fields here! ... */

        public String getNickname(){ // Computed value for `nickname`
            if(nickname == null){
                nickname = name+value;
            }
            return nickname;
        }
        /* ... More computed fields' getters here! ... */

    }
    @Delegate @Getter(AccessLevel.NONE) // Delegate Lombok Getters/Setters and custom Getters
    private final BuilderIgnored ignored = new BuilderIgnored();

}

对于 Person class 之外的人来说,positionnickname 实际上是内部 class 字段。

Person p = Person.builder().name("Test").value(123).build();
System.out.println(p); // Person(name=Test, value=123, ignored=Person.BuilderIgnored(position=null, nickname=Test123))
p.setNickname("Hello World");
p.setPosition("Manager");
System.out.println(p); // Person(name=Test, value=123, ignored=Person.BuilderIgnored(position=Manager, nickname=Hello World))

优点:

  • 不要强制排除字段为 final
  • 支持排除字段的计算值
  • 允许计算字段引用构建器设置的任何字段(换句话说,允许内部 class 是非静态的 class)
  • 不需要重复所有字段的列表(例如,列出除构造函数中排除的字段之外的所有字段)
  • 不要覆盖 Lombok 库的 @Builder(例如,创建 MyBuilder extends FooBuilder

缺点:

  • 被排除的字段实际上是Inner Class的字段;但是,使用具有适当 Getters/Setters 的 private 标识符,您可以模仿它们就像是真实字段一样
  • 因此,此方法限制您使用 Getters/Setters
  • 访问排除的字段
  • 计算值在调用 Getters 时延迟初始化,而不是 .build()

要从构建器中排除字段,请尝试使用 @Builder.Default

我喜欢并使用的一种方法是这个。 在构造函数中保留必需参数,并通过构建器设置可选参数。如果所需数量不是很大,则可以使用。

class A {
    private int required1;
    private int required2;
    private int optional1;
    private int optional2;

    public A(int required1, int required2) {
        this.required1 = required1;
        this.required2 = required2;
    }

    @Builder(toBuilder = true)
    public A setOptionals(int optional1, int optional2) {
        this.optional1 = optional1;
        this.optional2 = optional2;
        return this;
    }
}

然后用

构造它
A a = new A(1, 2).builder().optional1(3).optional2(4).build();

这种方法的好处是可选值也可以有默认值。