目录
[① 导读卡片](#① 导读卡片)
[② 背景与目标](#② 背景与目标)
[③ 概念与原理](#③ 概念与原理)
[Setter 注入:可选依赖场景可用](#Setter 注入:可选依赖场景可用)
[底层原理:Spring 如何注入?](#底层原理:Spring 如何注入?)
[④ 逻辑与对比](#④ 逻辑与对比)
[核心对比:构造器注入 vs 字段注入](#核心对比:构造器注入 vs 字段注入)
[⑤ 核心详解](#⑤ 核心详解)
[详解 1:依赖不可变(final 字段)](#详解 1:依赖不可变(final 字段))
[详解 2:循环依赖暴露](#详解 2:循环依赖暴露)
[详解 3:空值安全](#详解 3:空值安全)
[详解 4:单元测试友好](#详解 4:单元测试友好)
[详解 5:与 Spring 解耦](#详解 5:与 Spring 解耦)
[详解 6:Lombok 简化构造器注入](#详解 6:Lombok 简化构造器注入)
[⑥ 案例实战](#⑥ 案例实战)
[实战 1:从字段注入重构到构造器注入](#实战 1:从字段注入重构到构造器注入)
[实战 2:条件注入(Spring Boot 4.x+ 新特性)](#实战 2:条件注入(Spring Boot 4.x+ 新特性))
[实战 3:多实现类注入](#实战 3:多实现类注入)
[⑦ 避坑 & 最佳实践](#⑦ 避坑 & 最佳实践)
[❌ 常见坑点](#❌ 常见坑点)
[✅ 最佳实践](#✅ 最佳实践)
[Spring 官方怎么说?](#Spring 官方怎么说?)
[⑧ 总结 & 路线图](#⑧ 总结 & 路线图)
① 导读卡片
| 项目 | 内容 |
|---|---|
| 一句话定位 | 一篇讲透 Spring 依赖注入的三种方式(字段注入、Setter 注入、构造器注入),看完你就知道为什么大厂都推荐构造器注入 |
| 适合人群 | 初中级 Java 开发者、Spring 初学者、准备面试的同学 |
| 难度 | ⭐⭐(基础) |
| 阅读时长 | 12 分钟 |
| 前置知识 | 知道什么是 IoC(控制反转)、DI(依赖注入) |
② 背景与目标
为什么需要依赖注入?
没有 Spring 的时候,代码长这样:
java
public class OrderService {
private OrderRepository orderRepository = new OrderRepository(); // 硬编码!
}
这种写法的问题:OrderService 和 OrderRepository 紧耦合,想换一个 OrderRepository 的实现(比如从 MySQL 切到 MongoDB),必须改源码。
依赖注入(Dependency Injection, DI) 解决了这个问题------由 Spring 容器负责创建并注入依赖,业务类只声明「我需要什么」,不关心「谁来创建」。
学完本文,你能够:
掌握三种注入方式的写法与适用场景
理解为什么构造器注入是官方推荐方案
用代码证明字段注入有哪些潜在风险
在 Code Review 中自信地指出不合理注入方式
③ 概念与原理
三种注入方式概览
| 注入方式 | 写法特点 | 是否支持 final | 是否需要 Spring 注解 | 依赖可见性 |
|---|---|---|---|---|
| 字段注入 | 属性上标 @Autowired |
❌ 不支持 | ✅ 需要 | 隐藏 |
| Setter 注入 | Setter 方法上标 @Autowired |
❌ 不支持 | ✅ 需要 | 中等 |
| 构造器注入 | 构造器参数自动注入 | ✅ 支持 | ❌ 不需要 | 一目了然 |
字段注入:最常见但不推荐
java
@Service
public class OrderService {
@Autowired // 字段上直接标注
private OrderRepository orderRepository;
}
特点 :代码最简洁,但问题最多。
Setter 注入:可选依赖场景可用
java
@Service
public class OrderService {
private OrderRepository orderRepository;
@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
特点:支持运行时更换实现(不常用)。
构造器注入:官方推荐
java
@Service
public class OrderService {
private final OrderRepository orderRepository; // final!
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
特点 :final 保证不可变,没有 Spring 注解也 OK。
底层原理:Spring 如何注入?
Spring 通过 AutowiredAnnotationBeanPostProcessor 这个 BeanPostProcessor 处理 @Autowired:
java
// Spring 内部伪代码
public class AutowiredAnnotationBeanPostProcessor implements BeanPostProcessor {
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
// 1. 解析类中的 @Autowired 字段/方法
InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass());
// 2. 遍历所有需要注入的点
for (InjectedElement element : metadata) {
// 3. 从容器中查找匹配的 Bean
Object dependency = beanFactory.resolveDependency(element.getDependencyDescriptor());
// 4. 通过反射设置字段值(字段注入本质是反射)
element.inject(bean, dependency, null);
}
return pvs;
}
}
字段注入的本质是反射注入 ,而构造器注入是正常的 Java 构造器调用。
④ 逻辑与对比
核心对比:构造器注入 vs 字段注入
| 对比维度 | 构造器注入 | 字段注入 | 实际影响 |
|---|---|---|---|
| 依赖不可变 | ✅ 支持 final 字段 |
❌ 不支持 | 防止依赖被意外修改,更安全 |
| 循环依赖暴露 | ✅ 启动时直接报错 | ❌ 可能被三级缓存隐藏 | 提前发现问题 |
| 空值安全 | ✅ 构造时检查非空 | ❌ 运行时才报 NPE | 尽早发现配置错误 |
| 单元测试 | ✅ 直接 new 传 mock |
❌ 需要 @InjectMocks 等框架魔法 |
测试更简洁 |
| 依赖可见性 | ✅ 参数列表清晰 | ❌ 需要满类找 @Autowired |
代码审查更高效 |
| 与 Spring 解耦 | ✅ 无需 Spring 注解 | ❌ 依赖 @Autowired |
非 Spring 环境可复用 |
什么场景选什么?
你的场景 推荐方式
────────────────────────────────────────────
新项目、标准业务代码 → 构造器注入(推荐)
必须用字段注入的老项目 → 维持现状,逐步重构
可选依赖(非必需) → Setter 注入
单元测试中的 Mock 对象 → 构造器注入直接传
Spring Boot + Lombok → 构造器注入 + @RequiredArgsConstructor
⑤ 核心详解
详解 1:依赖不可变(final 字段)
java
// ❌ 字段注入 ------ 依赖可变,可被意外修改
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
public void dangerousMethod() {
this.orderRepository = null; // 编译通过,运行时 NPE!
}
}
// ✅ 构造器注入 ------ 依赖不可变
@Service
public class OrderService {
private final OrderRepository orderRepository; // final!
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void safeMethod() {
this.orderRepository = null; // ❌ 编译报错!final 字段不能重新赋值
}
}
详解 2:循环依赖暴露
java
// 两个类互相依赖
@Service
public class AService {
private final BService bService;
public AService(BService bService) {
this.bService = bService;
}
}
@Service
public class BService {
private final AService aService;
public BService(AService aService) {
this.aService = aService;
}
}
构造器注入版本:启动时直接报错:
Error creating bean with name 'aService': Requested bean is currently in creation: Is there an unresolvable circular reference?
字段注入版本:Spring 通过三级缓存勉强解决,启动成功,但:
-
隐藏了设计问题
-
未来代码复杂化后可能在某个请求时炸出奇怪的问题
-
线上事故比启动报错更难排查
核心结论:循环依赖是设计问题,不是技术问题。构造器注入强制你在开发阶段就重构代码(比如拆出一个 C 类),而不是留到生产环境出事。
详解 3:空值安全
java
// ✅ 构造器注入 + Lombok @NonNull
import lombok.NonNull;
@Service
public class UserService {
private final UserRepository repo;
public UserService(@NonNull UserRepository repo) {
this.repo = repo; // repo 为 null 时立即 NPE
}
}
// ❌ 字段注入 ------ 缺失依赖时延迟报错
@Service
public class UserService {
@Autowired
private UserRepository repo; // 如果这个 Bean 不存在,启动时不报错
public void findUser() {
repo.findById(1L); // ❌ 第一次调用时才 NPE!
}
}
实际后果:
-
字段注入:
UserService被成功创建,第一个用户请求进来 → 500 错误 → 线上事故 -
构造器注入:应用启动直接失败,CI/CD 流水线卡住,开发立刻知晓
详解 4:单元测试友好
java
// ❌ 字段注入 ------ 测试需要框架辅助
@Service
public class OrderService {
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
}
// 字段注入的测试
@ExtendWith(MockitoExtension.class) // 必须加这个
class OrderServiceTest {
@InjectMocks // 通过反射注入
private OrderService orderService;
@Mock
private PaymentService paymentService;
@Mock
private InventoryService inventoryService;
// @InjectMocks 有时会神秘失败,需要调试半天
}
// ✅ 构造器注入 ------ 测试就是普通 Java 对象
@Service
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
public OrderService(PaymentService paymentService, InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
}
// 构造器注入的测试 ------ 无任何注解
class OrderServiceTest {
private OrderService orderService;
@BeforeEach
void setUp() {
PaymentService paymentService = mock(PaymentService.class);
InventoryService inventoryService = mock(InventoryService.class);
orderService = new OrderService(paymentService, inventoryService); // 直接 new!
}
}
详解 5:与 Spring 解耦
java
// ✅ 构造器注入 ------ 不依赖任何 Spring 注解
public class PaymentProcessor {
private final PaymentGateway gateway;
public PaymentProcessor(PaymentGateway gateway) {
this.gateway = gateway;
}
}
// 在 Spring 中注册
@Configuration
public class AppConfig {
@Bean
public PaymentGateway gateway() {
return new StripeGateway();
}
@Bean
public PaymentProcessor processor() {
return new PaymentProcessor(gateway());
}
}
// 在非 Spring 环境(Lambda、批处理、单元测试)中同样可用
PaymentGateway mockGateway = mock(PaymentGateway.class);
PaymentProcessor processor = new PaymentProcessor(mockGateway);
详解 6:Lombok 简化构造器注入
java
// Lombok 最优雅的写法 ------ @RequiredArgsConstructor
@Service
@RequiredArgsConstructor // ✅ 为所有 final 字段生成构造器
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final InventoryService inventoryService;
// 不用写任何构造器代码!
}
等同于:
java
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentService paymentService;
private final InventoryService inventoryService;
public OrderService(OrderRepository orderRepository,
PaymentService paymentService,
InventoryService inventoryService) {
this.orderRepository = orderRepository;
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
}
⑥ 案例实战
实战 1:从字段注入重构到构造器注入
重构前(字段注入):
java
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
@Autowired
private AuditLogService auditLogService;
@Value("${app.max-login-attempts}")
private int maxLoginAttempts;
public User login(String username, String password) {
User user = userRepository.findByUsername(username);
if (user == null) {
auditLogService.log("LOGIN_FAILED", username);
throw new LoginException("用户不存在");
}
if (!passwordEncoder.matches(password, user.getPassword())) {
auditLogService.log("WRONG_PASSWORD", username);
throw new LoginException("密码错误");
}
return user;
}
}
重构后(构造器注入 + @Value 用构造器参数):
java
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
private final AuditLogService auditLogService;
private final int maxLoginAttempts;
public UserService(UserRepository userRepository,
EmailService emailService,
AuditLogService auditLogService,
@Value("${app.max-login-attempts}") int maxLoginAttempts) {
this.userRepository = userRepository;
this.emailService = emailService;
this.auditLogService = auditLogService;
this.maxLoginAttempts = maxLoginAttempts;
}
public User login(String username, String password) {
// 业务逻辑不变...
}
}
实战 2:条件注入(Spring Boot 4.x+ 新特性)
java
// 使用 @ConditionalOnMissingBean 实现条件注入
@Configuration
public class PaymentConfig {
@Bean
@ConditionalOnMissingBean
public PaymentGateway paymentGateway() {
// 如果没有自定义的 PaymentGateway,使用默认实现
return new StripePaymentGateway();
}
@Bean
public PaymentProcessor paymentProcessor(PaymentGateway gateway) {
return new PaymentProcessor(gateway);
}
}
实战 3:多实现类注入
java
// 定义接口
public interface PaymentService {
void pay(BigDecimal amount);
}
// 多个实现
@Component
@Primary // 默认使用
public class AlipayService implements PaymentService {
public void pay(BigDecimal amount) {
System.out.println("支付宝支付: " + amount);
}
}
@Component
public class WechatPayService implements PaymentService {
public void pay(BigDecimal amount) {
System.out.println("微信支付: " + amount);
}
}
// 注入方式 1:用 @Qualifier 指定
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(@Qualifier("wechatPayService") PaymentService paymentService) {
this.paymentService = paymentService;
}
}
// 注入方式 2:注入所有实现
@Service
public class PaymentRouter {
private final List<PaymentService> paymentServices; // 自动注入所有实现
public PaymentRouter(List<PaymentService> paymentServices) {
this.paymentServices = paymentServices;
}
}
⑦ 避坑 & 最佳实践
❌ 常见坑点
坑 1:字段注入 + final 关键字
java
// ❌ 编译不报错但毫无意义
@Autowired
private final SomeService someService; // final 字段必须在构造器中赋值!
✅ 解决:要么去掉 final 用字段注入,要么用构造器注入。
坑 2:循环依赖 + 构造器注入导致启动失败 看到 BeanCurrentlyInCreationException 不要慌,这是 Spring 在保护你。去重构代码结构。
✅ 消除循环依赖的方法:
-
使用接口分离(最常见的解法)
-
使用 @Lazy 延迟加载(临时方案)
-
提取公共逻辑到新类
坑 3:@Autowired(required=false) 的误用
java
@Autowired(required = false)
private SomeService someService; // 如果 Bean 不存在就不注入
// ❌ 使用时忘记判空
public void useService() {
someService.doSomething(); // NullPointerException!
}
坑 4:静态字段注入
java
@Component
public class Utils {
@Autowired
private static SomeService someService; // ❌ 静态字段不会被注入!
public static void doSomething() {
someService.call(); // NullPointerException
}
}
✅ 正确做法 :用构造器注入 + @PostConstruct 赋值给静态字段。
✅ 最佳实践
| 规则 | 说明 |
|---|---|
| 优先构造器注入 | Spring 官方推荐,Spring Boot 团队也在用 |
| Lombok + @RequiredArgsConstructor | 一行注解替代 N 行构造器代码 |
| 字段注入只用于测试 | 或者遗留代码不改动 |
| 多个同类型 Bean 用 @Qualifier | 配合 @Primary 设置默认实现 |
| 构造器参数过多 = 类职责过重 | 考虑拆分 Service 类 |
| 单元测试优先选构造器注入 | 减少测试框架依赖,提升可读性 |
Spring 官方怎么说?
Spring 团队: 自 Spring 4.x 起,构造器注入就是官方推荐的注入方式。单构造器类甚至不需要
@Autowired注解,Spring 会自动使用它。
⑧ 总结 & 路线图
一句话总结
字段注入图省事,构造器注入图省心。
三句话记住一天
| 注入方式 | 一句话总结 |
|---|---|
| 字段注入 | 简洁但有隐患,小项目可接受,大项目要重构 |
| Setter 注入 | 可选依赖时用,日常开发不常见 |
| 构造器注入 | 官方首选,final 安全、测试友好、解耦干净 |
下一步去哪?
| 学习方向 | 推荐内容 |
|---|---|
| @Autowired 底层原理 | 了解 AutowiredAnnotationBeanPostProcessor 源码 |
| Java Config 配置注入 | @Configuration + @Bean 的注入方式 |
| Qualifier & Primary | 多实现注入的精细控制 |
| Spring 4.x + 自动推断 | 单构造器如何省掉 @Autowired |
| 依赖注入设计模式 | 学习策略模式 + 工厂模式在注入中的应用 |
互动题:你项目中用的哪种注入方式?遇到过哪些因注入方式选错导致的 Bug?评论区聊聊 🚀