1. 概述

现代软件设计中最重要的开发原则之一就是依赖注入(Dependency Injection, DI),它自然而然地源于另一个关键原则:模块化

本教程将探讨Spring框架中的一种特定的DI技术——构造函数依赖注入。简单来说,这意味着我们在实例化类时传递所需的组件。

首先,我们需要在pom.xml中导入spring-boot-starter-web依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
 </dependency>

接下来,我们需要设置一个配置文件。这个文件可以是POJO(Plain Old Java Object)或XML文件,根据个人喜好选择。

2. 注解配置

Java配置文件看起来像带有额外注解的Java对象:

@Configuration
@ComponentScan("com.baeldung.constructordi")
public class Config {

    @Bean
    public Engine engine() {
        return new Engine("v8", 5);
    }

    @Bean
    public Transmission transmission() {
        return new Transmission("sliding");
    }
}

这里我们使用注解通知Spring运行时,这个类提供bean定义(@Bean注解),并且com.baeldung.spring包需要进行上下文扫描以查找其他bean。然后,我们定义一个Car类:

@Component
public class Car {

    @Autowired
    public Car(Engine engine, Transmission transmission) {
        this.engine = engine;
        this.transmission = transmission;
    }
}

Spring会在包扫描过程中遇到Car类,并通过调用带有@Autowired注解的构造函数初始化它的实例。

通过调用Config类的@Bean注解方法,我们可以获取EngineTransmission的实例。最后,我们需要使用我们的POJO配置启动一个ApplicationContext

ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
Car car = context.getBean(Car.class);

3. 隐式构造函数注入

从Spring 4.3开始,只有一个构造函数的类可以省略@Autowired注解。这是一个方便的小功能,可以减少一些样板代码。

此外,从4.3开始,我们可以在@Configuration注解的类中利用基于构造函数的注入。如果此类只有一个构造函数,也可以省略@Autowired注解。

4. XML配置

另一种使用构造函数依赖注入配置Spring运行时的方式是使用XML配置文件:

<bean id="toyota" class="com.baeldung.constructordi.domain.Car">
    <constructor-arg index="0" ref="engine"/>
    <constructor-arg index="1" ref="transmission"/>
</bean>

<bean id="engine" class="com.baeldung.constructordi.domain.Engine">
    <constructor-arg index="0" value="v4"/>
    <constructor-arg index="1" value="2"/>
</bean>

<bean id="transmission" class="com.baeldung.constructordi.domain.Transmission">
    <constructor-arg value="sliding"/>
</bean>

注意,constructor-arg可以接受一个字面值或另一个bean的引用,而且可以提供可选的明确的indextype。我们可以使用Typeindex属性来解决歧义(例如,如果构造函数接受多个相同类型的参数)。

name属性也可以用于XML到Java变量的匹配,但这时你的代码必须使用调试标志编译。

在这种情况下,我们需要使用ClassPathXmlApplicationContext来启动我们的Spring应用程序上下文:

ApplicationContext context = new ClassPathXmlApplicationContext("baeldung.xml");
Car car = context.getBean(Car.class);

5. 优缺点

与字段注入相比,构造函数注入有一些优势。

首先,它是可测试性的。 假设我们要单元测试一个使用字段注入的Spring bean:

public class UserService {
    
    @Autowired 
    private UserRepository userRepository;
}

在构建UserService实例时,我们无法初始化userRepository的状态。唯一的方法是通过反射API,这完全破坏了封装性。此外,与简单的构造函数调用相比,结果代码的安全性会降低。

此外,在字段注入中,我们不能强制类级不变量。 因此,可能会有一个没有正确初始化的userRepositoryUserService实例。因此,我们可能会在这里和那里遇到随机的NullPointerException。另一方面,使用构造函数创建对象实例在面向对象的角度来看更为自然。

另外,使用构造函数创建对象实例更符合面向对象的原则。

然而,构造函数注入的主要缺点是其冗长性,特别是当bean有很多依赖项时。有时,这可能是一个伪装的祝福,因为我们可能会更加努力地保持依赖项数量最小化。

6. 总结

本文简要展示了使用Spring框架实现两种不同的构造函数依赖注入的基本方法。

本文的完整实现可以在GitHub上找到