先看一个被写死的订单服务
假设订单创建需要调用支付服务。最直接的写法,是在业务类里把依赖 new 出来:
class OrderService {
private final PaymentService payment = new PaymentService();
void createOrder(Order order) {
payment.charge(order.total());
}
}这段代码能运行,但 OrderService 同时承担了两件事:处理订单业务,以及决定支付实现如何创建。以后换成模拟支付、支付宝支付,或者测试时不真正扣款,都要修改这个类。
问题不在 new 这个关键字本身,而在依赖方向:业务对象主动寻找具体实现。
把创建权移到外部
先让业务类只表达自己需要什么:
interface PaymentGateway {
void charge(long cents);
}
class OrderService {
private final PaymentGateway payment;
OrderService(PaymentGateway payment) {
this.payment = payment;
}
void createOrder(Order order) {
payment.charge(order.total());
}
}现在 OrderService 不知道支付对象来自哪里,也不关心它是线上实现还是测试替身。依赖从构造器进入,业务代码只使用接口。
这就是依赖注入(Dependency Injection,DI):对象需要的依赖由外部传入。它实现了控制反转(Inversion of Control,IoC):对象不再控制依赖的创建和查找。
查看 Mermaid 源码
flowchart LR
order[OrderService] --> contract[PaymentGateway 接口]
live[StripePaymentGateway] --> contract
test[FakePaymentGateway] --> contract
container[组装代码] --> order
container --> live组装代码决定使用哪个实现,OrderService 只依赖接口。Spring 容器做的事情,就是把这段组装关系从业务代码中接管过来。
Spring 只是把组装关系声明出来
用 Spring 时,代码通常会写成:
@Service
class OrderService {
private final PaymentGateway payment;
OrderService(PaymentGateway payment) {
this.payment = payment;
}
}
@Configuration
class PaymentConfig {
@Bean
PaymentGateway paymentGateway() {
return new StripePaymentGateway();
}
}@Service 让组件进入候选集合,@Bean 明确告诉容器如何创建一个 PaymentGateway。容器读取这些声明,先创建支付实现,再把它传给 OrderService。
调用关系可以写成:
读取组件声明
↓
创建 StripePaymentGateway
↓
调用 OrderService 构造器
↓
得到已组装的 OrderService注解不是思想本身。即使不用 Spring,也可以手写一个 PaymentGateway 和 OrderService 的组装函数;Spring 只是把对象图、生命周期和配置管理做成了通用容器。
为什么优先使用构造器注入
构造器注入有三个直接结果:
- 对象创建完成时,必要依赖已经存在;
- 依赖关系可以从构造器签名中读出来;
- 测试可以直接传入 Fake 实现,不需要启动容器。
var fake = new FakePaymentGateway();
var service = new OrderService(fake);
service.createOrder(new Order(1000));如果一个类需要十几个依赖,问题通常不是“构造器太长”,而是这个类承担了太多职责。依赖注入会把结构暴露出来,迫使我们重新划分模块。
两个容易混淆的边界
IoC 不等于“所有对象都必须交给 Spring”。简单的值对象、纯函数和一次性数据结构,不需要注册成 Bean。
DI 也不等于“到处调用容器取对象”。下面这种 Service Locator 会重新隐藏依赖:
class OrderService {
void createOrder(Order order) {
var payment = context.getBean(PaymentGateway.class);
payment.charge(order.total());
}
}读代码的人无法从构造器知道它需要支付服务,测试也必须准备一个全局容器。依赖注入要求依赖显式地出现在对象边界上。
下一篇的问题是:依赖交给容器后,容器究竟何时创建对象、如何处理对象之间的依赖,以及对象何时可以被安全使用?