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

1. 概述

在本文中,我们将介绍 IoC(控制反转)和 DI(依赖注入)的概念,以及如何在Spring 框架中实现这些概念。

2. 什么是控制反转

控制反转(Inversion of Control)是软件工程中的一项原则,它将对象或程序部分的控制权转移到容器或框架中。我们最常在面向对象编程中使用它。在传统的软件开发中,程序的流程由程序员通过编写代码直接控制,而 IoC 则使框架能够控制程序的流程,并调用我们的自定义代码。为了实现这一点,框架使用了带有内置附加行为的抽象。如果我们想添加自己的行为,就需要扩展框架的类或插入自己的类。 例如,如果一个类需要依赖另一个类的实例,通常情况下,你会在这个类内部直接创建这个依赖的实例。这种方式导致了高度的耦合和代码的依赖性,使得修改和测试变得更加困难。 控制反转的核心思想是,不再由程序代码直接控制对象的创建和对象之间的调用关系,而是将这些控制权交给外部的容器或框架来管理。这样,对象的创建和它们之间的调用关系是由容器在运行时动态实现的。通过这种方式,可以减少组件间的耦合,增加程序的灵活性和可扩展性。 这种架构的优势在于

控制反转可以通过多种方式实现,例如:策略设计模式、服务定位器模式、工厂模式和依赖注入(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 接口的实现:用于独立应用程序的AnnotationConfigApplicationContext、ClassPathXmlApplicationContext 和 FileSystemXmlApplicationContext,以及用于网络应用程序的 WebApplicationContext。

为了组装 Bean,容器会使用配置元数据,其形式可以是 XML 配置或注解。 下面是一种手动实例化容器的方法:

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

下面是一个使用 AnnotationConfigApplicationContext 手动实例化容器的示例:

AnnotationConfigApplicationContext annotationContext = new AnnotationConfigApplicationContext();

当您创建 AnnotationConfigApplicationContext 的实例并为其提供一个或多个配置类时,它会扫描这些类以查找 @Bean 注解和其他相关注解。然后,它会初始化和管理这些类中定义的 Bean,设置它们的依赖关系并管理它们的生命周期。 要设置上例中的 item 属性,我们可以使用元数据。然后,容器将读取这些元数据,并在运行时使用它们来组装 Bean。

Spring 中的依赖注入可以通过构造函数、设置器或字段来实现。

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 名称将默认为方法名称。 对于使用默认单例作用域的 Bean,Spring 会首先检查是否已经存在该 Bean 的缓存实例,如果不存在,才会创建一个新实例。如果我们使用原型作用域,容器会为每个方法调用返回一个新的 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. 基于设置器的依赖注入

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

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

我们也可以使用 XML 来配置相同的beans:

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

我们可以对同一个 Bean 结合使用基于构造函数和基于设置器的注入类型。Spring 文档建议对强制依赖关系使用基于构造函数的注入,而对可选依赖关系使用基于设置器的注入。

7. 基于字段的依赖注入

在基于字段的 DI 中,我们可以通过使用@Autowired 注解:

public class Store {
    @Autowired
    private Item item;
}

在构造存储对象时,如果没有构造函数或设置器方法来注入 Item Bean,容器将使用反射将 Item 注入Store。 我们也可以使用 XML 配置来实现这一目标。

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

单一责任原则是面向对象设计中的一个概念,它表示一个类应该只有一个改变的理由,意味着它应该只有一个工作或责任。当一个类通过其构造函数注入了多个依赖项时,这可能表明该类处理了太多的关注点。管理多个构造函数参数的需要可以作为一个威慑力量,推动开发者将类重构为更小、更专注的类,每个类都处理单一责任。 相比之下,因为基于字段的DI使得添加依赖项变得如此容易,而不立即考虑它们对类设计的影响,这可能导致类变得过于复杂并做了太多事情,从而违反了SRP。在不重新评估的情况下轻松添加依赖项可能会导致代码库维护性差和耦合度高。

8. 自动布线依赖关系

布线允许 Spring 容器通过检查已定义的 Bean 自动解决协作 Bean 之间的依赖关系。 使用 XML 配置为 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 配置按类型自动连接beans:

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

接下来,让我们通过 XML 向存储 Bean 的 item 属性注入一个名为 item 的 Bean:

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

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

我们还可以通过构造函数参数或设置器明确定义依赖关系,从而覆盖自动布线功能。

9. Lazy Initialized Beans

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

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

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

原文:

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