Spring 依赖注入(Dependency Injection,DI)是 Spring 框架的核心特性之一,它负责管理对象之间的依赖关系,降低了代码的耦合度,提高了代码的可测试性和可维护性。Spring 实现依赖注入的方式主要有以下几种,并结合底层原理进行说明:
1. 构造器注入(Constructor Injection):
- 原理: Spring 通过调用 Bean 的构造方法来注入依赖。
- 实现方式:
- XML 配置: 使用
<constructor-arg>
标签。 - 注解: 使用
@Autowired
注解(可以省略,Spring 4.3+ 支持在构造方法上隐式注入)。 - JavaConfig: 在
@Bean
方法中使用参数。
- XML 配置: 使用
- 优点:
- 依赖关系在对象创建时就确定,保证了对象在使用时所有依赖都已就绪。
- 依赖不可变(immutable),提高了对象的安全性。
- 更容易发现依赖问题(编译时就能发现)。
- 缺点:
- 当依赖较多时,构造方法参数列表会很长,不够灵活。
- 不适用于有多个可选依赖的情况。
示例:
public class MyService {private MyRepository myRepository;// 构造器注入@Autowired // 可省略public MyService(MyRepository myRepository) {this.myRepository = myRepository;}
}
2. Setter 方法注入(Setter Injection):
- 原理: Spring 通过调用 Bean 的 setter 方法来注入依赖。
- 实现方式:
- XML 配置: 使用
<property>
标签。 - 注解: 在 setter 方法上使用
@Autowired
注解。 - JavaConfig: 在
@Bean
方法中调用 setter 方法。
- XML 配置: 使用
- 优点:
- 更灵活,可以在对象创建后修改依赖。
- 适用于可选依赖。
- 缺点:
- 依赖关系在对象创建后才建立,可能导致在使用时依赖尚未注入(需要进行 null 检查)。
- 依赖可变,可能导致对象状态不一致。
示例:
public class MyService {private MyRepository myRepository;// Setter 方法注入@Autowiredpublic void setMyRepository(MyRepository myRepository) {this.myRepository = myRepository;}
}
3. 字段注入(Field Injection):
- 原理: Spring 通过反射直接设置 Bean 的字段值来注入依赖。
- 实现方式:
- 注解: 在字段上使用
@Autowired
注解。
- 注解: 在字段上使用
- 优点:
- 最简洁,代码量最少。
- 缺点:
- 依赖关系不明确,可读性较差。
- 破坏了封装性,可以直接修改私有字段。
- 难以进行单元测试(需要使用反射来模拟依赖)。
- 容易产生循环依赖
示例:
public class MyService {// 字段注入@Autowiredprivate MyRepository myRepository;
}
4. 方法注入(Method Injection,不常用):
- 通过Lookup方法或者CGLIB代理,在每次调用方法时,获取新的实例。
底层原理(简化版):
- BeanDefinition 解析: Spring 容器启动时,会解析配置文件(XML、注解、JavaConfig),将 Bean 的定义信息(类名、依赖关系、作用域等)封装成
BeanDefinition
对象。 - Bean 实例化: 当需要获取 Bean 时,Spring 根据
BeanDefinition
中的信息,通过反射创建 Bean 的实例。 - 依赖注入:
- 构造器注入: Spring 根据
BeanDefinition
中定义的构造方法参数,从容器中查找或创建相应的依赖对象,然后调用构造方法创建 Bean 实例。 - Setter 方法注入: Spring 根据
BeanDefinition
中定义的属性和 setter 方法,从容器中查找或创建相应的依赖对象,然后调用 setter 方法注入依赖。 - 字段注入: Spring 根据
BeanDefinition
中定义的字段和@Autowired
注解,从容器中查找或创建相应的依赖对象,然后通过反射直接设置字段值。
- 构造器注入: Spring 根据
- Bean 初始化: 依赖注入完成后,Spring 会调用 Bean 的初始化方法(如果有)。
- Bean 放入容器: 初始化完成后,Spring 将 Bean 放入容器中,供应用程序使用。
循环依赖问题:
- 什么是循环依赖: 当两个或多个 Bean 相互依赖时,就会形成循环依赖。例如,A 依赖 B,B 依赖 A。
- Spring 如何解决:
-
构造器注入的循环依赖无法解决,会抛出异常。
-
Setter 方法注入和字段注入的循环依赖,Spring 可以通过“三级缓存”机制解决。
- 一级缓存(singletonObjects): 存放完全初始化好的单例 Bean。
- 二级缓存(earlySingletonObjects): 存放早期暴露的 Bean 实例(已创建但未完全初始化)。
- 三级缓存(singletonFactories): 存放创建 Bean 实例的
ObjectFactory
。
-
解决过程(简化版):
- 创建 A 实例,将 A 的
ObjectFactory
放入三级缓存,并暴露一个早期的 A 实例到二级缓存。 - A 注入 B,发现 B 不存在,开始创建 B 实例。
- 创建 B 实例,将 B 的
ObjectFactory
放入三级缓存,并暴露一个早期的 B 实例到二级缓存。 - B 注入 A,从二级缓存中获取早期的 A 实例(虽然 A 还未完全初始化,但 B 可以先持有 A 的引用)。
- B 完成初始化,放入一级缓存。
- A 继续初始化,注入 B(从一级缓存中获取),完成初始化,放入一级缓存。
- 创建 A 实例,将 A 的
-
总结:
Spring 通过多种方式实现依赖注入,包括构造器注入、Setter 方法注入和字段注入。底层原理是基于反射和 BeanDefinition
对象。Spring 通过“三级缓存”机制解决了 Setter 方法注入和字段注入的循环依赖问题。