人人都会AI编程

8.3 依赖注入用法

更新时间:2026-07-11

依赖注入(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 中,顺序通常由 @OrderOrdered 接口控制。同样支持 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 依赖注入的最佳实践

  1. 构造器注入作为默认选择

强制依赖使用构造器注入,将字段设为 final,确保对象就绪后不可变。

  1. 保持依赖的数量在合理范围

如果一个类的构造器参数超过 4-5 个,说明这个类可能承担了过多职责,应考虑拆分。依赖多少一目了然,可倒逼良好设计。

  1. 避免在 @Bean 方法内部直接调用其他 @Bean 方法

Spring 通过 CGLIB 代理拦截 @Bean 方法调用以保证单例,如果直接从 @Configuration 类内部调用,应清楚其语义(默认被拦截返回单例)。更推荐通过参数注入的方式来明确依赖。

  1. 测试中善用构造器注入

单元测试或集成测试时,直接 new 出被测试对象并传入依赖的 Stub/Mock,完全不依赖 Spring 上下文,测试执行快、隔离性好。

  1. 谨慎使用 @Autowiredrequired = false

非必须的依赖可以考虑提供默认空实现或策略模式来消除 null 判断,将复杂性限制在配置阶段而非业务逻辑中。

  1. 简单值注入使用 @Value

对于字符串、数字等配置值,使用 @Value("${property.name}") 注入。配合 @ConfigurationProperties 可将一组属性优雅地绑定到 POJO 上。

8.3.5 循环依赖的处理与防范

依赖注入中最棘手的问题之一是循环依赖——Bean A 依赖 B,B 又依赖 A。Spring 对单例构造器注入的循环依赖无法解决(会抛出 BeanCurrentlyInCreationException),而 setter/字段注入可通过三级缓存机制解决部分情况,但强烈不推荐依赖这一特性。

发现循环依赖时应视为设计问题:抽离公共依赖为第三个 Bean 让双方共同依赖,或通过事件机制解耦。养成用构造器注入的习惯,循环依赖会在启动时就暴露,倒逼修正设计。这种“快速失败”远比运行时诡异的代理行为更可靠。


掌握了依赖注入的这些用法和原则,你在日常开发中就能写出清晰、健壮、易测的组件关系。下一节我们将展开讨论 Bean 的作用域与生命周期,进一步理解容器如何管理你的对象。