您的当前位置:首页《Spring 依赖注入方式全解析》

《Spring 依赖注入方式全解析》

2023-03-11 来源:小侦探旅游网

一、Spring 依赖注入概述

Spring 依赖注入(Dependency Injection,DI)是一种重要的设计模式,它在 Spring 框架中扮演着关键角色。依赖注入的核心概念是将对象所需的依赖关系由外部容器(通常是 Spring 容器)进行管理和注入,而不是让对象自己去创建和管理依赖。

这种方式具有极大的重要性。首先,它实现了解耦。在传统的编程方式中,对象之间的依赖关系通常是在对象内部通过直接实例化来建立的,这会导致对象之间高度耦合,难以维护和扩展。而依赖注入使得对象只关注自身的核心业务逻辑,不需要关心依赖对象的创建和获取方式,从而降低了对象之间的耦合度。

例如,在一个企业级应用中,一个业务服务类可能依赖于数据访问层的某个 DAO(Data Access Object)。如果采用传统方式,业务服务类需要自己实例化 DAO 对象,这样一旦 DAO 的实现发生变化,业务服务类也需要相应修改。但通过依赖注入,业务服务类只需要声明对 DAO 的依赖,由 Spring 容器在运行时将合适的 DAO 实例注入到业务服务类中,大大提高了代码的可维护性。

此外,依赖注入还提高了代码的可测试性。在单元测试中,可以轻松地替换依赖对象,模拟不同的场景,而不需要实际创建复杂的依赖关系。

总之,Spring 依赖注入通过将对象的依赖关系外部化,实现了解耦和可维护性,是 Spring 框架中不可或缺的一部分。

二、常见的依赖注入方式

(一)属性注入

属性注入是一种常见的依赖注入方式,它通过 set 方法注入 Bean 的属性值或依赖对象。这种方式具有很高的灵活性,因为可以在对象实例化后根据需要动态地设置属性值。

例如,在一个 Java 项目中,有一个名为UserService的服务类,它依赖于一个UserRepository接口的实现类来进行用户数据的操作。如果使用属性注入,可以在UserService类中定义一个UserRepository类型的属性,并提供对应的 set 方法。在 Spring 配置文件中,可以通过<property>标签将具体的UserRepository实现类注入到UserService中。

属性注入的优点在于灵活性高,可以根据不同的情况在运行时动态地设置属性值。同时,对于一些可选的依赖关系,也可以在需要的时候进行注入,而不是在对象实例化时强制注入。

(二)构造函数注入

构造函数注入是在对象实例化时,通过构造函数设置必要的属性。这种方式确保了对象实例化后即可使用,因为所有必要的依赖都在对象创建时被注入。

以一个学生管理系统为例,有一个Student类,它有name、age和grade等属性。如果使用构造函数注入,可以在Student类的构造函数中接收这些属性的值,并在对象创建时进行初始化。在 Spring 配置文件中,可以使用<constructor-arg>标签来指定构造函数的参数值。

构造函数注入的优点在于可以确保对象在创建时就处于一个完整的状态,避免了在使用对象之前可能出现的未初始化状态。同时,它也使得对象的依赖关系更加明确,因为在构造函数中可以清楚地看到对象所依赖的所有资源。

(三)Setter 方法注入

Setter 方法注入是在对象实例化后,通过调用 setter 方法实现依赖注入。这种方式使得依赖关系成为可选的,因为可以在需要的时候才进行注入。

比如在一个电商系统中,有一个Order类,它依赖于一个PaymentService类来处理支付操作。如果使用 Setter 方法注入,可以在Order类中定义一个PaymentService类型的属性,并提供对应的 setter 方法。在需要进行支付操作时,可以通过 Spring 容器调用 setter 方法将PaymentService实例注入到Order对象中。

Setter 方法注入的优点在于灵活性高,可以根据不同的业务场景在运行时动态地注入依赖关系。同时,对于一些可选的依赖,也可以在需要的时候进行注入,而不会在对象实例化时强制注入不必要的依赖。

三、其他依赖注入方式

(一)基于注解的自动装配

@Autowired注解是 Spring 框架中用于自动装配的重要注解之一。它可以应用于构造器、字段和方法注入。

在构造器注入中,当@Autowired注解用于构造器时,Spring 会在创建 Bean 实例时自动调用该构造器,并为其参数注入对应类型的实例。例如:

@Service

public class DriverServiceImpl implements DriverService {

    private DriverDao driverDao;

    @Autowired

    public DriverServiceImpl(DriverDao driverDao) {

        this.driverDao = driverDao;

    }

}

在接口注入中,@Autowired可以用于接口的实现类,自动注入实现接口的具体对象。

在方法注入中,如果方法有参数,会使用@Autowired的方式在容器中查找是否有该参数,并执行该方法。比如:


@Autowired

public void commonMethod(Bean04 bean04){

    System.out.println("普通方法的执行");

}

@Autowired默认按类型注入。这意味着 Spring 容器会自动查找与所需类型匹配的 Bean 进行注入。此时,要求 Spring 容器中有且仅有一个合适的 Bean 为其赋值。但如果项目中有多个 Bean 可以赋值,则会发生错误。可以通过结合@Qualifier注解来指定具体的 Bean 名称进行注入,避免这种错误。

(二)Resource 注入

@Resource注解是 Java 标准(JSR-250)提供的注解,Spring 也支持该注解。它主要有name和type两个重要属性。

如果同时指定了name和type,则从 Spring 上下文中找到唯一匹配的 bean 进行装配,找不到则抛出异常。例如:


@Resource(name = "myBean", type = MyBean.class)

private MyBean myBean;

如果指定了name,则从上下文中查找名称(id)匹配的 bean 进行装配,找不到则抛出异常。

如果指定了type,则从上下文中找到类型匹配的唯一 bean 进行装配,找不到或者找到多个,都会抛出异常。

如果既没有指定name,又没有指定type,则自动按照 byName 方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。

(三)接口注入

接口注入是一种通过接口来实现依赖注入的方式。在接口中定义要注入的信息,然后通过实现该接口的类来完成注入。

例如:


public class ClassA {

    private InterfaceB clzB;

    public init() {

        Object obj = Class.forName(Config.BImplementation).newInstance();

        clzB = (InterfaceB)obj;

    }

}

在这种方式中,通过接口将调用者与实现者分离,提高了代码的可维护性和可扩展性。但接口注入模式因为具备侵入性,它要求组件必须与特定的接口相关联,因此并不被看好,实际使用有限。

四、不同注入方式对比

(一)可靠性

  • 属性注入:不可靠。属性注入是在对象实例化后通过 set 方法进行注入,这意味着在对象使用过程中,属性可能会被意外修改,导致对象状态不可预测。
  • 构造函数注入:可靠。构造函数注入在对象实例化时就将所有必要的依赖注入,一旦对象创建完成,其依赖关系就不会再发生变化,保证了对象的稳定性和可靠性。
  • Setter 方法注入:不可靠。虽然 Setter 方法注入可以在对象实例化后进行依赖注入,但这也使得对象的依赖关系可以在运行时被随意修改,增加了对象状态的不确定性。

(二)可维护性

  • 属性注入:差。属性注入的依赖关系不明显,难以直接从代码中看出对象的依赖关系,不利于代码的维护和理解。
  • 构造函数注入:好。构造函数中明确列出了对象所依赖的资源,使得依赖关系一目了然,方便开发者进行代码维护和分析。
  • Setter 方法注入:差。Setter 方法注入的依赖关系也不够直观,需要通过查看 setter 方法才能确定对象的依赖关系,增加了维护的难度。

(三)可测试性

  • 属性注入:差。在进行单元测试时,由于属性注入的对象可能会受到外部环境的影响,难以进行有效的模拟和控制,导致测试难度较大。
  • 构造函数注入:好。构造函数注入使得对象的依赖关系在创建时就确定,在单元测试中可以方便地通过构造函数传入模拟的依赖对象,进行测试。
  • Setter 方法注入:好。Setter 方法注入可以在测试时根据需要设置不同的依赖对象,方便进行各种场景的测试。

(四)灵活性

  • 属性注入:很灵活。属性注入可以在对象实例化后根据需要动态地设置属性值,对于一些可选的依赖关系非常方便。但这种灵活性也可能导致代码的混乱和不可控。
  • 构造函数注入:不灵活。构造函数注入在对象创建时就确定了依赖关系,不能在运行时进行修改,缺乏一定的灵活性。
  • Setter 方法注入:很灵活。Setter 方法注入可以在对象实例化后根据业务需求动态地注入依赖关系,具有较高的灵活性。但也可能导致依赖关系的不明确和代码的复杂性。

(五)循环关系检测

  • 属性注入:不检测。属性注入方式不会自动检测 Bean 之间的循环依赖关系,可能会导致应用程序出现问题而难以排查。
  • 构造函数注入:自动检测。构造函数注入在对象创建时会自动检测循环依赖关系,如果存在循环依赖,会抛出异常,便于开发者及时发现和解决问题。
  • Setter 方法注入:不检测。Setter 方法注入也不会自动检测循环依赖关系,可能会导致应用程序出现死锁等问题。

(六)性能表现

  • 属性注入:启动快。属性注入在启动时不需要进行复杂的依赖关系处理,启动速度相对较快。
  • 构造函数注入:启动慢。构造函数注入需要在对象创建时处理所有的依赖关系,这可能会导致启动时间延长。
  • Setter 方法注入:启动快。Setter 方法注入在对象实例化时不需要处理依赖关系,可以在需要时进行注入,启动速度相对较快。

五、Spring 官方推荐及原因

Spring 官方推荐构造器注入,这一推荐有多个重要原因。

首先,IDEA 警告提示 “Field injection is not recommended”,即不建议使用属性注入(字段注入)。这是因为属性注入存在一些弊端。属性注入是通过在类的变量上使用注解进行依赖注入,本质上是通过反射的方式直接注入到字段。虽然这种方式非常简洁,代码看起来简单易懂,类可以专注于业务而不被依赖注入所污染,只需要把注解扔到变量之上就好,不需要特殊的构造器或者 set 方法,依赖注入容器会提供所需的依赖。但是,成也萧何败也萧何,属性注入也会引发很多问题。

一方面,容易违背单一职责原则。使用属性注入方式,添加依赖很简单,普通开发者很可能会无意识地给一个类添加很多依赖,而当使用构造器方式注入,到了某个特定的点,构造器中的参数变得太多以至于很明显地发现 something is wrong。拥有太多的依赖通常意味着类要承担更多的责任,明显违背了单一职责原则。

另一方面,属性注入会导致依赖注入与容器本身耦合。具体表现为类和依赖容器强耦合,不能在容器外使用;不能绕过反射(例如单元测试的时候)进行实例化,必须通过依赖容器才能实例化,这更像是集成测试;不能使用属性注入的方式构建不可变对象(final 修饰的变量)。

其次,Spring 开发团队建议 “Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies”,即在 beans 中永远使用基于构造器的依赖注入,对于必须的依赖,永远使用断言来确认。构造器注入有以下几个好处:

  1. 易于理解和维护:构造器注入提供了清晰的依赖关系,使代码更易于理解和维护。构造函数参数直观地表示了类所需的依赖项,降低了代码的复杂性。比如PaymentProcessor依赖于PaymentGateway,通过构造器注入一目了然。
  1. 依赖注入的一致性:构造器注入鼓励将所有依赖项都放在构造函数中,从而确保类的实例在被创建时处于一致的状态。这有助于避免在使用对象时遇到空指针异常或未初始化的依赖项。
  1. 不可变性:通过使用final关键字,构造器注入可以实现不可变性,这意味着一旦依赖项被设置,它们不能再被修改。这可以提高代码的安全性和稳定性。例如ShoppingCart类通过构造器注入一个不可变的List<Item>。
  1. 依赖项解析:构造器注入使依赖项的解析变得更加明确。当容器创建 Bean 实例时,容器只需查找所需的构造函数参数,而不需要进行复杂的解析或猜测。
  1. 避免循环依赖:构造器注入有助于避免循环依赖问题,因为在创建 Bean 实例时,构造函数参数必须已经可用。这有助于减少潜在的运行时错误。

综上所述,Spring 官方推荐构造器注入是出于提高代码质量、可测试性和可维护性的考虑。

因篇幅问题不能全部显示,请点此查看更多更全内容