透彻理解依赖注入(DI)的设计模式及其在 Spring Boot 中的核心地位
掌握三种主要的注入方式(构造器、Setter、字段),并明确为何构造器注入是官方推荐标准
学会处理依赖冲突,熟练使用 @Qualifier、@Primary 和 @Resource 解决多实现类问题
理解循环依赖的成因及在纯构造器注入场景下的解决方案
能够编写符合“不可变性”原则的高质量 Spring 组件代码
依赖注入(Dependency Injection, DI)是控制反转(IoC)的具体实现形式。它的核心思想非常朴素:对象不应该自己创建它所依赖的对象,而应该由外部容器将这些依赖“送”给它。
在早期的 Java EE 开发中,我们习惯于在类内部直接使用 new 关键字实例化依赖项:
// ❌ 传统方式:高耦合,难以测试
public class OrderService {
private PaymentProcessor processor = new PayPalProcessor(); // 硬编码依赖
public void processOrder() {
processor.pay();
}
}这种写法带来了严重的问题:
耦合度高:OrderService 与 PayPalProcessor 强绑定,若想切换到 StripeProcessor,必须修改源码。
难以测试:单元测试时无法轻松替换为模拟对象(Mock),导致测试依赖于真实的支付网关。
生命周期难管理:对象的创建、销毁逻辑分散在各个类中。
Spring Boot 通过 DI 容器解决了这些问题。容器负责创建所有对象(Bean),分析它们之间的依赖关系,并在运行时将正确的实例注入到目标对象中。
Spring 支持三种主要的依赖注入方式。理解它们的区别是掌握 Spring DI 的关键。
通过类的构造函数将依赖项传入。这是 Spring 官方文档唯一推荐的方式。
优点:
保证不可变性:依赖字段可以声明为 final,防止被意外修改。
强制依赖显性化:如果缺少必要依赖,应用启动时直接失败(Fail Fast),避免运行时空指针异常。
易于单元测试:测试时无需启动 Spring 容器,直接 new 对象并传入 Mock 即可。
避免循环依赖隐患:构造器注入能更清晰地暴露循环依赖问题。
代码示例:
@Service
public class OrderService {
private final PaymentProcessor processor;
// Spring 4.3+:单构造器可省略 @Autowired
public OrderService(PaymentProcessor processor) {
this.processor = processor;
}
}通过标准的 Java Bean setter 方法注入依赖。适用于可选依赖。
优点:
灵活,可以在对象创建后重新配置依赖。
适合处理那些有默认值、非必须的依赖项。
缺点:
对象可能处于“部分初始化”状态(依赖未被注入)。
无法使用 final 修饰字段。
代码示例:
@Service
public class ReportService {
private DataSource dataSource;
@Autowired // 可选依赖通常配合 @Required 或逻辑判断使用
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
}直接使用 @Autowired 注解在私有字段上。
为什么不推荐?
违反封装原则:依赖关系对外部调用者隐藏,只能通过反射注入。
测试困难:无法通过构造函数或 Setter 传入 Mock 对象,必须依赖 Spring 测试上下文。
易导致神类(God Class):由于注入太方便,开发者倾向于在一个类中注入过多依赖,违背单一职责原则。
无法声明为 final:破坏了不可变性。
代码示例(仅作了解,请勿在生产环境使用):
@Service
public class LegacyService {
@Autowired // 不推荐
private UserRepository userRepository;
}在实际开发中,一个接口往往有多个实现类。例如,NotificationService 可能有 EmailNotification 和 SmsNotification 两个实现。当 Spring 容器尝试注入 NotificationService 时,它会发现有两个候选者,从而抛出 NoUniqueBeanDefinitionException。
以下是解决此类冲突的三种标准方案:
在注入点明确告诉 Spring 需要哪个具体的 Bean。Bean 的名称默认是类名首字母小写,也可以通过 @Component("customName") 自定义。
@Service
public class UserService {
private final NotificationService notificationService;
// 明确指定使用 "emailNotification" 这个 Bean
public UserService(@Qualifier("emailNotification") NotificationService notificationService) {
this.notificationService = notificationService;
}
}如果在大多数情况下都优先使用某一个实现,可以在该实现类上添加 @Primary 注解。这样,在不指定 @Qualifier 的情况下,Spring 会默认注入这个被标记为首选的 Bean。
@Component
@Primary // 标记为首选实现
public class EmailNotification implements NotificationService {
// ... 实现细节
}
@Component
public class SmsNotification implements NotificationService {
// ... 实现细节
}@Resource 是 Java 标准注解(不属于 Spring 特有),它默认按名称(name)进行匹配,其次才按类型。这通常比 @Autowired 更直观。
@Service
public class OrderService {
private final NotificationService notificationService;
// 默认按名称 "smsNotification" 查找,找不到再按类型
public OrderService(@Resource(name = "smsNotification") NotificationService notificationService) {
this.notificationService = notificationService;
}
}循环依赖是指两个或多个 Bean 互相依赖对方才能完成初始化。 例如:A 依赖 B,B 又依赖 A。
@Service
public class ServiceA {
private final ServiceB serviceB;
public ServiceA(ServiceB serviceB) { this.serviceB = serviceB; }
}
@Service
public class ServiceB {
private final ServiceA serviceA;
public ServiceB(ServiceA serviceA) { this.serviceA = serviceA; }
}如果使用构造器注入,上述代码会导致 BeanCurrentlyInCreationException,因为 A 在创建时需要 B,而 B 在创建时又需要 A,形成死锁。
最佳实践
循环依赖通常是设计缺陷的信号。它意味着两个类的职责耦合度过高。最好的解决方法是提取公共逻辑到第三个类(Service C),让 A 和 B 都依赖 C,从而打破循环。
妥协方案
如果无法重构,可以将其中一方的依赖改为 Setter 注入。Spring 容器通过“三级缓存”机制可以解决单例模式下 Setter 注入的循环依赖问题。
@Service
public class ServiceA {
private ServiceB serviceB;
// 改为 Setter 注入
@Autowired
public void setServiceB(ServiceB serviceB) {
this.serviceB = serviceB;
}
}延迟加载
在构造器参数上使用 @Lazy,告诉 Spring 先注入一个代理对象(Proxy),等到真正调用方法时再去获取真实的 Bean。
@Service
public class ServiceA {
private final ServiceB serviceB;
// 使用 @Lazy 打破初始化死锁
public ServiceA(@Lazy ServiceB serviceB) {
this.serviceB = serviceB;
}
}以下图表展示了 Spring Boot 容器如何处理依赖注入,特别是构造器注入的流程:
