Spring 中的控制反转(IoC)和依赖注入(DI)

1、概览

本文将带你了解 IoC(控制反转)和 DI(依赖注入)的概念,以及如何在 Spring 中实现这些概念。

2、控制反转(IoC)是什么?

控制反转(Inversion of Control)是软件工程中的一项原则,它将对象或程序部分的控制权转移到容器或框架中。在面向对象的编程中经常使用。

也有人叫 “反转控制”。

在传统编程中,我们使用自定义代码来调用库,而 IoC 则使框架能够控制程序的流程,并调用我们的自定义代码。框架使用了带有内置附加行为的抽象类来实现这一点。如果我们想添加自己的行为,就需要继承框架的类或插入自己的类。

这种架构的优势在于:

  • 将任务的执行与任务的实现分离开来
  • 更容易在不同的实现之间切换
  • 程序的模块化程度更高
  • 通过隔离组件或模拟其依赖,并允许组件通过 “约定” 进行通信,从而更轻松地测试程序

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

3、依赖注入(DI)是什么?

依赖注入是可以用来实现 IoC 的一种模式,其中被反转的控制是 设置对象的依赖

将对象与其他对象 “装配” 起来,或将对象 “注入” 到其他对象中是由组装器(Assembler)而不是对象本身来完成的。

下面是我们在传统编程中创建依赖对象的方法:

public class Store {
    private Item item;
 
    public Store() {
        item = new ItemImpl1();     // 实例化所需的依赖
    }
}

重写示例,通过使用 DI,而无需指定 Item 的实现:

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

在接下来的章节中,将带你了解如何通过元数据(Metadata)来实现 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 Scope(默认)的 Bean,Spring 会首先检查是否已经存在该 Bean 的缓存实例,如果不存在,才会创建一个新实例。如果使用 prototype Scope,容器会为每个方法调用返回一个新的 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、基于字段的依赖注入

在基于字段的依赖注入,可以通过 @Autowired 注解来注入依赖:

public class Store {
    @Autowired
    private Item item; 
}

在构造 Store 对象时,如果没有构造函数或 Setter 方法来注入 Item Bean,容器将使用反射将 Item 注入到 Store(也可以使用 XML 配置来实现)。

这种方法可能看起来更简单、更干净,但不建议使用,因为它有一些缺点,例如:

  • 该方法使用反射来注入依赖,这比基于构造函数或基于 Setter 注入方法的成本更高。
  • 使用这种方法很容易会不断地添加多个依赖。如果使用构造函数注入,过多的参数会让我们意识到该类做了不止一件事,这可能会违反 “单一责任原则”。

8、自动装配

通过检查已定义的 Bean,装配(Wiring)允许 Spring 容器自动解析协作 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 起,autowrie 属性已被弃用。

还可以使用 @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 向 store Bean 的 item 属性注入一个名为 item 的 Bean:

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

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

还可以通过构造函数参数或 Setter 方法明确定义依赖,从而覆盖自动装配功能。

9、 Bean 的延迟初始化

默认情况下,容器会在初始化过程中创建和配置所有单例(singleton) Bean。为了避免这种情况,我们可以在 Bean 配置中使用值为 truelazy-init 属性:

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

如上,item1 Bean 只会在首次请求时初始化,而不会在启动时初始化。这样做的好处是初始化时间更快,但代价是在请求 Bean 后才会发现任何配置错误,而这可能是在应序运行数小时甚至数天后。

10、总结

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

此外,你还可以在 Spring 中文文档 中了解 IoC 和 DI 的 Spring 实现。


Ref:https://www.baeldung.com/inversion-control-and-dependency-injection-in-spring