How to exclude property from Lombok builder?
JavaBuilderLombokJava Problem Overview
I have a class called as "XYZClientWrapper" , which have following structure:
@Builder
XYZClientWrapper{
String name;
String domain;
XYZClient client;
}
What I want no build function generated for property XYZClient client
Does Lombok supports such use case?
Java Solutions
Solution 1 - Java
Yes, you can place @Builder on a constructor or static (factory) method, containing just the fields you want.
Disclosure: I am a Lombok developer.
Solution 2 - Java
Alternatively, I found out that marking a field as final, static or static final instructs @Builder
to ignore this field.
@Builder
public class MyClass {
private String myField;
private final String excludeThisField = "bar";
}
Lombok 1.16.10
Solution 3 - Java
Create the builder in code and add a private setter for your property.
@Builder
XYZClientWrapper{
String name;
String domain;
XYZClient client;
public static class XYZClientWrapperBuilder {
private XYZClientWrapperBuilder client(XYZClient client) { return this; }
}
}
Solution 4 - Java
Here is my preferred solution. With that, you can create your field client
at the end and have it depending on other fields that previously set by the builder.
XYZClientWrapper{
String name;
String domain;
XYZClient client;
@Builder
public XYZClientWrapper(String name, String domain) {
this.name = name;
this.domain = domain;
this.client = calculateClient();
}
}
Solution 5 - Java
For factory static method example
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
}
}
then you can use as follows:
Car toyotaCorollaCar=Car.builder().name("Toyota").model("Corolla").build();
// You can see now that Car.builder().engine() is not available
Notice the static method of
will be called whenever build() is called, so doing something like Car.builder().name("Toyota")
won't actually set the value "Toyota"
into name
unless build()
is called and then assigning logic within the constructor static method of
is executed.
Also, Notice that the of
method is privately accessed so that build
method is the only method visible to the callers
Solution 6 - Java
I found that I was able to implement a "shell" of the static Builder class, add the method I want to hide with a private access modifier, and it is no longer accessible in the builder. Likewise I can add custom methods to the builder as well.
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;
}
}
}
Solution 7 - Java
I found one more solution You can wrap your field into initiated final wrapper or proxy. The easiest way to wrap it into AtomicReference.
@Builder
public class Example {
private String field1;
private String field2;
private final AtomicReference<String> excluded = new AtomicReference<>(null);
}
You can interact with it inside by get and set methods but it won't be appeared in builder.
excluded.set("Some value");
excluded.get();
Solution 8 - Java
Adding a so called 'partial builder' to the class with Lombok @Builder
can help. The trick is to add a inner partial builder class like this:
@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 allows the generated builder class name to be changed. The example above assumed the default builder class name is used.
Solution 9 - Java
I have another approach using @Delegate
and Inner Class
, which supports "computed values" for the excluded fields.
First, we move the fields to be excluded into an Inner Class
to avoid Lombok from including them in the Builder.
Then, we use @Delegate
to expose Getters/Setters of the builder-excluded fields.
Example:
@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();
}
It will be transparent to outside of this Person
class that position
and nickname
are actually inner class' fields.
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))
Pros:
- Do not force the excluded fields to be
final
- Support computed values for the excluded fields
- Allow computed fields to refer to any fields set by the builder (In other words, allow the inner class to be non-static class)
- Do not need to repeat the list of all fields (Eg. listing all fields except the excluded ones in a constructor)
- Do not override Lombok library's
@Builder
(Eg. creatingMyBuilder extends FooBuilder
)
Cons:
- The excluded fields are actually fields of
Inner Class
; however, usingprivate
identifier with proper Getters/Setters you can mimic as if they were real fields - Therefore, this approach limits you to access the excluded fields using Getters/Setters
- The computed values are lazy initialized when Getters are invoked, not when
.build()
.
Solution 10 - Java
To exclude field from builder, try using @Builder.Default
Solution 11 - Java
One method I like and use is this. Keep required parameters in constructor, and set optional through builder. Works if number of required is not very big.
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;
}
}
And then construct it with
A a = new A(1, 2).builder().optional1(3).optional2(4).build();
Nice thing with this approach is that optionals can also have default value.
Solution 12 - Java
One approach I have used before was to group instance fields into Configuration fields and Session fields. Configuration fields go as class instances and are visible to the Builder, while Session fields go into a nested private static class
and are accessed via a concrete final
instance field (which the Builder will ignore by default).
Something like this:
@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);
}
}