1. 概述

Lombok 是一个非常有用的库,用于简化样板代码。如果您还不熟悉它,强烈建议您先阅读之前的教程——Lombok 入门教程

在这个文章中,我们将演示它与 Spring 的基于构造函数的依赖注入(Constructor-Based Dependency Injection)结合使用时的可用性。

2. 基于构造函数的依赖注入

在 Spring 中使用基于构造函数的依赖注入(Constructor-Based Dependency Injection)是一种很好的方式。这种方法要求我们在构造函数中明确传递组件的依赖项。

相较于基于字段的依赖注入(Field-Based Dependency Injection),它还提供了以下优点:

  • 不需要创建针对测试的配置组件——依赖项在构造函数中被明确注入。
  • 一致的设计——所有必需的依赖通过构造函数定义得到强调和管理。
  • 简单的单元测试——减少 Spring 框架的开销。
  • 回收对 final 关键字使用的自由。

然而,由于需要编写构造函数,通常会导致较大的代码量。考虑 GreetingServiceFarewellService 的两个例子:

@Component
public class GreetingService {

    @Autowired
    private Translator translator;

    public String produce() {
        return translator.translate("hello");
    }
}
@Component
public class FarewellService {

    private final Translator translator;

    public FarewellService(Translator translator) {
        this.translator = translator;
    }

    public String produce() {
        return translator.translate("bye");
    }
}

这两个组件基本上做着同样的事情——它们调用一个可配置的 Translator,并传递特定任务的单词。

然而,第二个版本由于构造函数的样板代码而显得更加冗长,这些样板代码对代码本身并没有实际价值。

在最新的 Spring 发布版中,构造函数不再需要使用 @Autowired 标注。

3. 使用 Lombok 的构造注入

借助 Lombok,我们可以为类的所有字段(使用 @AllArgsConstructor)或所有 final 类字段(使用 @RequiredArgsConstructor)自动生成构造函数。此外,如果您仍然需要一个空构造函数,可以在类上附加 @NoArgsConstructor 标注。

让我们创建一个与之前类似的第三个组件:

@Component
@RequiredArgsConstructor
public class ThankingService {

    private final Translator translator;

    public String produce() {
        return translator.translate("thank you");
    }
}

上述注解将导致 Lombok 为我们生成构造函数:

@Component
public class ThankingService {

    private final Translator translator;

    public String thank() {
        return translator.translate("thank you");
    }

    /* Generated by Lombok */
    public ThankingService(Translator translator) {
        this.translator = translator;
    }
}

4. 多个构造函数

只要组件中只有一个构造函数,并且 Spring 能够明确选择正确的构造函数来实例化新对象,就不必为其添加注解。一旦有多个构造函数,还需要为 IoC 容器要使用的那个构造函数添加注解。

ApologizeService 为例:

@Component
@RequiredArgsConstructor
public class ApologizeService {

    private final Translator translator;
    private final String message;

    @Autowired
    public ApologizeService(Translator translator) {
        this(translator, "sorry");
    }

    public String produce() {
        return translator.translate(message);
    }
}

这个组件可以选择性地配置 message 字段,该字段在组件创建后不能更改(因此没有 setter)。因此,我们需要提供两个构造函数:一个用于完整配置,另一个用于隐式默认的 message 值。

除非其中一个构造函数被标注为 @Autowired@Inject@Resource,否则 Spring 将抛出错误:

Failed to instantiate [...]: No default constructor found;

如果我们想为 Lombok 生成的构造函数添加注解,需要通过 @AllArgsConstructoronConstructor 参数传递:

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ApologizeService {
    // ...
}

onConstructor 参数接受一个注解数组(如示例所示,也可以是一个单独的注解)。双下划线语法是为了处理向后兼容性问题。根据文档说明:

这种奇怪的语法是为了让这个特性在 javac 7 编译器中工作;@__ 类型是对不存在的 __(双下划线)注解类型的引用,这使得 javac 7 在编译过程中不会因错误而立即终止,因为有可能后续的注解处理器会创建 __ 类型。

5. 总结

在这篇教程中,我们展示了在减少样板代码方面,没有必要优先选择基于字段的依赖注入而非基于构造函数的依赖注入。

得益于 Lombok,我们可以在不降低运行时性能的情况下自动化常见代码生成,将冗长且难以理解的代码缩短到单行注解的使用。

本教程中使用的代码可在 GitHub 查看。