1. 概述

本教程将介绍IoC(控制反转)和DI(依赖注入)的核心概念,并展示它们在Spring框架中的具体实现方式。

2. 什么是控制反转?

控制反转是软件工程中的一项原则,它将对象或程序部分的控制权转移给容器或框架。我们通常在面向对象编程的上下文中使用它。

与传统编程(我们的自定义代码调用库)相反,IoC使框架能够控制程序流程并调用我们的自定义代码。为此,框架使用带有附加行为的抽象。如果我们想添加自己的行为,需要扩展框架类或插入自己的类。

这种架构的优势包括:

  • 将任务执行与其实现解耦
  • 更容易在不同实现间切换
  • 提高程序模块化程度
  • 通过隔离组件或模拟其依赖项来简化测试,并允许组件通过契约通信

我们可以通过多种机制实现控制反转:策略设计模式、服务定位器模式、工厂模式和依赖注入(DI)。

接下来我们将重点讨论DI。

3. 什么是依赖注入?

依赖注入是实现IoC的一种模式,其中被反转的控制是设置对象的依赖关系。

将对象与其他对象连接(或"注入"对象到其他对象)由组装器完成,而不是由对象自身完成。

以下是传统编程中创建对象依赖的方式:

public class Store {
    private Item item;
 
    public Store() {
        item = new ItemImpl1();    
    }
}

在上面的例子中,我们需要在Store类内部实例化Item接口的实现。

使用DI,我们可以重写示例而不指定想要的Item实现:

public class Store {
    private Item item;
    public Store(Item item) {
        this.item = item;
    }
}

在接下来的章节中,我们将了解如何通过元数据提供Item的实现。

IoC和DI都是简单的概念,但它们对我们构建系统的方式有深远影响,因此完全理解它们非常值得。

4. Spring IoC容器

IoC容器是实现IoC框架的共同特征。

在Spring框架中,ApplicationContext接口代表IoC容器。Spring容器负责实例化、配置和组装称为bean的对象,以及管理它们的生命周期。

Spring框架提供了ApplicationContext接口的多个实现:用于独立应用的AnnotationConfigApplicationContextClassPathXmlApplicationContextFileSystemXmlApplicationContext,以及用于Web应用的WebApplicationContext

为了组装bean,容器使用配置元数据,这些元数据可以是XML配置或注解的形式。

以下是手动实例化容器的一种方式:

ApplicationContext context
  = new ClassPathXmlApplicationContext("applicationContext.xml");

以下是使用AnnotationConfigApplicationContext手动实例化容器的示例:

AnnotationConfigApplicationContext annotationContext = new AnnotationConfigApplicationContext();

当创建AnnotationConfigApplicationContext实例并提供一个或多个配置类时,它会扫描这些类中的*@Bean*注解和其他相关注解。然后初始化并管理这些类中定义的bean,设置它们的依赖关系并管理其生命周期。详细示例可参考这里

要设置上面示例中的item属性,我们可以使用元数据。然后容器将读取此元数据并在运行时使用它来组装bean。

Spring中的依赖注入可以通过构造函数、setter方法或字段完成。

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

基于构造函数的依赖注入中,容器将调用带有参数的构造函数,每个参数代表我们想要设置的依赖项。

Spring主要通过类型解析每个参数,其次是属性名称,最后是索引用于消除歧义。让我们看看使用注解配置bean及其依赖项:

@Configuration
public class AppConfig {

    @Bean
    public Item item1() {
        return new ItemImpl1();
    }

    @Bean
    public Store store() {
        return new Store(item1());
    }
}

@Configuration注解表示该类是bean定义的源。我们也可以将其添加到多个配置类。

我们在方法上使用*@Bean*注解来定义bean。如果不指定自定义名称,则bean名称默认为方法名。

对于默认singleton作用域的bean,Spring首先检查是否已存在该bean的缓存实例,仅当不存在时才创建新实例。如果使用prototype作用域,容器会为每个方法调用返回一个新的bean实例。

创建bean配置的另一种方式是通过XML配置:

<bean id="item1" class="org.baeldung.store.ItemImpl1" /> 
<bean id="store" class="org.baeldung.store.Store"> 
    <constructor-arg type="ItemImpl1" index="0" name="item" ref="item1" /> 
</bean>

6. 基于Setter的依赖注入

对于基于setter的DI,容器在调用无参构造函数或无参静态工厂方法实例化bean后,会调用我们类的setter方法。让我们使用注解创建此配置:

@Bean
public Store store() {
    Store store = new Store();
    store.setItem(item1());
    return store;
}

我们也可以使用XML进行相同的bean配置:

<bean id="store" class="org.baeldung.store.Store">
    <property name="item" ref="item1" />
</bean>

我们可以对同一个bean组合使用基于构造函数和基于setter的注入类型。Spring文档建议对必需依赖项使用基于构造函数的注入,对可选依赖项使用基于setter的注入。

7. 基于字段的依赖注入

在基于字段的DI中,我们可以通过用*@Autowired*注解标记依赖项来注入它们:

public class Store {
    @Autowired
    private Item item; 
}

在构造Store对象时,如果没有构造函数或setter方法来注入Item bean,容器将使用反射将Item注入到Store中。

我们也可以使用XML配置实现这一点。

这种方法可能看起来更简洁,但我们不推荐使用它,因为它有几个缺点:

  • 此方法使用反射注入依赖项,比基于构造函数或setter的注入成本更高
  • 使用这种方法很容易不断添加多个依赖项。如果我们使用构造函数注入,多个参数会让我们思考该类是否承担了过多职责,可能违反单一职责原则

关于*@Autowired*注解的更多信息可在Spring中的装配文章中找到。

8. 自动装配依赖项

装配允许Spring容器通过检查已定义的bean,自动解析协作bean之间的依赖关系。

使用XML配置自动装配bean有四种模式:

  • no: 默认值 – 表示不对bean使用自动装配,我们必须显式命名依赖项
  • byName: 根据属性名称进行自动装配,因此Spring会查找与需要设置的属性同名的bean
  • byType: 类似于byName自动装配,但基于属性类型。这意味着Spring会查找与属性类型相同的bean来设置。如果存在多个该类型的bean,框架会抛出异常
  • constructor: 基于构造函数参数进行自动装配,意味着Spring会查找与构造函数参数类型相同的bean

例如,让我们将上面定义的item bean按类型自动装配到store bean中:

@Bean(autowire = Autowire.BY_TYPE)
public class Store {
    private Item item;
    public setItem(Item item) {
        this.item = item;
    }
}

注意:从Spring 5.1开始,autowire属性已被弃用。

我们也可以使用*@Autowired*注解按类型自动装配bean:

public class Store {
    
    @Autowired
    private Item item;
}

如果存在多个相同类型的bean,我们可以使用*@Qualifier*注解按名称引用bean:

public class Store {
    
    @Autowired
    @Qualifier("item1")
    private Item item;
}

现在让我们通过XML配置按类型自动装配bean:

<bean id="store" class="org.baeldung.store.Store" autowire="byType"> </bean>

接下来,让我们通过XML按名称将名为item的bean注入到store bean的item属性中:

<bean id="item" class="org.baeldung.store.ItemImpl1" />

<bean id="store" class="org.baeldung.store.Store" autowire="byName">
</bean>

我们也可以通过构造函数参数或setter显式定义依赖项来覆盖自动装配。

9. 延迟初始化的Bean

默认情况下,容器在初始化期间创建和配置所有单例bean。为避免这种情况,我们可以在bean配置上使用值为truelazy-init属性:

<bean id="item1" class="org.baeldung.store.ItemImpl1" lazy-init="true" />

因此,item1 bean只会在首次请求时初始化,而不是在启动时初始化。这样做的好处是初始化时间更快,但代价是我们只有在请求bean后才会发现任何配置错误,这可能是在应用程序已经运行数小时甚至数天之后。

10. 结论

本文介绍了控制反转和依赖注入的概念,并在Spring框架中进行了示例说明。

我们可以在Martin Fowler的文章中阅读更多关于这些概念的内容:

此外,我们可以在Spring框架参考文档中学习Spring对IoC和DI的实现。