SpringNotes

第 04 章:依赖注入模式

zjc 于 2026-01-04 发布

这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 依赖注入看似只是几个注解,实际决定了代码的可测试性、可维护性和运行行为。本章比较常见注入方式,并给出工程选择建议。

4.1 构造器注入

推荐方式:

@Service
public class OrderService {
    private final OrderRepository repository;
    private final InventoryClient inventoryClient;
    private final ApplicationEventPublisher eventPublisher;

    public OrderService(OrderRepository repository,
                        InventoryClient inventoryClient,
                        ApplicationEventPublisher eventPublisher) {
        this.repository = repository;
        this.inventoryClient = inventoryClient;
        this.eventPublisher = eventPublisher;
    }
}

优点:

  1. 依赖不可变;
  2. 必需依赖一目了然;
  3. 单元测试可直接 new;
  4. 容易发现类职责过多;
  5. 避免容器 API 侵入业务。

当构造器参数过多时,通常不是注入方式的问题,而是类承担了太多职责。

4.2 字段注入

@Service
public class OrderService {
    @Autowired
    private OrderRepository repository;
}

问题:

  1. 依赖无法声明为 final;
  2. 不看注解难以发现依赖;
  3. 测试需要反射或容器;
  4. 隐藏循环依赖和过度耦合;
  5. Spring 官方也不推荐作为常规方式。

它适合快速示例,不适合作为团队规范。

4.3 Setter 注入

@Service
public class ReportService {
    private ReportRenderer renderer;

    @Autowired
    public void setRenderer(ReportRenderer renderer) {
        this.renderer = renderer;
    }
}

适合可选依赖或需要运行时重新配置的场景。缺点是对象可能处于不完整状态,调用方需要处理依赖为 null 的情况。

业务服务的必需依赖应优先走构造器。

4.4 多候选 Bean

场景:

OrderRepository 接口
  |-- MysqlOrderRepository
  |-- MongoOrderRepository

注入时报错:

expected single matching bean but found 2

解决方式一:@Primary

@Primary
@Repository
public class MysqlOrderRepository implements OrderRepository {
}

解决方式二:@Qualifier

public OrderService(
        @Qualifier("mongoOrderRepository") OrderRepository repository) {
    this.repository = repository;
}

解决方式三:按名称注入。

public OrderService(
        @Autowired(required = false) Map<String, Handler> handlers) {
    this.handlers = handlers;
}

策略模式常用 Map 或 List 注入:

public DiscountService(Map<String, DiscountStrategy> strategies) {
    this.strategies = strategies;
}

4.5 泛型注入

Spring 可以利用泛型元数据选择 Bean:

public interface Converter<S, T> {
    T convert(S source);
}

@Component
public class StringToOrderConverter implements Converter<String, Order> {
}

@Component
public class StringToUserConverter implements Converter<String, User> {
}

注入:

private final Converter<String, Order> orderConverter;

注意泛型擦除和代理对解析的影响,复杂场景应显式命名。

4.6 Optional 与 ObjectProvider

可选依赖:

public AuditService(Optional<AuditClient> client) {
    this.client = client.orElse(null);
}

延迟或条件获取:

public AuditService(ObjectProvider<AuditClient> clientProvider) {
    this.clientProvider = clientProvider;
}

public void audit(OrderEvent event) {
    AuditClient client = clientProvider.getIfAvailable();
    if (client != null) {
        client.send(event);
    }
}

区别:

方式 特点
Optional 注入时解析一次
ObjectProvider 延迟解析,适合条件依赖和多候选
@Autowired(required=false) 传统可选注入
@Nullable 显式允许为空

不要用 Optional 掩盖所有依赖问题。如果依赖是必需的,直接构造器注入更清晰。

4.7 循环依赖设计问题

@Service
public class OrderService {
    private final InventoryService inventoryService;
}

@Service
public class InventoryService {
    private final OrderService orderService;
}

更常见的设计味道:

  1. 两个服务彼此查询对方状态;
  2. 一个类同时负责流程编排和细节执行;
  3. 查询逻辑散落在多个服务;
  4. 缺少领域事件;
  5. 模块边界没有定义清楚。

重构方式:

@Service
public class OrderFacade {
    private final OrderService orderService;
    private final InventoryService inventoryService;
}

或者:

eventPublisher.publishEvent(new OrderCreatedEvent(orderId));

依赖方向应表达业务责任,而不是为了让代码编译通过。

4.8 配置注入

不推荐:

@Value("${pay.url}")
private String payUrl;

大量散落的 @Value 会导致配置不可见、难以校验。

推荐:

@ConfigurationProperties(prefix = "pay")
public record PayProperties(
        String url,
        Duration timeout,
        int retryTimes) {
}

启用:

@ConfigurationPropertiesScan
@SpringBootApplication
public class Application {
}

配置类可用 JSR-303 校验:

@ConfigurationProperties(prefix = "pay")
@Validated
public record PayProperties(
        @NotBlank String url,
        @NotNull Duration timeout,
        @Min(0) @Max(5) int retryTimes) {
}

4.9 测试中的注入

单元测试优先直接构造:

class OrderServiceTest {

    @Test
    void should_create_order() {
        OrderRepository repository = mock(OrderRepository.class);
        InventoryClient client = mock(InventoryClient.class);
        OrderService service = new OrderService(repository, client);
    }
}

切片测试:

@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    MockMvc mockMvc;

    @MockBean
    OrderService orderService;
}

Spring Boot 3.4 后测试相关注解有演进,使用当前版本的推荐注解即可。

4.10 团队规范建议

  1. 业务服务一律构造器注入;
  2. 必需依赖不使用 Optional;
  3. 多实现显式命名;
  4. 策略集合可用 Map/List;
  5. 配置使用 @ConfigurationProperties
  6. 禁止字段注入;
  7. 构造器超过 7 个依赖时强制重构评审;
  8. 循环依赖不允许通过属性绕过;
  9. 基础设施组件才使用 Aware;
  10. 测试优先直接构造依赖。

本章小结

构造器注入是最推荐的依赖注入方式,它能暴露必需依赖、保持对象不可变并提升可测试性。字段注入适合演示,不适合生产代码。多候选 Bean 通过 @Primary@Qualifier 或集合注入处理;配置类应集中绑定并校验。出现循环依赖时,优先重构职责和依赖方向。

思考题

  1. 为什么构造器注入更容易发现类的职责膨胀?
  2. OptionalObjectProvider 分别适合什么场景?
  3. 注入 Map<String, Handler> 时 key 是什么?
  4. 如何避免配置散落各处?
  5. 循环依赖的正面解法有哪些?