源本科技 | 码上会

Spring Boot 依赖注入

2026/03/24
31
0

学习目标

  • 透彻理解依赖注入(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();
    }
}

这种写法带来了严重的问题:

  1. 耦合度高OrderServicePayPalProcessor 强绑定,若想切换到 StripeProcessor,必须修改源码。

  2. 难以测试:单元测试时无法轻松替换为模拟对象(Mock),导致测试依赖于真实的支付网关。

  3. 生命周期难管理:对象的创建、销毁逻辑分散在各个类中。

Spring Boot 通过 DI 容器解决了这些问题。容器负责创建所有对象(Bean),分析它们之间的依赖关系,并在运行时将正确的实例注入到目标对象中。


三种注入方式

Spring 支持三种主要的依赖注入方式。理解它们的区别是掌握 Spring DI 的关键。

1. 构造器注入(推荐)

通过类的构造函数将依赖项传入。这是 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;
    }
}

2. Setter 注入

通过标准的 Java Bean setter 方法注入依赖。适用于可选依赖

优点:

  • 灵活,可以在对象创建后重新配置依赖。

  • 适合处理那些有默认值、非必须的依赖项。

缺点:

  • 对象可能处于“部分初始化”状态(依赖未被注入)。

  • 无法使用 final 修饰字段。

代码示例:

@Service
public class ReportService {
    private DataSource dataSource;

    @Autowired // 可选依赖通常配合 @Required 或逻辑判断使用
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}

3. 字段注入(不推荐)

直接使用 @Autowired 注解在私有字段上。

为什么不推荐?

  • 违反封装原则:依赖关系对外部调用者隐藏,只能通过反射注入。

  • 测试困难:无法通过构造函数或 Setter 传入 Mock 对象,必须依赖 Spring 测试上下文。

  • 易导致神类(God Class):由于注入太方便,开发者倾向于在一个类中注入过多依赖,违背单一职责原则。

  • 无法声明为 final:破坏了不可变性。

代码示例(仅作了解,请勿在生产环境使用):

@Service
public class LegacyService {
    @Autowired // 不推荐
    private UserRepository userRepository;
}

方式对比

特性

构造器注入

Setter 注入

字段注入

推荐程度

⭐⭐⭐⭐⭐ (强烈推荐)

⭐⭐ (仅限可选依赖)

❌ (避免使用)

不可变性 (final)

支持

不支持

不支持

强制依赖检查

启动时检查

运行时可能为空

运行时可能为空

单元测试友好度

极高 (无需容器)

低 (需容器 / 反射)

循环依赖处理

直接报错 (暴露问题)

支持 (三级缓存解决)

支持 (三级缓存解决)

代码清晰度

依赖一目了然

较清晰

依赖隐藏


依赖冲突与多实现

在实际开发中,一个接口往往有多个实现类。例如,NotificationService 可能有 EmailNotificationSmsNotification 两个实现。当 Spring 容器尝试注入 NotificationService 时,它会发现有两个候选者,从而抛出 NoUniqueBeanDefinitionException

以下是解决此类冲突的三种标准方案:

方案一:使用 @Qualifier

在注入点明确告诉 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

如果在大多数情况下都优先使用某一个实现,可以在该实现类上添加 @Primary 注解。这样,在不指定 @Qualifier 的情况下,Spring 会默认注入这个被标记为首选的 Bean。

@Component
@Primary // 标记为首选实现
public class EmailNotification implements NotificationService {
    // ... 实现细节
}

@Component
public class SmsNotification implements NotificationService {
    // ... 实现细节
}

方案三:使用 @Resource

@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,形成死锁。

解决方案

1. 重构代码

最佳实践

循环依赖通常是设计缺陷的信号。它意味着两个类的职责耦合度过高。最好的解决方法是提取公共逻辑到第三个类(Service C),让 A 和 B 都依赖 C,从而打破循环。

2. 使用 Setter 注入

妥协方案

如果无法重构,可以将其中一方的依赖改为 Setter 注入。Spring 容器通过“三级缓存”机制可以解决单例模式下 Setter 注入的循环依赖问题。

@Service
public class ServiceA {
    private ServiceB serviceB;
    
    // 改为 Setter 注入
    @Autowired
    public void setServiceB(ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

3. 使用 @Lazy

延迟加载

在构造器参数上使用 @Lazy,告诉 Spring 先注入一个代理对象(Proxy),等到真正调用方法时再去获取真实的 Bean。

@Service
public class ServiceA {
    private final ServiceB serviceB;

    // 使用 @Lazy 打破初始化死锁
    public ServiceA(@Lazy ServiceB serviceB) {
        this.serviceB = serviceB;
    }
}

架构流程图

以下图表展示了 Spring Boot 容器如何处理依赖注入,特别是构造器注入的流程: