Spring IoC 控制反转研究:从对象创建到容器生命周期

举报
yd_236998400 发表于 2026/10/26 09:25:27 2026/10/26
【摘要】 Spring IoC 控制反转研究:从对象创建到容器生命周期Spring 最重要的能力,不是帮我们少写几个 new,而是把对象创建、依赖组装、生命周期管理和扩展逻辑从业务代码中抽离出来,交给一个可扩展的容器统一协调。理解 IoC,应该从“控制权到底转移了什么”开始,而不是停留在“自动注入依赖”这句口号上。 一、为什么要研究 IoC在一个小型程序里,直接创建对象并不难:public clas...

Spring IoC 控制反转研究:从对象创建到容器生命周期

Spring 最重要的能力,不是帮我们少写几个 new,而是把对象创建、依赖组装、生命周期管理和扩展逻辑从业务代码中抽离出来,交给一个可扩展的容器统一协调。理解 IoC,应该从“控制权到底转移了什么”开始,而不是停留在“自动注入依赖”这句口号上。

一、为什么要研究 IoC

在一个小型程序里,直接创建对象并不难:

public class OrderService {
    private final OrderRepository repository = new JdbcOrderRepository();
}

但随着系统规模变大,这段代码会逐渐暴露出几个问题:

  • OrderService 依赖了具体实现 JdbcOrderRepository,替换为远程实现、缓存实现或测试替身时需要修改业务代码。
  • 对象的创建时机、构造参数和生命周期散落在各个类中,无法统一管理。
  • 依赖关系隐藏在实现内部,阅读类的构造函数时看不出它需要哪些协作者。
  • 测试时很难替换基础设施,容易被迫启动数据库、消息队列等外部资源。
  • 日志、事务、权限等横切逻辑如果直接写进业务类,会让核心代码越来越臃肿。

IoC 的研究价值,就在于解释 Spring 如何把这些控制权收回容器,并且让业务对象保持相对纯粹。

★ Insight ─────────────────────────────────────

  • IoC 解决的不是“对象如何被注入”这么一个局部问题,而是“谁负责决定对象的创建、组装和生命周期”。
  • DI 是 IoC 的主要实现方式,但 IoC 不等于注解注入;构造器、工厂方法、配置文件和手工注册都可以实现控制反转。
  • Spring 的关键抽象不是 @Autowired,而是 BeanDefinition。注解只是把元数据转成 Bean 定义,容器再根据定义完成后续工作。

─────────────────────────────────────────────────

本文以 Spring Framework 6 / Spring Boot 3 的常见用法为背景。源码中的具体方法名可能随版本演进,但整体职责划分相对稳定。

二、IoC、DI 与容器:三个概念不要混为一谈

2.1 IoC:控制权发生了转移

传统程序由业务对象主动控制依赖的创建:

public class OrderService {
    private final OrderRepository repository;

    public OrderService() {
        this.repository = new JdbcOrderRepository();
    }
}

这里的控制关系是:OrderService 决定使用哪个实现,也决定什么时候创建它。

使用 IoC 后,类只表达自己需要什么:

public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

OrderService 不再负责创建 OrderRepository,创建和组装工作由外部容器完成。控制权从业务对象转移到容器,这就是 Inversion of Control。

2.2 DI:依赖被外部注入

Dependency Injection 是实现 IoC 的一种具体方式。依赖可以通过多种入口传入:

  • 构造器注入:对象创建时传入必需依赖。
  • Setter 注入:对象创建后通过方法设置可选依赖。
  • 字段注入:容器通过反射直接设置字段。
  • 方法注入:容器调用配置方法,把依赖作为参数传入。

现代 Spring 项目更推荐构造器注入:

@Service
public class OrderService {
    private final OrderRepository repository;
    private final PaymentGateway paymentGateway;

    public OrderService(
            OrderRepository repository,
            PaymentGateway paymentGateway) {
        this.repository = repository;
        this.paymentGateway = paymentGateway;
    }
}

构造器注入有几个实际优势:依赖不可变、对象创建后状态完整、依赖关系显式、单元测试不需要启动 Spring 容器。单构造器场景下,Spring 通常不要求额外写 @Autowired。

2.3 容器:IoC 的运行时执行者

Spring 中常见的容器层次如下:

BeanFactory
    └── ListableBeanFactory
          └── ApplicationContext
                └── AnnotationConfigApplicationContext
                └── ClassPathXmlApplicationContext
                └── Spring Boot 的应用上下文

BeanFactory 提供最基本的 Bean 创建和获取能力。ApplicationContext 在此基础上增加了事件发布、国际化、资源加载、环境配置以及自动发现后处理器等完整应用能力。

实际应用中,我们通常不直接调用 new 创建业务对象,而是让配置类、组件扫描和自动配置把组件注册到 ApplicationContext 中:

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

三、BeanDefinition:容器手里的“对象蓝图”

Spring 不会在扫描到 @Service 后立刻把所有对象都随手创建出来。更准确的过程是:先把类上的信息解析成 BeanDefinition,再根据这些定义创建 Bean。

一个 BeanDefinition 可以理解为对象的生产说明书,通常包含:

  • Bean 的类型或工厂方法信息;
  • Bean 名称和别名;
  • 作用域,例如 singleton、prototype 或 Web 作用域;
  • 是否延迟初始化;
  • 构造参数和属性依赖;
  • 初始化方法和销毁方法;
  • 是否参与自动代理和其他后处理流程。

例如下面的配置:

@Configuration
public class PaymentConfig {
    @Bean
    public PaymentGateway paymentGateway(PaymentProperties properties) {
        return new HttpPaymentGateway(properties.endpoint());
    }
}

Spring 需要记录的不只是 PaymentGateway 这个返回类型,还包括工厂方法是 paymentGateway、工厂对象是 PaymentConfig、方法参数需要解析 PaymentProperties,以及这个 Bean 的默认作用域等信息。

组件扫描的本质也类似:扫描器发现带有 @Component、@Service、@Repository 等注解的类,把它们转换为 BeanDefinition,注册到 BeanDefinitionRegistry。注册完成后,容器才有了批量创建对象的依据。

四、容器启动:refresh() 做了什么

Spring 容器的启动流程很长,但可以先抓住主线。以 AbstractApplicationContext#refresh() 为观察入口,大致可以抽象成下面的步骤:

准备并刷新应用上下文
    ↓
创建或准备 BeanFactory
    ↓
加载并注册 BeanDefinition
    ↓
注册 BeanFactoryPostProcessor
    ↓
执行 BeanFactoryPostProcessor,修改 BeanDefinition
    ↓
注册 BeanPostProcessor
    ↓
初始化消息源、事件广播器等上下文组件
    ↓
实例化非懒加载的单例 Bean
    ↓
发布 ContextRefreshedEvent

4.1 先注册定义,再创建实例

这两个阶段的分离非常关键:

  1. 扫描和配置解析阶段只负责收集元数据。
  2. BeanFactoryPostProcessor 可以在实例化前调整 BeanDefinition。
  3. 容器根据最终定义创建 Bean,并执行依赖解析和生命周期回调。

这也是为什么 Spring 可以实现配置占位符、自动配置、条件装配等能力。容器先拥有一张完整的“对象地图”,再决定哪些对象真正需要被创建。

4.2 BeanFactoryPostProcessor 与 BeanPostProcessor

两类后处理器名字很像,但处理时机不同:

类型 处理对象 典型职责
BeanFactoryPostProcessor BeanDefinition / BeanFactory 元数据 修改属性、解析占位符、注册额外定义
BeanPostProcessor 已创建的 Bean 实例 属性注入、初始化回调、生成代理对象

可以用一句话记忆:BeanFactoryPostProcessor 改“生产说明书”,BeanPostProcessor 改“生产出来的对象”。

AutowiredAnnotationBeanPostProcessor 负责处理 @Autowired、@Value 等注入注解;AbstractAutoProxyCreator 则会根据切面和匹配规则决定是否为 Bean 创建 AOP 代理。

五、一个 Bean 是如何被创建出来的

从 AbstractAutowireCapableBeanFactory 相关逻辑看,一个普通 Bean 的创建过程可以概括为:

根据 BeanName 获取 Bean
    ↓
检查单例缓存
    ↓
确定 Bean 类型与构造器
    ↓
实例化原始对象
    ↓
提前暴露单例工厂(必要时)
    ↓
填充属性和解析依赖
    ↓
执行 Aware 回调
    ↓
执行初始化前的 BeanPostProcessor
    ↓
执行初始化方法
    ↓
执行初始化后的 BeanPostProcessor
    ↓
返回最终对象或代理对象

5.1 实例化:构造器不是容器魔法,而是依赖解析结果

对于构造器注入,Spring 会先找出候选构造器,再解析构造器参数。假设:

public OrderService(OrderRepository repository) {
    this.repository = repository;
}

容器并不是简单地反射调用无参构造器,而是要回答几个问题:

  • OrderRepository 有几个候选实现?
  • 是否有 @Primary 指定默认实现?
  • 是否有 @Qualifier 限定 Bean 名称?
  • 依赖本身是否已经创建?
  • 是否形成了无法解析的循环依赖?

解析成功后,Spring 才会把依赖实例作为参数传给构造器。

5.2 属性填充:注解注入发生在 Bean 创建过程中

字段注入看起来很简单:

@Service
public class ReportService {
    @Autowired
    private ReportRepository repository;
}

但运行时需要通过 BeanPostProcessor 找到注入点、解析依赖、反射写入字段,并处理必要的限定条件。字段注入把依赖隐藏在类内部,也让对象脱离容器后处于不完整状态,因此通常不如构造器注入清晰。

5.3 初始化:对象可用之前还有一串回调

Bean 初始化阶段可能涉及:

  • BeanNameAware、BeanFactoryAware、ApplicationContextAware 等 Aware 接口;
  • @PostConstruct 标记的方法;
  • InitializingBean#afterPropertiesSet();
  • 自定义 init-method;
  • BeanPostProcessor#postProcessBeforeInitialization();
  • BeanPostProcessor#postProcessAfterInitialization()。

这些回调不是越多越好。业务初始化逻辑应该尽量集中,避免一个 Bean 同时实现多个生命周期接口,再叠加注解和 XML 配置,最后连创建顺序都没人敢动。

六、三级缓存与循环依赖:最容易被讲错的部分

Spring 常见的单例循环依赖场景是属性注入:

@Component
public class A {
    @Autowired
    private B b;
}

@Component
public class B {
    @Autowired
    private A a;
}

Spring 为单例 Bean 维护了几层缓存,常见概念包括:

  • 一级缓存:完整创建并初始化后的单例对象。
  • 二级缓存:提前暴露的早期单例引用。
  • 三级缓存:用于生成早期引用的 ObjectFactory,可在需要时创建代理引用。

简化后的流程是:创建 A 时发现需要 B;创建 B 时又发现需要 A。此时 A 还没有完全初始化,但容器可以通过三级缓存获取 A 的早期引用,把它注入 B,然后完成 B,再回到 A 完成属性填充。

这里有三个边界必须记住:

  1. 这种机制主要针对单例和属性 / Setter 注入。
  2. 构造器循环依赖无法靠“提前暴露半成品对象”解决,因为对象连构造都没有完成。
  3. prototype Bean 不享受同样的单例缓存机制,循环依赖通常会直接失败。

因此,循环依赖能被容器“处理”不代表设计合理。它往往说明两个类承担的职责耦合过深,拆分领域服务、引入更稳定的抽象通常比依赖容器兜底更好。

★ Insight ─────────────────────────────────────

  • 三级缓存的重点不只是“缓存了三个 Map”,而是为了在 Bean 尚未完成初始化时,仍然能为 AOP 代理等扩展点提供早期引用。
  • 循环依赖之所以表现得时好时坏,通常取决于注入方式、作用域和是否需要代理;不能只记“Spring 能解决循环依赖”这句半真半假的结论。
  • 设计上真正应该追问的是“为什么两个对象必须互相持有”,而不是“怎样把容器调到能启动”。能启动不等于依赖关系健康。

─────────────────────────────────────────────────

七、IoC 与 AOP 为什么能自然结合

IoC 容器掌握了 Bean 的创建过程,因此可以在对象返回给业务代码之前插入代理:

@Service
public class PaymentService {
    @Transactional
    public void pay(Long orderId) {
        // payment logic
    }
}

业务代码注入的对象,可能并不是 PaymentService 原始实例,而是 JDK Dynamic Proxy 或 CGLIB 代理。代理在调用目标方法前后加入事务开启、提交和回滚逻辑,真正的目标对象则由容器继续管理。

这解释了几个常见现象:

  • @Transactional 通常只对经过代理的外部调用生效。
  • 同一个类中通过 this.method() 进行内部调用,可能绕过代理。
  • 使用 JDK 动态代理时,注入点通常应面向接口;CGLIB 则基于子类生成代理。
  • 从容器里拿到的 Bean 类型可能是代理类型,直接比较实现类或强制转换时要留意。

IoC 是 AOP 的基础设施。没有统一的 Bean 创建和返回过程,事务、缓存、权限、异步等横切能力就很难以一致方式织入。

八、Bean 的作用域与生命周期

Spring 默认作用域是 singleton,但它不是 JVM 级别的全局单例,而是“在一个 ApplicationContext 内共享一个实例”:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ExportTask {
}

常见作用域包括:

作用域 含义 适用场景
singleton 每个容器一个实例 无状态服务、Repository、配置组件
prototype 每次获取时创建新实例 有短生命周期状态的对象
request 每个 HTTP 请求一个实例 Web 请求上下文数据
session 每个 HTTP Session 一个实例 会话级状态
application 每个 Web 应用上下文一个实例 应用级 Web 组件

需要特别注意:把 prototype Bean 注入 singleton Bean 时,注入动作通常只发生一次,后续并不会因为每次调用方法就自动生成新对象。若确实需要按使用时机创建,可以考虑 ObjectProvider<T>、Provider<T> 或显式工厂。

销毁阶段也有对应机制:@PreDestroy、DisposableBean#destroy() 和自定义 destroy-method 都可以参与容器关闭流程。非容器创建的对象不会自动享受这些生命周期管理能力,这也是“把对象交给 Spring 管理”这句话的实际含义。

九、三种注册方式与适用边界

9.1 组件扫描

适合应用内部由自己控制的业务组件:

@Component
public class TaxCalculator {
}

@Service
public class InvoiceService {
    private final TaxCalculator taxCalculator;

    public InvoiceService(TaxCalculator taxCalculator) {
        this.taxCalculator = taxCalculator;
    }
}

优点是简洁,缺点是类和 Spring 注解产生了直接联系,扫描范围、命名规则和条件装配也需要清楚管理。

9.2 Java 配置与 @Bean

适合第三方类、复杂构造过程和需要明确装配关系的对象:

@Configuration
public class ClientConfiguration {
    @Bean
    public HttpClient httpClient(ClientProperties properties) {
        return HttpClient.newBuilder()
                .connectTimeout(properties.connectTimeout())
                .build();
    }
}

这种方式让创建逻辑显式可见,也适合把外部库对象纳入容器。

9.3 编程式注册

在框架扩展、插件系统或动态模块中,可以直接操作注册表:

registry.registerBeanDefinition("dynamicHandler", definition);

它的灵活性最高,但也最容易把容器变成隐藏的全局状态。业务代码一般不应到处直接操作 BeanDefinitionRegistry。

十、常见误区与工程判断

10.1 把 IoC 理解成“少写 new”

少写 new 只是表象。真正的收益是依赖关系外置、生命周期统一、扩展点集中。若只是把 new 换成 applicationContext.getBean(),控制权仍然在业务代码手里,只是从显式依赖变成了字符串查找,反而更难维护。

10.2 到处注入 ApplicationContext

下面这种写法会把 Service 变成容器脚本:

public class OrderService {
    private final ApplicationContext context;

    public void submit() {
        var repository = context.getBean(OrderRepository.class);
        repository.save();
    }
}

更好的方式是直接声明依赖:

public class OrderService {
    private final OrderRepository repository;

    public OrderService(OrderRepository repository) {
        this.repository = repository;
    }
}

只有在插件发现、动态类型解析、基础设施桥接等确实需要容器能力的边界位置,才考虑注入 ApplicationContext 或 ObjectProvider。

10.3 为了“方便”使用字段注入

字段注入的代码短,但代价是依赖隐藏、不可变性丢失、纯单元测试变麻烦。项目中保留字段注入往往是历史兼容或框架限制,新增代码优先使用构造器注入更稳妥。

10.4 一个接口只有一个实现,却滥用 @Qualifier

@Qualifier 是解决多个候选实现的工具,不是装饰品。只有一个实现时,构造器按类型注入已经足够;当实现确实存在多个时,优先通过有意义的 Bean 名称、@Primary 或配置条件表达选择规则。

10.5 把容器当成万能服务定位器

Service Locator 会隐藏依赖,使类的行为依赖运行时字符串和全局状态。IoC 的目标是让依赖关系更透明,而不是把所有对象塞进一个“能拿到任何东西”的入口。

十一、如何调试 Bean 装配问题

遇到 NoSuchBeanDefinitionException、NoUniqueBeanDefinitionException 或循环依赖异常时,可以按下面顺序排查:

  1. 确认目标类型是否被注册为 Bean。
  2. 确认组件扫描路径是否覆盖了实现类所在包。
  3. 确认实现类是否是具体类,或者是否有对应的 @Bean 方法。
  4. 如果候选对象有多个,检查 @Primary、@Qualifier 和 Bean 名称。
  5. 检查构造器依赖图,确认是否形成构造器循环依赖。
  6. 检查是否因为 @Profile、@Conditional 或配置属性导致 Bean 没有生效。
  7. 检查是否注入了代理对象,尤其是接口和实现类的类型边界。
  8. 打开 Spring 的相关 debug 日志,观察条件评估报告和 Bean 创建日志。

一个很实用的思路是:把异常还原成一张依赖图。节点是 Bean,边是依赖关系;缺 Bean 是节点不存在,多 Bean 是入边没有唯一目标,循环依赖则是图中出现了环。这样看,比盯着一大段异常堆栈有效得多。

十二、研究结论:IoC 的边界在哪里

IoC 不是把程序交给 Spring 后就万事大吉。它更像一种组织复杂度的方法:

  • 业务对象声明职责和依赖,不负责基础设施的创建细节。
  • 容器负责根据元数据组装对象,并统一协调生命周期。
  • 后处理器为事务、缓存、校验和代理等扩展能力提供入口。
  • 作用域和生命周期决定对象状态是否安全,不能只看注入语法。
  • 容器能力应该集中在组合根和基础设施边界,业务代码不应主动到处索取容器服务。

从源码角度看,Spring IoC 的核心链路可以压缩成一句话:

组件扫描和配置解析生成 BeanDefinition,BeanFactory 根据定义完成依赖解析与对象创建,BeanPostProcessor 在生命周期关键节点扩展行为,ApplicationContext 再把这些能力组织成完整的应用运行时。

理解这条链路之后,@Component、@Autowired、@Bean、@Transactional 就不再是互相孤立的注解,而是同一套容器机制在不同阶段的表现。Spring IoC 真正反转的,是对象创建与协作关系的控制权;真正值得掌握的,也正是这份控制权如何被容器接管、细化并在边界处交还给业务代码。

参考方向

  • Spring Framework Reference Documentation:Core Technologies / The IoC Container
  • org.springframework.beans.factory.BeanFactory
  • org.springframework.context.ApplicationContext
  • org.springframework.beans.factory.support.DefaultListableBeanFactory
  • org.springframework.context.support.AbstractApplicationContext#refresh
  • org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。