这是《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;
}
}
优点:
- 依赖不可变;
- 必需依赖一目了然;
- 单元测试可直接 new;
- 容易发现类职责过多;
- 避免容器 API 侵入业务。
当构造器参数过多时,通常不是注入方式的问题,而是类承担了太多职责。
4.2 字段注入
@Service
public class OrderService {
@Autowired
private OrderRepository repository;
}
问题:
- 依赖无法声明为 final;
- 不看注解难以发现依赖;
- 测试需要反射或容器;
- 隐藏循环依赖和过度耦合;
- 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;
}
更常见的设计味道:
- 两个服务彼此查询对方状态;
- 一个类同时负责流程编排和细节执行;
- 查询逻辑散落在多个服务;
- 缺少领域事件;
- 模块边界没有定义清楚。
重构方式:
@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 团队规范建议
- 业务服务一律构造器注入;
- 必需依赖不使用 Optional;
- 多实现显式命名;
- 策略集合可用 Map/List;
- 配置使用
@ConfigurationProperties; - 禁止字段注入;
- 构造器超过 7 个依赖时强制重构评审;
- 循环依赖不允许通过属性绕过;
- 基础设施组件才使用 Aware;
- 测试优先直接构造依赖。
本章小结
构造器注入是最推荐的依赖注入方式,它能暴露必需依赖、保持对象不可变并提升可测试性。字段注入适合演示,不适合生产代码。多候选 Bean 通过 @Primary、@Qualifier 或集合注入处理;配置类应集中绑定并校验。出现循环依赖时,优先重构职责和依赖方向。
思考题
- 为什么构造器注入更容易发现类的职责膨胀?
Optional和ObjectProvider分别适合什么场景?- 注入 Map<String, Handler> 时 key 是什么?
- 如何避免配置散落各处?
- 循环依赖的正面解法有哪些?