Automatic Generation of the Builder Pattern with FreeBuilder – 用FreeBuilder自动生成生成器模式

最后修改: 2019年 8月 8日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll use the FreeBuilder library to generate builder classes in Java.

在本教程中,我们将使用FreeBuilder库来生成Java中的构建器类。

2. Builder Design Pattern

2.构建者设计模式

Builder is one of the most widely used Creation Design Patterns in object-oriented languages. It abstracts the instantiation of a complex domain object and provides a fluent API for creating an instance. It thereby helps to maintain a concise domain layer.

Builder是面向对象语言中使用最广泛的创建设计模式之一。它抽象了复杂领域对象的实例化,并为创建实例提供了流畅的API。因此,它有助于维护一个简洁的领域层。

Despite its usefulness, a builder is generally complex to implement, particularly in Java. Even simpler value objects require a lot of boilerplate code.

尽管它很有用,但构建器的实现通常很复杂,特别是在Java中。即使是比较简单的值对象也需要大量的模板代码。

3. Builder Implementation in Java

3.在Java中实现生成器

Before we proceed with FreeBuilder, let’s implement a boilerplate builder for our Employee class:

在我们继续使用FreeBuilder之前,让我们为我们的Employee 类实现一个模板构建器。

public class Employee {

    private final String name;
    private final int age;
    private final String department;

    private Employee(String name, int age, String department) {
        this.name = name;
        this.age = age;
        this.department = department;
    }
}

And an inner Builder class:

还有一个内部的Builder类。

public static class Builder {

    private String name;
    private int age;
    private String department;

    public Builder setName(String name) {
        this.name = name;
        return this;
    }

    public Builder setAge(int age) {
        this.age = age;
        return this;
    }

    public Builder setDepartment(String department) {
        this.department = department;
        return this;
    }

    public Employee build() {
        return new Employee(name, age, department);
    }
}

Accordingly, we can now use the builder for instantiating the Employee object:

因此,我们现在可以使用构建器来实例化Employee对象。

Employee.Builder emplBuilder = new Employee.Builder();

Employee employee = emplBuilder
  .setName("baeldung")
  .setAge(12)
  .setDepartment("Builder Pattern")
  .build();

As shown above, a lot of boilerplate code is necessary for implementing a builder class.

如上所示,实现一个构建器类需要大量的模板代码。

In the later sections, we’ll see how FreeBuilder can instantly simplify this implementation.

在后面的章节中,我们将看到FreeBuilder如何立即简化这种实现。

4. Maven Dependency

4.Maven的依赖性

To add the FreeBuilder library, we’ll add the FreeBuilder Maven dependency in our pom.xml:

为了添加FreeBuilder库,我们将在pom.xml中添加FreeBuilder Maven依赖项

<dependency>
    <groupId>org.inferred</groupId>
    <artifactId>freebuilder</artifactId>
    <version>2.4.1</version>
</dependency>

5. FreeBuilder Annotation

5.FreeBuilder注释

5.1. Generating a Builder

5.1.生成一个生成器

FreeBuilder is an open-source library that helps developers avoid the boilerplate code while implementing builder classes. It makes use of annotation processing in Java to generate a concrete implementation of the builder pattern.

FreeBuilder是一个开源的库,它可以帮助开发者在实现构建器类时避免模板代码。它利用Java中的注解处理来生成构建器模式的具体实现。

We’ll annotate our Employee class from the earlier section with @FreeBuilder and see how it automatically generates the builder class:

我们将@FreeBuilder注解我们在前面章节中的Employee类,看看它如何自动生成构建器类。

@FreeBuilder
public interface Employee {
 
    String name();
    int age();
    String department();
    
    class Builder extends Employee_Builder {
    }
}

It’s important to point out that Employee is now an interface rather than a POJO class. Furthermore, it contains all the attributes of an Employee object as methods.

需要指出的是,Employee现在是一个接口,而不是一个POJO类。此外,它包含了Employee对象的所有属性,作为方法。

Before we continue to use this builder, we must configure our IDEs to avoid any compilation issues. Since FreeBuilder automatically generates the Employee_Builder class during compilation, the IDE usually complains of ClassNotFoundException on line number 8.

在我们继续使用这个构建器之前,我们必须配置我们的IDE以避免任何编译问题。由于FreeBuilder在编译过程中自动生成了Employee_Builder类,IDE通常会在第8行抱怨ClassNotFoundException

To avoid such issues, we need to enable annotation processing in IntelliJ or Eclipse. And while doing so, we’ll use FreeBuilder’s annotation processor org.inferred.freebuilder.processor.Processor. Additionally, the directory used for generating these source files should be marked as Generated Sources Root.

为了避免此类问题,我们需要在IntelliJEclipse中启用注释处理。而在这样做的时候,我们将使用FreeBuilder的注释处理器org.inferred.freebuilder.processor.Processor。另外,用于生成这些源文件的目录应该被标记为生成的源根

Alternatively, we can also execute mvn install to build the project and generate the required builder classes.

另外,我们也可以执行mvn install来构建项目并生成所需的构建器类。

Finally, we have compiled our project and can now use the Employee.Builder class:

最后,我们已经编译了我们的项目,现在可以使用Employee.Builder类。

Employee.Builder builder = new Employee.Builder();
 
Employee employee = builder.name("baeldung")
  .age(10)
  .department("Builder Pattern")
  .build();

All in all, there are two main differences between this and the builder class we saw earlier. First, we must set the value for all attributes of the Employee class. Otherwise, it throws an IllegalStateException.

总而言之,这与我们之前看到的构建器类有两个主要的区别。首先,我们必须为Employee类的所有属性设置值。否则,它会抛出一个IllegalStateException

We’ll see how FreeBuilder handles optional attributes in a later section.

我们将在后面的章节中看到FreeBuilder如何处理可选的属性。

Second, the method names of Employee.Builder don’t follow the JavaBean naming conventions. We’ll see this in the next section.

第二,Employee.Builder的方法名称没有遵循JavaBean的命名惯例。我们将在下一节中看到这一点。

5.2. JavaBean Naming Convention

5.2.JavaBean的命名规则

To enforce FreeBuilder to follow the JavaBean naming convention, we must rename our methods in Employee and prefix the methods with get:

为了使FreeBuilder遵循JavaBean的命名规则,我们必须重新命名我们在Employee中的方法,并在方法前加上get

@FreeBuilder
public interface Employee {
 
    String getName();
    int getAge();
    String getDepartment();

    class Builder extends Employee_Builder {
    }
}

This will generate getters and setters that follow the JavaBean naming convention:

这将产生遵循JavaBean命名惯例的getters和setters。

Employee employee = builder
  .setName("baeldung")
  .setAge(10)
  .setDepartment("Builder Pattern")
  .build();

5.3. Mapper Methods

5.3.映射器方法

Coupled with getters and setters, FreeBuilder also adds mapper methods in the builder class. These mapper methods accept a UnaryOperator as input, thereby allowing developers to compute complex field values.

与getters和setters相结合,FreeBuilder还在构建器类中增加了映射器方法。这些映射器方法接受UnaryOperator作为输入,从而使开发人员能够计算复杂的字段值。

Suppose our Employee class also has a salary field:

假设我们的Employee类也有一个工资字段。

@FreeBuilder
public interface Employee {
    Optional<Double> getSalaryInUSD();
}

Now suppose we need to convert the currency of the salary that is provided as input:

现在,假设我们需要转换作为输入的工资的货币。

long salaryInEuros = INPUT_SALARY_EUROS;
Employee.Builder builder = new Employee.Builder();

Employee employee = builder
  .setName("baeldung")
  .setAge(10)
  .mapSalaryInUSD(sal -> salaryInEuros * EUROS_TO_USD_RATIO)
  .build();

FreeBuilder provides such mapper methods for all fields.

FreeBuilder为所有字段提供了这样的映射方法。

6. Default Values and Constraint Checks

6.默认值和约束条件检查

6.1. Setting Default Values

6.1.设置默认值

The Employee.Builder implementation we have discussed so far expects the client to pass values for all fields. As a matter of fact, it fails the initialization process with an IllegalStateException in case of missing fields.

到目前为止,我们所讨论的Employee.Builder实现期望客户端为所有字段传递值。事实上,在缺少字段的情况下,它的初始化过程会出现IllegalStateException

In order to avoid such failures, we can either set default values for fields or make them optional.

为了避免这种失败,我们可以为字段设置默认值或使其成为可选项

We can set default values in the Employee.Builder constructor:

我们可以在Employee.Builder构造器中设置默认值。

@FreeBuilder
public interface Employee {

    // getter methods

    class Builder extends Employee_Builder {

        public Builder() {
            setDepartment("Builder Pattern");
        }
    }
}

So we simply set the default department in the constructor. This value will apply to all Employee objects.

所以我们只需在构造函数中设置默认的部门。这个值将适用于所有雇员对象。

6.2. Constraint Checks

6.2.约束检查

Usually, we have certain constraints on field values. For example, a valid email must contain an “@” or the age of an Employee must be within a range.

通常情况下,我们对字段值有一定的约束。例如,一个有效的电子邮件必须包含一个”@”,或者一个雇员的年龄必须在一个范围内。

Such constraints require us to put validations on input values. And FreeBuilder allows us to add these validations by merely overriding the setter methods:

这种约束要求我们对输入值进行验证。而FreeBuilder允许我们仅仅通过覆盖setter methods来添加这些验证信息。

@FreeBuilder
public interface Employee {

    // getter methods

    class Builder extends Employee_Builder {

        @Override
        public Builder setEmail(String email) {
            if (checkValidEmail(email))
                return super.setEmail(email);
            else
                throw new IllegalArgumentException("Invalid email");

        }

        private boolean checkValidEmail(String email) {
            return email.contains("@");
        }
    }
}

7. Optional Values

7.可选值

7.1. Using Optional Fields

7.1.使用可选字段

Some objects contain optional fields, the values for which can be empty or null. FreeBuilder allows us to define such fields using the Java Optional type:

有些对象包含可选字段,其值可以是空或空。FreeBuilder允许我们使用Java Optional类型来定义此类字段。

@FreeBuilder
public interface Employee {

    String getName();
    int getAge();

    // other getters
    
    Optional<Boolean> getPermanent();

    Optional<String> getDateOfJoining();

    class Builder extends Employee_Builder {
    }
}

Now we may skip providing any value for Optional fields:

现在我们可以跳过为Optional字段提供任何值。

Employee employee = builder.setName("baeldung")
  .setAge(10)
  .setPermanent(true)
  .build();

Notably, we simply passed the value for permanent field instead of an Optional. Since we didn’t set the value for dateOfJoining field, it will be Optional.empty() which is the default for Optional fields.

值得注意的是,我们只是传递了permanent字段的值,而不是一个Optional。因为我们没有为dateOfJoining字段设置值,它将是Optional.empty() ,这是对Optional字段的默认。

7.2. Using @Nullable Fields

7.2.使用@Nullable Fields

Although using Optional is recommended for handling nulls in Java, FreeBuilder allows us to use @Nullable for backward compatibility:

虽然推荐使用Optional 来处理Java中的nulls,但FreeBuilder允许我们使用@Nullable进行向后兼容

@FreeBuilder
public interface Employee {

    String getName();
    int getAge();
    
    // other getter methods

    Optional<Boolean> getPermanent();
    Optional<String> getDateOfJoining();

    @Nullable String getCurrentProject();

    class Builder extends Employee_Builder {
    }
}

The use of Optional is ill-advised in some cases which is another reason why @Nullable is preferred for builder classes.

使用Optional在某些情况下是不明智的,这也是为什么@Nullable是构建器类的首选原因。

8. Collections and Maps

8.收藏和地图

FreeBuilder has special support for collections and maps:

FreeBuilder对集合和地图有特殊支持。

@FreeBuilder
public interface Employee {

    String getName();
    int getAge();
    
    // other getter methods

    List<Long> getAccessTokens();
    Map<String, Long> getAssetsSerialIdMapping();


    class Builder extends Employee_Builder {
    }
}

FreeBuilder adds convenience methods to add input elements into the Collection in the builder class:

FreeBuilder增加了方便的方法来将输入元素添加到构建器类的集合中

Employee employee = builder.setName("baeldung")
  .setAge(10)
  .addAccessTokens(1221819L)
  .addAccessTokens(1223441L, 134567L)
  .build();

There is also a getAccessTokens() method in the builder class which returns an unmodifiable list. Similarly, for Map:

在构建器类中还有一个getAccessTokens() 方法,它返回一个不可修改的列表。同样地,对于Map:来说

Employee employee = builder.setName("baeldung")
  .setAge(10)
  .addAccessTokens(1221819L)
  .addAccessTokens(1223441L, 134567L)
  .putAssetsSerialIdMapping("Laptop", 12345L)
  .build();

The getter method for Map also returns an unmodifiable map to the client code.

getter 方法为Map 返回一个不可修改的地图给客户端代码。

9. Nested Builders

9.嵌套建造者

For real-world applications, we may have to nest a lot of value objects for our domain entities. And since the nested objects can themselves need builder implementations, FreeBuilder allows nested buildable types.

对于现实世界的应用,我们可能要为我们的领域实体嵌套很多价值对象。而由于嵌套的对象本身可能需要构建器的实现,FreeBuilder允许嵌套的可构建类型。

For example, suppose we have a nested complex type Address in the Employee class:

例如,假设我们在Employee类中有一个嵌套的复合类型Address

@FreeBuilder
public interface Address {
 
    String getCity();

    class Builder extends Address_Builder {
    }
}

Now, FreeBuilder generates setter methods that take Address.Builder as an input together with Address type:

现在,FreeBuilder生成的setter方法将Address.BuilderAddress类型一起作为输入。

Address.Builder addressBuilder = new Address.Builder();
addressBuilder.setCity(CITY_NAME);

Employee employee = builder.setName("baeldung")
  .setAddress(addressBuilder)
  .build();

Notably, FreeBuilder also adds a method to customize the existing Address object in the Employee:

值得注意的是,FreeBuilder还添加了一个方法来自定义地址对象在雇员:中的现有地址对象。

Employee employee = builder.setName("baeldung")
  .setAddress(addressBuilder)
  .mutateAddress(a -> a.setPinCode(112200))
  .build();

Along with FreeBuilder types, FreeBuilder also allows nesting of other builders such as protos.

除了FreeBuilder类型外,FreeBuilder还允许嵌套其他构建器,如protos

10. Building Partial Object

10.构建部分对象

As we’ve discussed before, FreeBuilder throws an IllegalStateException for any constraint violation — for instance, missing values for mandatory fields.

正如我们之前所讨论的,FreeBuilder对任何违反约束的情况抛出一个IllegalStateException–例如,强制性字段的缺失值。

Although this is desired for production environments, it complicates unit testing that is independent of constraints in general.

尽管这对于生产环境来说是想要的,但它使独立于一般约束的单元测试变得复杂

To relax such constraints, FreeBuilder allows us to build partial objects:

为了放松这种限制,FreeBuilder允许我们建立部分对象。

Employee employee = builder.setName("baeldung")
  .setAge(10)
  .setEmail("abc@xyz.com")
  .buildPartial();

assertNotNull(employee.getEmail());

So, even though we haven’t set all the mandatory fields for an Employee, we could still verify that the email field has a valid value.

因此,即使我们没有为Employee设置所有的必填字段,我们仍然可以验证email字段有一个有效值。

11. Custom toString() Method

11.自定义toString()方法

With value objects, we often need to add a custom toString() implementation. FreeBuilder allows this through abstract classes:

对于值对象,我们经常需要添加一个自定义的toString() 实现。FreeBuilder通过抽象类允许这样做。

@FreeBuilder
public abstract class Employee {

    abstract String getName();

    abstract int getAge();

    @Override
    public String toString() {
        return getName() + " (" + getAge() + " years old)";
    }

    public static class Builder extends Employee_Builder{
    }
}

We declared Employee as an abstract class rather than an interface and provided a custom toString() implementation.

我们将Employee 声明为一个抽象类而不是一个接口,并提供了一个自定义的toString() 实现。

12. Comparison with Other Builder Libraries

12.与其他生成器库的比较

The builder implementation we have discussed in this article is very similar to those of Lombok, Immutables, or any other annotation processor. However, there are a few distinguishing characteristics that we have discussed already:

我们在本文中讨论的构建器实现与LombokImmutables或任何其他annotation处理器非常相似。然而,有几个显著的特征,我们已经讨论过了。

    • Mapper methods
    • Nested Buildable Types
    • Partial Objects

13. Conclusion

13.结语

In this article, we used the FreeBuilder library to generate a builder class in Java. We implemented various customizations of a builder class with the help of annotations, thus reducing the boilerplate code required for its implementation.

在这篇文章中,我们使用FreeBuilder库在Java中生成一个构建器类。我们在注解的帮助下实现了构建器类的各种定制,从而减少了其实现所需的模板代码

We also saw how FreeBuilder is different from some of the other libraries and briefly discussed some of those characteristics in this article.

我们也看到了FreeBuilder与其他一些库的不同之处,并在本文中简要地讨论了其中的一些特点。

All the code examples are available over on GitHub.

所有的代码实例都可以在GitHub上找到。