传统开发:失控的"new"地狱
// 传统开发方式(紧耦合)
public class OrderService {
// 硬编码依赖
private PaymentService payment = new AlipayService();
private Logger logger = new FileLogger("/tmp/log");
void pay() {
payment.process(); // 想换成微信支付?改代码重编译!
}
}
在传统模式开发时。我们想要使用一个对象时,都需要手动new一个对象,这个对象什么时候new,在哪里new,怎么new,都是开发者自己决定的,这就要求开发者对要使用的对象的内部细节非常的了解。要是这个对象是自己写的还好。要是别人写的,那我们在使用一个对像前还需要先去了解这个对象的整个依赖。
同时一个对象可能还依赖其他的对象,我们可能需要一个对象A,但对象A又依赖对象B、对象C 那么我们就又需要创建对象B和对象C,同时对象B和对象C可能又依赖其他的对象。为了拿到对象A,我们可能要额外的创建好多其他的对象。工作量逐渐开始失控。
痛点清单:
- 🔧 改需求要动源代码
- 🧪 没法做单元测试
- 🕸️ 依赖关系像蜘蛛网
于是聪明的你想到了把new对象的权利丢给其他人,我想要的时候直接找别人要就好了。
我们把你的这种想法起了一个好听的名字叫控制反转(IOC)
控制反转(IOC):把"new"的权力上交
定义:
控制反转是一种设计原则,将对象的创建、依赖管理权从程序员转移给框架/容器,实现解耦。
上面我们说到了我们想把new的权利外包出去,但外包给谁呢?这是一个Spring 的框架接下了这个活,他、它把所有的对象都放在了一个叫IOC容器的地方,我们需要使用对象的话直接告诉它需要使用哪个对象就好了,于是我们之前的代码变成了这样
// 交出控制权后的代码
public class OrderService {
@Autowired // 声明需要什么
private PaymentService payment;
@Autowired
private Logger logger; // 不再关心具体实现
}
权力转移示意图:
| 传统方式 | IoC方式 |
|---|---|
开发者手动new对象 |
容器自动创建和管理对象 |
| 直接调用依赖对象 | 依赖由容器注入 |
高耦合(如A a = new A()) |
低耦合(@Autowired private A a) |
本质:好莱坞原则——"别找我们,我们会找你"
Spring实现:
通过ApplicationContext管理所有Bean的生命周期。
依赖注入(Dependency Injection,)
定义
依赖注入(Dependency Injection,DI)是一种设计模式,是IOC的具体实现方式,由容器动态地将依赖关系注入到对象中。
在 Spring 接下了对象创建的控制权后,也遇到了对象间互相依赖的问题。于是Spring要求对象自己向Spring声明创建时需要依赖的对象,这些对象Spring也从IOC容器中获取,Spring将这种做法叫做依赖注入
核心思想:
- 谁负责创建依赖? → 容器(Spring IoC 容器)
- 谁决定依赖关系? → 配置(注解、XML、Java Config)
- 对象如何获取依赖? → 被动接收(通过构造函数、Setter 或字段注入)
依赖注入的三种方式
Spring 提供了 3 种主要的依赖注入方式:
1. 构造器注入(Constructor Injection)
推荐方式!(Spring 官方首选)
public class OrderService {
private final PaymentService paymentService;
// 构造器注入(Spring 4.3+ 可自动装配,无需 @Autowired)
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
✅ 优点:
- 强制依赖:对象创建时必须提供所有依赖,避免了
NullPointerException。 - 不可变依赖:
final关键字确保依赖不会被修改。 - 易于测试:可以直接传入 Mock 对象进行单元测试。
❌ 缺点:
- 依赖较多时,构造函数会很长(但可以使用 Lombok
@RequiredArgsConstructor简化)。
2. Setter 注入(Setter Injection)
适用于可选依赖或需要动态变更依赖的场景。
public class OrderService {
private PaymentService paymentService;
// Setter 注入(@Autowired 可省略)
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
✅ 优点:
- 允许动态替换依赖(如切换支付方式)。
- 适用于可选依赖(可以不调用 Setter)。
❌ 缺点:
- 依赖可能处于半初始化状态(对象创建完成但依赖未设置)。
- 不能保证依赖非空(除非手动检查)。
3. 字段注入(Field Injection)
最方便但不推荐(仅适用于快速原型开发)。
public class OrderService {
@Autowired // 直接注入字段
private PaymentService paymentService;
}
✅ 优点:
- 代码简洁,写起来快。
❌ 缺点:
- 破坏封装性:字段必须是
public或提供反射访问权限。 - 难以测试:必须用 Spring 容器或反射才能注入 Mock。
- 不能声明
final:依赖可能被修改。
依赖注入的进阶技巧
最佳实战
优先使用构造器注入
- 保证依赖不可变
- 避免NPE(依赖必须非空)
避免字段注入
- 不利于测试(需反射设置字段)
- 隐藏依赖关系
结合
@Qualifier解决歧义@Autowired @Qualifier("mysqlRepository") private UserRepository repository;可选依赖使用
@Autowired(required=false)处理多个同类型 Bean
如果多个 PaymentService 实现类存在,Spring 会报 NoUniqueBeanDefinitionException。
解决方案:
// 方式1:@Qualifier 指定 Bean 名称
@Autowired
@Qualifier("alipayService") // 指定注入的 Bean ID
private PaymentService paymentService;
// 方式2:@Primary 标记默认 Bean
@Bean
@Primary
public PaymentService wechatPayService() {
return new WeChatPayService();
}
可选依赖(允许 null)
// 方式1:@Autowired(required = false)
@Autowired(required = false)
private Optional<Logger> logger; // 如果没有 Logger Bean,logger = Optional.empty()
// 方式2:Java 8 Optional
@Autowired
public void setLogger(Optional<Logger> logger) {
logger.ifPresent(l -> this.logger = l);
}
3. 集合类型自动注入
Spring 会自动收集所有匹配类型的 Bean:
@Autowired
private List<PaymentService> paymentServices; // 注入所有 PaymentService 实现类
补充:依赖查找(Dependency Lookup, DL)
定义
依赖查找是主动从容器中获取依赖对象的方式,开发者需要显式调用 API(如
getBean())来查找依赖。
在通过Spring 在IOC容器中获取对象时,除了依赖注入的方式,还提供了一种;就是需要什么对象,你自己去找,把主动权又交还了一部分给开发者。还是需要开发者主动跟Spring容器中去找对象,但是不需要创建,创建对象的工作还是由Spring完成。
这也是控制反转的一种实现方式
1. 典型实现方式
// 使用 ApplicationContext 查找 Bean
ApplicationContext context = ...;
UserService userService = context.getBean("userService", UserService.class);
特点:
- 控制权在开发者手中:需要手动调用容器 API 获取依赖。
- 强依赖容器:代码中必须引入 Spring 相关类(如
ApplicationContext)。 - 灵活性高:可以动态决定获取哪个 Bean。
2. 适用场景
- 动态决策依赖(如根据配置选择不同的实现)。
- 在非 Spring 管理的类中获取 Bean(如工具类)。
3. 优缺点
✅ 优点:
- 灵活控制依赖获取时机。
- 适合动态场景(如运行时切换实现)。
❌ 缺点:
- 侵入性强:代码依赖 Spring API。
- 难以测试:必须启动 Spring 容器才能测试。
- 容易出错:手动查找可能触发
NoSuchBeanDefinitionException。
对比:依赖查找 vs依赖注入
| 特性 | 依赖查找(DL) | 依赖注入(DI) |
|---|---|---|
| 控制方式 | 主动查找(getBean()) |
被动接收(@Autowired) |
| 代码侵入性 | 高(依赖 Spring API) | 低(仅需注解) |
| 灵活性 | 高(可动态获取 Bean) | 较低(启动时确定) |
| 可测试性 | 差(需容器环境) | 好(可直接 Mock 注入) |
| 适用场景 | 动态依赖、工具类 | 大多数业务逻辑 |