1. Overview

Project Lombok’s @Builder is a helpful mechanism for using the Builder pattern without writing boilerplate code. We can apply this annotation to a Class or a method.

In this quick tutorial, we’ll look at the different use cases for @Builder.

2. Maven Dependencies

First, we need to add Project Lombok to our pom.xml:

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.30</version>
</dependency>

Maven Central has the latest version of Project Lombok here.

3. Using @Builder on a Class

In the first use case, we’re merely implementing a Class, and we want to use a builder to create instances of our class.

The first and only step is to add the annotation to the class declaration:

@Getter
@Builder
public class Widget {
    private final String name;
    private final int id;
}

Lombok does all the work for us. We can now build a Widget and test it:

Widget testWidget = Widget.builder()
  .name("foo")
  .id(1)
  .build();

assertThat(testWidget.getName())
  .isEqualTo("foo");
assertThat(testWidget.getId())
  .isEqualTo(1);

If we want to create copies or near-copies of objects, we can add the property toBuilder = true to the @Builder annotation:

@Builder(toBuilder = true)
public class Widget {
//...
}

This tells Lombok to add the toBuilder() method to our Class. When we invoke the toBuilder() method, it returns a builder initialized with the properties of the instance it’s called on:

Widget testWidget = Widget.builder()
  .name("foo")
  .id(1)
  .build();

Widget.WidgetBuilder widgetBuilder = testWidget.toBuilder();

Widget newWidget = widgetBuilder.id(2).build();
assertThat(newWidget.getName())
  .isEqualTo("foo");
assertThat(newWidget.getId())
  .isEqualTo(2);

We can see in the test code that the builder class generated by Lombok is named like our class with “Builder” appended to it, WidgetBuilder in this case*.* We can then modify the properties we want, and build() a new instance.

If we need to specify the required fields, we can use the annotation configuration to create an auxiliary builder:

@Builder(builderMethodName = "internalBuilder")
public class RequiredFieldAnnotation {
    @NonNull
    private String name;
    private String description;

    public static RequiredFieldAnnotationBuilder builder(String name) {
        return internalBuilder().name(name);
    }
}

In this case, we’re hiding the default builder as internalBuilder and creating our own. Thus, when we create the builder, we must provide the required parameter:

RequiredField.builder("NameField").description("Field Description").build();

Also, to make sure our field exists, we can add the @NonNull annotation.

4. Using @Builder on a Method

Suppose we’re using an object that we want to construct with a builder, but we can’t modify the source or extend the Class.

First, let’s create a quick example using Lombok’s @Value annotation:

@Value
final class ImmutableClient {
    private int id;
    private String name;
}

Now we have a final Class with two immutable members, getters for them, and an all-arguments constructor.

We covered how to use @Builder on a Class, but we can also use it on methods. We’ll use this ability to work around not being able to modify or extend ImmutableClient.

Then we’ll create a new class with a method for creating ImmutableClients:

class ClientBuilder {

    @Builder(builderMethodName = "builder")
    public static ImmutableClient newClient(int id, String name) {
        return new ImmutableClient(id, name);
    }
}

This annotation creates a method named builder() that returns a Builder for creating ImmutableClients.

Now let’s build an ImmutableClient:

ImmutableClient testImmutableClient = ClientBuilder.builder()
  .name("foo")
  .id(1)
  .build();
assertThat(testImmutableClient.getName())
  .isEqualTo("foo");
assertThat(testImmutableClient.getId())
  .isEqualTo(1);

5. Exclude Fields From Builder

Typically, excluding fields from a builder refers to omitting some attributes when constructing objects. This can be very handy when we want to create an object with a specific configuration or when certain fields are irrelevant or optional.

In short, we can use the @Builder annotation to mark directly a custom factory method or a constructor that excludes particular fields.

So, let’s see it in action. For instance, we’ll consider the ClassWithExcludedFields class:

public class ClassWithExcludedFields {

    private int id;
    private String includedField;
    private String excludedField;

}

Next, let’s add a static method that excludes excludedField:

@Builder(builderMethodName = "customBuilder")
public static ClassWithExcludedFields of(int id, String includedField) {
    ClassWithExcludedFields myObject = new ClassWithExcludedFields();
    myObject.setId(id);
    myObject.setIncludedField(includedField);

    return myObject;
}

Here, excludedField isn’t part of the method parameters. So, customBuilder won’t generate a method to set a value for it.

Finally, let’s create a test case to confirm that everything works as expected:

@Test
public void whenUsingCustomBuilder_thenExcludeUnspecifiedFields() {
    ClassWithExcludedFields myObject = ClassWithExcludedFields.customBuilder()
        .id(3)
        .includedField("Included Field")
        // .excludedField() no method to set excludedField
        .build();

    assertThat(myObject.getId()).isEqualTo(3);
    assertThat(myObject.getIncludedField()).isEqualTo("Included Field");
}

As we see above, customBuilder allows customizing which fields to include or exclude in the builder.

Since Lombok v1.16.16, we can alternatively use the @Builder.Default annotation to set a default value for the field we want to ignore in the builder. For example, let’s annotate excludedField with @Builder.Default:

@Builder.Default
private String excludedField = "Excluded Field using Default";

It’s straightforward and readable. The specified default value is used when the field isn’t explicitly set in the builder.

Lastly, let’s confirm this using another test case:

@Test
public void whenUsingBuilderDefaultAnnotation_thenExcludeField() {
    ClassWithExcludedFields myObject = ClassWithExcludedFields.builder()
        .id(3)
        .includedField("Included Field")
        .build();

    assertThat(myObject.getId()).isEqualTo(3);
    assertThat(myObject.getIncludedField()).isEqualTo("Included Field");
    assertThat(myObject.getExcludedField()).isEqualTo("Excluded Field using Default");
}

As we can see, excludedField is set to the default value even though we didn’t use it in the builder.

6. Conclusion

In this brief article, we used Lombok’s @Builder annotation on a method to create a builder for a final Class, and we learned how to make some of the Class fields required.

Code samples, as always, can be found over on GitHub.


« 上一篇: Kotlin教程