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 方法实现依赖注入。这种方式使得依赖关系成为可选的,因为可以在需要的时候才进行注入。
比如在一个电商系统中,有一个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注解是 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;
}
}
在这种方式中,通过接口将调用者与实现者分离,提高了代码的可维护性和可扩展性。但接口注入模式因为具备侵入性,它要求组件必须与特定的接口相关联,因此并不被看好,实际使用有限。
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 中永远使用基于构造器的依赖注入,对于必须的依赖,永远使用断言来确认。构造器注入有以下几个好处:
综上所述,Spring 官方推荐构造器注入是出于提高代码质量、可测试性和可维护性的考虑。
因篇幅问题不能全部显示,请点此查看更多更全内容