有了依赖注入,还差一个运行时
上一篇把 OrderService 的依赖交给外部组装,但真实应用不只有两个对象。订单服务可能依赖支付网关、库存仓库和事件发布器,支付网关又依赖 HTTP 客户端。
如果每个入口都手动写一遍组装代码,依赖关系仍然会散落在各处。Spring 引入容器,是为了集中管理这张对象图。
查看 Mermaid 源码
flowchart TD
context[ApplicationContext] --> order[OrderService]
context --> payment[PaymentGateway]
context --> stock[StockRepository]
order --> payment
order --> stock容器保存的不是已经创建好的对象清单,而是“如何创建对象”的定义,以及对象之间的依赖关系。
Bean 从哪里开始,到哪里结束
把一个 Bean 的生命周期压缩成一条可观察的轨迹:
读取 Bean 定义
↓
实例化对象
↓
注入依赖
↓
执行后置处理器
↓
调用初始化回调
↓
对外提供 Bean
↓
容器关闭时执行销毁回调这里的关键是“对外提供 Bean”发生在初始化之后。业务代码拿到的通常不是一个半成品对象。
可以用一个需要连接池的组件表示初始化和销毁:
@Component
class ReportExporter {
private ConnectionPool pool;
@Autowired
void setPool(ConnectionPool pool) {
this.pool = pool;
}
@PostConstruct
void open() {
pool.checkHealth();
}
@PreDestroy
void close() {
pool.shutdown();
}
}@PostConstruct 和 @PreDestroy 只是生命周期回调的声明方式。真正负责调用它们的是容器。
组装根应该在哪里
一个应用总要有一个地方决定“使用哪些实现”。在 Spring 中,这个位置通常是配置类和启动入口:
@Configuration
class OrderModule {
@Bean
PaymentGateway paymentGateway(HttpClient http) {
return new StripePaymentGateway(http);
}
@Bean
OrderService orderService(
PaymentGateway payment,
StockRepository stock
) {
return new OrderService(payment, stock);
}
}容器看到 orderService 需要两个参数,就会继续寻找对应的 Bean。这个过程可以抽象为:
- 读取
OrderService的构造器参数; - 按类型或限定符找到
PaymentGateway和StockRepository; - 先创建它们,再调用
OrderService构造器; - 缓存结果,按作用域决定后续是否复用。
这就是“对象图”的具体含义:每个节点是一个对象,每条边是一个依赖。
单例不是全局变量
Spring 默认的 singleton scope 表示:在一个容器中,同一个 Bean 定义通常对应一个共享实例。它仍然由容器管理,可以被替换、装饰和销毁。
因此,单例 Bean 最好保持无请求状态:
@Service
class OrderService {
// 可以保存不可变依赖
private final PaymentGateway payment;
// 不要把当前用户、当前订单写进字段
}把请求数据放进单例字段,会让并发请求互相覆盖。容器负责生命周期,不会自动替你修复线程安全问题。
循环依赖暴露了什么
如果 OrderService 依赖 AuditService,而 AuditService 又依赖 OrderService,容器无法按构造器顺序完成创建:
创建 OrderService → 需要 AuditService
创建 AuditService → 又需要 OrderService这通常说明两个组件的职责边界有问题。不要先想着换字段注入绕过启动错误,先把共同职责提取成第三个组件,或者让事件承担单向通知。
容器的报错在这里是一种结构反馈:它把手动组装时容易被忽略的依赖环显式暴露出来。
下一篇会继续追问:对象已经由容器创建好了,事务和日志又是怎样在不修改业务方法的情况下被加进去的?