Spring IoC 依赖注入:从原理到实践的深度解析
第一部分:哲学与基石——什么是 IoC?
1.1 控制反转的起源
在传统的 Java SE 编程中,对象的创建与管理由开发者手动控制(new关键字)。这种模式导致了极高的耦合度,即“好莱坞原则”(Don‘t call us, we’ll call you)的反面——对象主动去获取依赖。
IoC(Inversion of Control)将这种“控制权”进行了转移:
传统模式:
UserService service = new UserService(new UserDao());(主动创建)IoC 模式:容器负责创建和管理对象,对象只需被动接受依赖。
1.2 DI 是 IoC 的实现方式
DI(Dependency Injection,依赖注入)是 IoC 的具体落地形式。IoC 是一种设计原则,而 DI 是实现该原则的具体模式。
Spring 通过 DI 将对象之间的依赖关系从代码中剥离,转移到配置或元数据中定义。
1.3 Spring IoC 容器的核心概念
Bean:由 Spring IoC 容器管理的对象。
BeanFactory:IoC 容器的顶级接口,提供最基础的容器功能(懒加载)。
ApplicationContext:继承自 BeanFactory,提供了更强大的功能(AOP、事件传播、国际化、Web 集成),是实际开发中的首选。
第二部分:Bean 的注册与元数据
在 Spring 将对象“注入”之前,首先需要告诉容器有哪些“Bean”以及它们之间的关系。
2.1 三种配置元数据方式
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| XML 配置 | 遗留系统、第三方框架集成 | 结构清晰,修改无需编译 | 繁琐,冗长,非类型安全 |
| 注解 (Annotation) | 现代开发主流 | 简洁,类型安全,开发效率高 | 侵入性(但 Spring 鼓励) |
| Java Config ( @Configuration ) | 复杂配置、条件配置 | 纯 Java 代码,完全类型安全,灵活度最高 | 对开发者代码能力有要求 |
代码示例:Java Config 模式
java
@Configuration public class AppConfig { // 1. 显式注册 Bean @Bean public DataSource dataSource() { HikariDataSource ds = new HikariDataSource(); ds.setJdbcUrl("jdbc:mysql://localhost:test"); return ds; } // 2. 依赖注入:调用方法即表示引用 Bean @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { // 自动注入 return new JdbcTemplate(dataSource); } // 3. 组件扫描 @Bean public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { return new PropertySourcesPlaceholderConfigurer(); } }第三部分:Bean 的生命周期(深度剖析)
这是理解 Spring 原理的关键。一个 Bean 从创建到销毁,Spring 为其提供了大量的扩展点。
3.1 生命周期流程图解
3.2 关键阶段详解
实例化 (Instantiation):
Spring 通过反射(或 CGLIB)调用构造器创建一个空的对象实例。
注意:此时对象内部的属性(如
@Autowired字段)均为null。
属性填充 (Populate Properties):
Spring 处理
@Autowired、@Resource、@Value等注解。依赖注入的核心时刻:Spring 从容器中查找或创建依赖对象,通过反射设置到当前对象的字段或 setter 方法中。
Aware 接口回调:
BeanNameAware:获取 Bean 在容器中的名称。BeanFactoryAware/ApplicationContextAware:获取容器自身的引用(虽然不推荐业务代码中使用)。
BeanPostProcessor (前置处理):
这是一个强大的扩展机制。Spring 的 AOP 就是在这里利用动态代理将原始 Bean 替换为代理对象。
例如
AbstractAutoProxyCreator的postProcessAfterInitialization方法会生成代理对象。
初始化 (Initialization):
@PostConstruct注解的方法执行。InitializingBean.afterPropertiesSet()执行。自定义
init-method执行。
BeanPostProcessor (后置处理):
如果 AOP 开启,这里会将原始 Bean 包装成 Proxy 返回给容器。
3.3 实战:自定义 BeanPostProcessor 实现监控
java
@Component public class TimeCostBeanPostProcessor implements BeanPostProcessor { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) { if (bean.getClass().getName().contains("Service")) { System.out.println("Bean [" + beanName + "] 开始初始化"); } return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean.getClass().getName().contains("Service")) { // 此处可以返回代理对象 System.out.println("Bean [" + beanName + "] 初始化完成"); } return bean; } }第四部分:依赖注入的四种模式与最佳实践
4.1 构造器注入 (Constructor Injection) —— 官方推荐
Spring 4.x 之后强烈推荐,因为它是不可变的 (Immutable)且防止循环依赖(虽然构造器注入无法解决循环依赖,但强制你思考设计)。
java
@Component public class UserService { private final UserDao userDao; private final OrderDao orderDao; // 如果只有一个构造器,@Autowired 可以省略 public UserService(UserDao userDao, OrderDao orderDao) { this.userDao = userDao; this.orderDao = orderDao; } }4.2 Setter 注入
允许部分依赖注入,支持在运行时重新注入,通常用于可选依赖。
java
@Component public class MailService { private String host; @Autowired public void setHost(@Value("${mail.host}") String host) { this.host = host; } }4.3 字段注入 (Field Injection) —— 反模式
简洁但应避免在业务代码中使用。缺点是无法进行单元测试(无构造器),且容易导致NullPointerException在未完全初始化的场景。
java
@Component public class UserService { @Autowired // 不推荐 private UserDao userDao; }4.4 精确匹配:@Primary 与 @Qualifier
当一个接口有多个实现时,必须指明注入哪个。
java
// 方案一:标记首选 @Primary @Component public class H2UserDao implements UserDao {} // 方案二:指定名称 @Autowired @Qualifier("mysqlUserDao") private UserDao userDao;第五部分:循环依赖的终极解决之道(三级缓存)
循环依赖是面试中的高频难点,也是理解 Spring 底层原理的试金石。
5.1 什么是循环依赖?
Class A 依赖 B,Class B 依赖 A。
5.2 为什么 Spring 能解决(大部分)循环依赖?
Spring 依靠三级缓存和提前暴露机制解决单例模式下的 setter 注入循环依赖。
三级缓存结构 (DefaultSingletonBeanRegistry):
一级缓存 (singletonObjects):成品 Bean 的 Map。
二级缓存 (earlySingletonObjects):早期暴露的 Bean(未完全初始化,但已实例化)的 Map。
三级缓存 (singletonFactories):对象工厂(
ObjectFactory)的 Map,用于生成代理对象。
5.2.1 解决流程解析 (A依赖B,B依赖A)
创建 A:A 实例化(构造器执行完),放入三级缓存
singletonFactories。填充 A:发现 A 需要 B,去容器找 B。
创建 B:B 实例化,放入三级缓存。
填充 B:发现 B 需要 A,从容器找 A。
获取 A:B 从三级缓存中获取到 A 的
ObjectFactory,调用getObject()拿到 A 的早期引用(如果 A 需要 AOP,此时生成代理),放入二级缓存,删除三级缓存。B 完成:B 注入 A 成功,B 完成初始化,B 成为成品放入一级缓存。
A 继续:A 注入 B 成功(此时 B 已是一级缓存成品),A 完成初始化,A 成为成品放入一级缓存。
5.3 无法解决的场景:构造器注入循环依赖
如果 A 和 B 都是通过构造器注入(<constructor-arg>),Spring 会在实例化阶段就要求依赖存在,此时 Bean 尚未放入三级缓存,因此无法解决,会抛出BeanCurrentlyInCreationException。
第六部分:@Autowired 的深度解析
@Autowired是 Spring 依赖注入的核心注解,由AutowiredAnnotationBeanPostProcessor处理。
6.1 装配规则
By Type (byType):根据类型查找匹配的 Bean。
By Name (byName):如果类型匹配找到多个 Bean,则尝试根据字段名或参数名进行匹配。
@Qualifier:如果仍无法确定,使用
@Qualifier指定名称。
6.2 required 属性
@Autowired(required = false):如果依赖不存在,不注入(不抛异常),适用于非必要依赖。
6.3 集合注入
java
@Component public class PaymentService { // 注入所有 PaymentStrategy 类型的 Bean @Autowired private List<PaymentStrategy> strategies; // 注入 Map,key 为 Bean 名称,value 为 Bean 实例 @Autowired private Map<String, PaymentStrategy> strategyMap; }第七部分:Spring 4+ / Spring Boot 时代的进阶特性
7.1 条件装配 (@Conditional)
Spring Boot 自动配置的核心。只有满足特定条件时,Bean 才会被注册。
java
@Configuration public class DatabaseConfig { @Bean @ConditionalOnMissingBean // 如果用户没自己定义 DataSource,才创建默认的 @ConditionalOnClass(name = "com.mysql.cj.jdbc.Driver") public DataSource dataSource() { return new HikariDataSource(); } }7.2 FactoryBean —— 复杂对象构建
当通过简单的new无法构建对象时(如需要复杂逻辑的 SDK 客户端),实现FactoryBean。
java
@Component public class MyBeanFactoryBean implements FactoryBean<MyBean> { @Override public MyBean getObject() throws Exception { // 复杂的构建逻辑 MyBean bean = new MyBean(); bean.setComplexProperty(calculateComplex()); return bean; } @Override public Class<?> getObjectType() { return MyBean.class; } @Override public boolean isSingleton() { return true; } }7.3 延迟注入 (ObjectProvider / Provider)
解决可选依赖或处理循环依赖的备选方案。
java
@Component public class ServiceA { @Autowired private ObjectProvider<ServiceB> serviceBProvider; public void doSomething() { // 只有在真正需要时才获取 ServiceB ServiceB serviceB = serviceBProvider.getIfAvailable(); } }第八部分:源码视角的依赖注入
为了真正理解原理,我们需要窥探 Spring 源码的关键步骤。
8.1 核心方法:doCreateBean(AbstractAutowireCapableBeanFactory)
java
// 伪代码还原核心逻辑 protected Object doCreateBean(String beanName, ...) { // 1. 实例化 (调用构造器) BeanWrapper instanceWrapper = createBeanInstance(beanName, ...); Object bean = instanceWrapper.getWrappedInstance(); // 2. 是否允许早期暴露?如果是单例且允许循环依赖,加入三级缓存 boolean earlySingletonExposure = isSingleton() && allowCircularReferences; if (earlySingletonExposure) { addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, ...)); } // 3. 属性填充 (依赖注入的核心) populateBean(beanName, ...); // 4. 初始化 (Aware, PostConstruct, init-method) exposedObject = initializeBean(beanName, ...); return exposedObject; }8.2populateBean中的依赖注入
在populateBean方法中,Spring 会调用InstantiationAwareBeanPostProcessor的postProcessProperties方法。AutowiredAnnotationBeanPostProcessor会扫描 Bean 中所有带有@Autowired的字段和方法,并利用反射将容器中的依赖注入进去。
第九部分:实战陷阱与性能优化
9.1 常见陷阱
NullPointerException 在 PostConstruct 中
原因:
@PostConstruct在依赖注入之后执行,但如果在构造器里调用了@Autowired的 Bean,就会报错。解决:不要在构造器里调用依赖对象,改用
@PostConstruct。
接口多实现导致的
NoUniqueBeanDefinitionException使用
@Primary或@Qualifier解决。
同一个类内部方法调用 @Transactional 失效
原因:这是 AOP 代理导致的。内部调用
this.method()不会经过代理对象。解决:将方法拆分到不同 Bean,或使用
AopContext.currentProxy()。
静态成员变量无法注入
Spring 不允许注入静态变量。必须改为非静态,或通过 Setter 注入(但也不推荐)。
9.2 启动性能优化
懒加载 (
@Lazy):大型应用可将非核心 Bean 设为懒加载,减少启动时间。排除不必要的自动配置:在 Spring Boot 中使用
@SpringBootApplication(exclude = {...})。
第十部分:总结
Spring IoC 依赖注入的核心思想是“让容器管理对象”,其本质是一个巨大的HashMap集合(注册表)配合反射机制。
理解 Bean 的生命周期是成为 Spring 专家的必经之路。
构造器注入是最佳实践,保证了不可变性。
三级缓存是 Spring 解决单例 Setter 循环依赖的精妙设计。
BeanPostProcessor是 Spring 框架 AOP、事务管理等所有高级功能的基石。
