依赖注入(Dependency Injection,简称DI)是 Spring 最核心的机制之一。它负责将 Bean 所需要的其他 Bean 或简单值自动组装进来,而无需手动查找。掌握 DI 的常用方式与最佳实践,是写出干净、可测试 Spring 代码的关键。
8.3.1 三种注入方式
Spring 提供了三种主要的依赖注入方式,每种都有各自的适用场景和优缺点。
1. 构造器注入
这是目前最推荐的方式。依赖通过类的构造器传入,对象创建完毕时所有强制依赖全都就位,不可能出现半初始化状态。
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
// 构造器注入,Spring 会自动识别并注入参数
public OrderService(OrderRepository orderRepository,
PaymentService paymentService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
}
}
如果类只有一个构造器,@Autowired 可以省略(Spring 4.3 起默认处理)。构造器注入的好处十分明显:
- 不可变性:可以将字段声明为
final,杜绝后续意外修改。 - 明确的依赖清单:通过构造器参数一眼看出该类依赖哪些组件,方便理解和重构。
- 易于测试:单元测试时直接
new并传入 Mock 对象,无需依赖 Spring 容器。 - 防止 “依赖缺失”:编译期就能发现缺少依赖的问题,而不是运行时
NullPointerException。
对于可选依赖,可以使用 Optional 或 @Nullable 包装构造参数,但实践中更建议将可选依赖改为默认实现或通过 setter 注入。
2. Setter 注入
通过 setter 方法或任意方法完成注入。适用于 可选依赖 或 可动态重新配置 的依赖。
@Service
public class NotificationService {
private EmailSender emailSender;
// 默认构造器
public NotificationService() {}
@Autowired
public void setEmailSender(EmailSender emailSender) {
this.emailSender = emailSender;
}
}
与构造器注入相比,Setter 注入可以反复调用,允许在运行时更换具体实现。但缺点也很明显:对象可能处于依赖不全的状态,需要调用者小心维护。因此,建议仅将 Setter 注入用于非强制性的依赖。
3. 字段注入
直接在字段上标注 @Autowired,代码最简洁,却也是最不被推荐的方式。
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
}
字段注入的诱人之处在于没有额外的构造器或 setter 代码,但隐藏了严重的问题:
- 破坏封装性:依赖完全对外不可见,阅读类时不清楚需要什么才能正常工作。
- 强制依赖 Spring 容器:在纯 Java 测试中无法直接
new后手动设置字段,必须依赖反射或MockitoJUnitRunner等 Spring 容器测试。 - 无法使用
final:字段只能是可变的,存在被意外修改的风险。 - 当类依赖增多时:若一个类依赖超过 3-4 个直接依赖,字段注入容易让开发者忽视类职责过重的问题,构造器注入的臃肿参数列表则是一种代码臭味警示。
在实际项目中,优先使用构造器注入,字段注入可以考虑用于测试类或极简单的原型 Demo,但不应在业务代码中大面积使用。
8.3.2 @Autowired 的详细用法
@Autowired 是 Spring 提供的核心注入注解,可以用在构造器、setter、字段以及任何自定义方法上。其默认行为是 按类型(byType)匹配,即容器查找与声明类型兼容的 Bean 并注入。
1. 必须注入 vs. 可选注入
默认情况下,@Autowired 标注的依赖是 必需的。如果容器中没有找到对应类型的 Bean,启动时会直接抛出异常。当依赖不是绝对必要时,可以将 required 属性设为 false。
@Component
public class ReportService {
@Autowired(required = false)
private AdvancedFormatter formatter;
}
此时如果 AdvancedFormatter 不存在,Spring 不会报错,而 formatter 保持为 null。更好的做法是使用 Java 8 的 Optional:
@Autowired
public void setFormatter(Optional<AdvancedFormatter> formatter) {
formatter.ifPresent(f -> this.formatter = f);
}
2. 多个同类型 Bean 的处理
当容器中存在多个同类型的 Bean(例如多个 Validator 实现)时,单独使用 @Autowired 会报 NoUniqueBeanDefinitionException。有两种常用解决办法:
- 使用
@Qualifier指定 Bean 的名称
@Autowired
@Qualifier("strictValidator")
private Validator validator;
- 注入整个集合,然后运行时选择
@Autowired
private List<Validator> validators;
Spring 会自动将所有 Validator 类型的 Bean 注入到 List 中,顺序通常由 @Order 或 Ordered 接口控制。同样支持 Map<String, Validator>,其中 key 为 Bean 的名称。
- 使用
@Primary标记首选 Bean
在其中一个实现上标注 @Primary,让 Spring 在遇到冲突时默认选择它。配合 @Qualifier 还可以覆盖该默认选择。
@Component
@Primary
public class DefaultValidator implements Validator { ... }
8.3.3 结合 Java Config 的显式注入
在 @Configuration 类中使用 @Bean 声明 Bean 时,注入更加灵活:
- 方法参数自动注入:Spring 会自动将容器中匹配类型的 Bean 传入方法参数。
@Configuration
public class ServiceConfig {
@Bean
public OrderService orderService(OrderRepository repository,
PaymentService paymentService) {
return new OrderService(repository, paymentService);
}
}
- 结合
@Qualifier消除歧义:当形参无法区分时,仍可在参数上使用@Qualifier。
8.3.4 依赖注入的最佳实践
- 构造器注入作为默认选择
强制依赖使用构造器注入,将字段设为 final,确保对象就绪后不可变。
- 保持依赖的数量在合理范围
如果一个类的构造器参数超过 4-5 个,说明这个类可能承担了过多职责,应考虑拆分。依赖多少一目了然,可倒逼良好设计。
- 避免在
@Bean方法内部直接调用其他@Bean方法
Spring 通过 CGLIB 代理拦截 @Bean 方法调用以保证单例,如果直接从 @Configuration 类内部调用,应清楚其语义(默认被拦截返回单例)。更推荐通过参数注入的方式来明确依赖。
- 测试中善用构造器注入
单元测试或集成测试时,直接 new 出被测试对象并传入依赖的 Stub/Mock,完全不依赖 Spring 上下文,测试执行快、隔离性好。
- 谨慎使用
@Autowired的required = false
非必须的依赖可以考虑提供默认空实现或策略模式来消除 null 判断,将复杂性限制在配置阶段而非业务逻辑中。
- 简单值注入使用
@Value
对于字符串、数字等配置值,使用 @Value("${property.name}") 注入。配合 @ConfigurationProperties 可将一组属性优雅地绑定到 POJO 上。
8.3.5 循环依赖的处理与防范
依赖注入中最棘手的问题之一是循环依赖——Bean A 依赖 B,B 又依赖 A。Spring 对单例构造器注入的循环依赖无法解决(会抛出 BeanCurrentlyInCreationException),而 setter/字段注入可通过三级缓存机制解决部分情况,但强烈不推荐依赖这一特性。
发现循环依赖时应视为设计问题:抽离公共依赖为第三个 Bean 让双方共同依赖,或通过事件机制解耦。养成用构造器注入的习惯,循环依赖会在启动时就暴露,倒逼修正设计。这种“快速失败”远比运行时诡异的代理行为更可靠。
掌握了依赖注入的这些用法和原则,你在日常开发中就能写出清晰、健壮、易测的组件关系。下一节我们将展开讨论 Bean 的作用域与生命周期,进一步理解容器如何管理你的对象。