Spring DI 详解:Autowired 属性注入、构造方法注入与 Setter 注入实践
1. DI 与 IoC 的关系:先把概念说清楚
在 Spring 体系里,DI(Dependency Injection,依赖注入)不是孤立概念,它是 IoC(Inversion of Control,控制反转)最常见、最核心的落地方式。
很多文章会把两者混用,但严格来说,它们并不完全等价:
-
IoC 是一种设计思想
- 对象不再主动创建或查找自己依赖的对象,而是把对象的创建、装配、生命周期管理交给容器。
-
DI 是 IoC 的实现方式
- 容器把对象依赖的其他对象“注入”进来,而不是让对象自己通过
new、静态工厂或服务定位器去获取。
- 容器把对象依赖的其他对象“注入”进来,而不是让对象自己通过
如果没有 Spring,业务代码通常会写成这样:
public class OrderService {
private OrderRepository orderRepository = new OrderRepository();
public void createOrder() {
orderRepository.save();
}
}
这段代码的问题非常明显:
OrderService与OrderRepository强耦合- 依赖对象的创建方式被写死
- 单元测试不方便替换 Mock 实现
- 后续切换实现类、增加代理、接入事务、AOP 都会变得麻烦
而使用 Spring DI 后:
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void createOrder() {
orderRepository.save();
}
}
此时:
OrderService只依赖抽象关系或容器提供的对象- 依赖的创建与装配交由 Spring 容器完成
- 更容易测试、扩展和维护
- Spring 可以在 Bean 创建阶段完成代理增强、生命周期回调、依赖检查
因此,理解 Spring 的 DI,不能只停留在“会写 @Autowired”的层面,而应该理解它背后的容器装配机制、注入方式差异、适用场景和工程实践。
2. Spring 容器是如何完成依赖注入的
Spring DI 的前提是:被管理对象必须先成为 Spring Bean。 也就是说,Spring 先通过配置类、XML、组件扫描、@Bean 方法等方式把对象注册进容器,然后在 Bean 创建和初始化过程中处理依赖关系。
一个 Bean 进入 Spring 容器,大致经历以下过程:
-
BeanDefinition 注册
- Spring 扫描
@Component、@Service、@Repository、@Controller - 或解析
@Configuration+@Bean - 或解析 XML 配置
- Spring 扫描
-
实例化 Bean
- 通过构造器、工厂方法等创建对象
-
属性填充
- 对字段、Setter、其他可注入点进行依赖注入
-
Aware 接口回调
- 如
BeanNameAware、BeanFactoryAware
- 如
-
BeanPostProcessor 前置处理
-
初始化方法执行
@PostConstructInitializingBean.afterPropertiesSet()- 自定义 init-method
-
BeanPostProcessor 后置处理
- 例如 AOP 代理通常在这个阶段产生
-
Bean 可用
-
容器关闭时销毁
@PreDestroy等
在这套流程里,依赖注入主要发生在两个地方:
- 构造方法注入:实例化阶段完成
- 字段注入、Setter 注入:属性填充阶段完成
这也是为什么构造器注入和属性注入在使用体验、约束能力、空值控制、测试友好性上会有明显差别。
3. Spring 中常见的三种注入方式
Spring 中最常见的依赖注入方式有三类:
-
属性注入(字段注入,Field Injection)
- 典型写法是
@Autowired标注在成员变量上
- 典型写法是
-
构造方法注入(Constructor Injection)
- 依赖通过构造器参数传入
-
Setter 注入(Setter Injection)
- 依赖通过
setXxx()方法传入
- 依赖通过
三者都能工作,但它们的设计约束、可维护性和工程推荐级别并不一样。
先看一个统一的业务场景:
public interface UserRepository {
void save(String username);
}
@Repository
public class JdbcUserRepository implements UserRepository {
@Override
public void save(String username) {
System.out.println("save user: " + username);
}
}
下面分别用三种方式实现 UserService。
4. 属性注入 @Autowired:最常见,但并非最佳实践
4.1 基本写法
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public void register(String username) {
userRepository.save(username);
}
}
这就是最常见的 Spring 注入写法。 Spring 在创建 UserService 实例后,会通过反射把容器中的 UserRepository Bean 注入到该字段中。
这种方式的优点很直观:
- 写法最短
- 上手成本低
- 代码视觉上简洁
- 在很多老项目中大量存在
但“简单”并不代表“最优”,它在工程实践里存在很多隐患。
4.2 属性注入的底层机制
当 Spring 创建完 UserService 实例之后,会进入属性填充阶段。 AutowiredAnnotationBeanPostProcessor 会扫描 Bean 中标注了 @Autowired、@Value、@Inject 等注解的成员,然后解析依赖并通过反射进行赋值。
也就是说,字段注入不是通过构造器,不是通过显示方法调用,而是通过容器后处理器在运行时完成。
因此它的特点是:
- 注入行为是隐式的
- 依赖关系不体现在构造器签名中
- 对象可以在“依赖未注入完成”前被创建出来
这恰恰是很多问题的根源。
4.3 @Autowired 的匹配规则
Spring 默认按类型注入。
例如:
@Autowired
private UserRepository userRepository;
Spring 会去容器里找 UserRepository 类型的 Bean。
场景一:只有一个候选 Bean
直接注入成功。
场景二:有多个候选 Bean
会抛出异常,常见异常是:
NoUniqueBeanDefinitionException
例如:
@Repository
public class JdbcUserRepository implements UserRepository {
}
@Repository
public class RedisUserRepository implements UserRepository {
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
}
此时 Spring 不知道该注入哪个实现。
解决方式有三种。
方式一:配合 @Qualifier
@Service
public class UserService {
@Autowired
@Qualifier("jdbcUserRepository")
private UserRepository userRepository;
}
方式二:使用 @Primary
@Repository
@Primary
public class JdbcUserRepository implements UserRepository {
}
这样当按类型注入且存在多个实现时,优先注入 @Primary 标记的 Bean。
方式三:按名称匹配作为补充
在某些场景下,Spring 会结合字段名、参数名辅助匹配,但核心仍然是按类型解析,再结合限定条件选择 Bean,不应把“字段名碰巧对上”当成主要设计手段。
4.4 required 属性
@Autowired 默认是必须依赖:
@Autowired
private UserRepository userRepository;
等价于:
@Autowired(required = true)
private UserRepository userRepository;
如果找不到依赖,启动时就会报错。
也可以改成非必须:
@Autowired(required = false)
private UserRepository userRepository;
但这通常意味着该字段可能为 null,需要在业务代码里自行判断:
public void register(String username) {
if (userRepository != null) {
userRepository.save(username);
}
}
这种设计会降低对象状态的一致性。 对于核心依赖,不推荐把 required=false 当作常规方案。
更合理的做法一般是:
- 用构造器明确声明“这是必须依赖”
- 对可选依赖使用
Optional<T>、ObjectProvider<T>或条件化装配
例如:
@Service
public class UserService {
private final Optional<UserRepository> userRepository;
public UserService(Optional<UserRepository> userRepository) {
this.userRepository = userRepository;
}
public void register(String username) {
userRepository.ifPresent(repo -> repo.save(username));
}
}
4.5 属性注入的优点
虽然工程上不推荐作为首选,但字段注入也不是一无是处,它的优点主要有:
1. 代码短,初学者容易接受
@Autowired
private UserRepository userRepository;
相比构造器注入,视觉上确实更“省事”。
2. 老项目兼容性高
大量 Spring、Spring Boot 早期项目都使用字段注入。 在维护老系统时,理解和兼容这种写法是必要的。
3. 对样板代码敏感的场景看起来简洁
在依赖很多但结构简单的类里,字段注入会显得比较紧凑。
但这些优点更多是“书写便利性”,而不是“设计质量”。
4.6 属性注入的核心问题
1. 依赖关系不显式
看下面这段代码:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
@Autowired
private SmsService smsService;
}
从类的对外接口上看,根本不知道这个类依赖什么。 只有进入类内部、阅读字段,才能发现它依赖多个组件。
而构造器注入会把依赖关系直接暴露在签名中:
public UserService(UserRepository userRepository,
EmailService emailService,
SmsService smsService) {
...
}
这会让类的设计更加透明。
2. 无法保证对象在构造完成后就是“完整可用”的
字段注入依赖容器后续通过反射赋值。 也就是说,对象先被创建,再被填充依赖。
这带来一个问题:对象构造完成时,并不一定处于完整状态。
而好的面向对象设计通常要求: 一个对象一旦构造成功,就应该满足基本可用条件。
3. 不利于单元测试
字段注入的类在脱离 Spring 容器后,测试会比较麻烦。
例如:
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public void register(String username) {
userRepository.save(username);
}
}
在单元测试中,你不能优雅地直接构造它并传入 Mock,往往只能:
- 启动 Spring 测试上下文
- 或者通过反射设置字段
- 或者引入
@InjectMocks
而构造器注入则天然适合测试:
UserRepository mockRepo = Mockito.mock(UserRepository.class);
UserService userService = new UserService(mockRepo);
这也是现代 Spring 项目越来越偏向构造器注入的重要原因。
4. 不利于不可变设计
字段注入要求依赖字段通常不能是 final,因为 Spring 需要在实例化后再赋值。
@Autowired
private UserRepository userRepository;
这意味着依赖本身不能天然具备不可变约束。 而构造器注入可以把依赖声明为 final:
private final UserRepository userRepository;
这样类的状态更稳定,也更符合线程安全和可维护性思路。
5. 容易掩盖设计问题
一个类如果依赖过多,构造器注入会很快暴露这个问题,因为构造器参数会越来越长。 而字段注入会让这种坏味道被“隐藏”起来,导致类不断膨胀却不易察觉。
例如一个类有 10 个 @Autowired 字段,代码仍然“能写”,但这通常已经意味着职责过重、设计失衡。
4.7 属性注入适合什么场景
严格地说,在现代 Spring 工程中,字段注入更适合以下有限场景:
- 历史项目维护
- 测试样例、演示项目、临时代码
- 对设计约束要求不高的简单组件
- 某些框架集成代码中,对非核心依赖的快速装配
但在正式业务系统中,不应把字段注入作为默认首选方案。
5. 构造方法注入:当前最推荐的依赖注入方式
5.1 基本写法
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void register(String username) {
userRepository.save(username);
}
}
这就是典型构造器注入。
不过在现代 Spring 中,如果类只有一个构造器,那么 @Autowired 可以省略:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void register(String username) {
userRepository.save(username);
}
}
这也是当前最常见、最推荐的写法。
5.2 为什么构造器注入是首选
1. 依赖关系显式
构造器参数就是这个类运行所需的依赖列表。 阅读类签名就能立即知道它依赖什么。
2. 保证对象完整性
对象只有在所有必要依赖都准备好后才能构造成功。 这符合“构造即完成初始化”的设计原则。
3. 支持 final 字段
private final UserRepository userRepository;
这让依赖在对象生命周期内不可变,更安全、更清晰。
4. 更利于单元测试
无需启动 Spring 容器,直接 new 出对象即可测试。
5. 更容易暴露设计问题
构造器参数过多时,说明这个类依赖太多、职责过重,需要拆分。 这是一种非常有价值的设计反馈机制。
5.3 只有一个构造器时,@Autowired 为什么可以省略
从 Spring 4.3 开始,如果一个 Bean 类中只有一个构造器,Spring 会自动使用这个构造器进行注入,即使没有显式标注 @Autowired。
例如:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
这在 Spring Framework 4.3+ 中成立。 Spring Boot 的现代项目通常都基于这一行为,因此实际开发中经常不再写构造器上的 @Autowired。
版本说明
- Spring Framework 4.3 之前:单构造器通常仍建议显式写
@Autowired - Spring Framework 4.3 及之后:单构造器可自动注入,无需
@Autowired - Spring Boot 2.x / 3.x:通常直接采用“单构造器省略
@Autowired”的写法
5.4 多构造器场景
如果一个类有多个构造器,Spring 就无法自动判断该选哪个,此时需要通过 @Autowired 明确指定注入构造器。
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
this.emailService = null;
}
@Autowired
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
}
不过从设计角度看,业务 Bean 最好避免保留多个语义混乱的构造器。 通常推荐:
- 对外只保留一个主构造器
- 必需依赖全部放进该构造器
- 可选依赖用 Setter、
ObjectProvider或其他机制处理
5.5 多实现注入时的构造器写法
当接口有多个实现时,也可以在构造器参数上配合 @Qualifier:
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(@Qualifier("jdbcUserRepository") UserRepository userRepository) {
this.userRepository = userRepository;
}
}
这种写法比字段上配 @Qualifier 更清晰,因为依赖约束直接体现在构造器签名中。
5.6 Lombok 与构造器注入
在真实项目中,构造器注入经常和 Lombok 一起使用,以减少模板代码。
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public void register(String username) {
userRepository.save(username);
}
}
@RequiredArgsConstructor 会为所有 final 字段生成构造器。 配合 Spring 单构造器自动注入机制,代码会很简洁。
但要注意:
- 使用 Lombok 是为了减少样板代码,不是为了掩盖依赖设计
- 一个类如果有太多
final字段,即使写法很优雅,也仍然意味着职责可能过重
5.7 构造器注入的潜在问题
构造器注入虽然最推荐,但也不是没有代价。
1. 依赖很多时,构造器会很长
public UserService(A a, B b, C c, D d, E e, F f) { ... }
这会让代码显得冗长。 但这个问题本质上不是“构造器注入的错”,而是类的职责过多。
2. 循环依赖更容易暴露
例如:
A的构造器依赖BB的构造器依赖A
@Service
public class AService {
public AService(BService bService) {
}
}
@Service
public class BService {
public BService(AService aService) {
}
}
这种构造器循环依赖会导致容器无法创建 Bean。
但从工程角度讲,这恰恰是优点: 它强迫你正视设计问题,而不是让问题潜伏。
6. Setter 注入:适合可选依赖和后置配置
6.1 基本写法
@Service
public class UserService {
private UserRepository userRepository;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void register(String username) {
userRepository.save(username);
}
}
Setter 注入的本质是:Spring 创建对象后,再调用 Setter 方法把依赖设置进去。
6.2 Setter 注入的特点
Setter 注入介于字段注入和构造器注入之间:
- 比字段注入更显式,因为依赖通过公开方法注入
- 但仍然是在对象创建后完成赋值
- 适合“非必须”“可替换”“后置设置”的依赖
6.3 Setter 注入适合哪些场景
1. 可选依赖
如果某个依赖不是对象运行的绝对前提,就可以考虑 Setter 注入。
@Service
public class NotificationService {
private SmsService smsService;
@Autowired(required = false)
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
}
public void notifyUser(String msg) {
if (smsService != null) {
smsService.send(msg);
}
}
}
2. 配置型依赖、开关型依赖
例如某些策略、扩展点、插件、适配器是可插拔的,不一定在构造阶段强制存在。
3. 遗留框架或 JavaBean 风格对象
有些对象强调无参构造 + Setter 赋值,这种情况下 Setter 注入比较自然。
6.4 Setter 注入的优点
1. 依赖可选性更强
构造器更适合必须依赖,Setter 更适合可选依赖。
2. 可读性比字段注入更好
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
至少依赖注入行为通过方法暴露出来了。
3. 支持后续替换
某些场景下,一个依赖在生命周期中可能需要调整或替换,Setter 形式更灵活。
6.5 Setter 注入的缺点
1. 对象可能处于不完整状态
和字段注入一样,Setter 注入是在对象实例化后执行的。 如果依赖没有及时注入,对象可能不能正确工作。
2. 不适合不可变设计
Setter 天然意味着“可修改”,不适合 final 依赖。
3. 依赖是否必须不够直观
一个 Setter 方法到底是必须调用还是可选调用,要靠开发者约定和文档约束,语义不如构造器强。
7. 三种注入方式的对比
下面从工程实践角度做一个系统比较。
| 对比维度 | 属性注入 @Autowired |
构造方法注入 | Setter 注入 |
|---|---|---|---|
| 依赖是否显式 | 否 | 是 | 较明显 |
是否支持 final |
不适合 | 支持 | 不支持 |
| 是否利于单元测试 | 较差 | 最好 | 较好 |
| 对象完整性 | 较弱 | 最强 | 较弱 |
| 是否适合必须依赖 | 一般 | 最适合 | 一般 |
| 是否适合可选依赖 | 可用但不优雅 | 可结合 Optional |
最自然 |
| 是否容易暴露设计问题 | 不容易 | 容易 | 一般 |
| 使用便利性 | 最高 | 较高 | 中等 |
| 现代 Spring 推荐程度 | 不推荐作为首选 | 最推荐 | 特定场景可用 |
结论非常明确:
- 必须依赖:优先使用构造方法注入
- 可选依赖:可考虑 Setter 注入
- 字段注入:不作为正式项目默认方案
8. @Autowired 不只是字段注入,它也能标在构造器、Setter、方法参数上
很多人一提 @Autowired,脑海里就只有:
@Autowired
private XxxService xxxService;
实际上,@Autowired 是一个自动装配注解,它不仅能标在字段上,还能标在:
- 构造器
- Setter 方法
- 普通方法
- 数组、集合、Map 参数
- 某些场景下的配置方法参数
例如:
构造器注入
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
Setter 注入
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
普通方法注入
@Autowired
public void init(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
普通方法注入虽然技术上可行,但在日常业务代码中并不常作为主流选择。 它通常用于某些初始化组合场景,而不是通用依赖声明方式。
9. 注入集合、数组、Map:Spring 的批量装配能力
当容器中有多个同类型 Bean 时,Spring 不仅可以注入其中一个,也可以一次性注入全部候选对象。
9.1 注入 List
public interface PayService {
String type();
}
@Service
public class AliPayService implements PayService {
@Override
public String type() {
return "alipay";
}
}
@Service
public class WechatPayService implements PayService {
@Override
public String type() {
return "wechat";
}
}
@Service
public class PaymentRouter {
private final List<PayService> payServices;
public PaymentRouter(List<PayService> payServices) {
this.payServices = payServices;
}
public PayService getService(String type) {
return payServices.stream()
.filter(service -> service.type().equals(type))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("unsupported pay type"));
}
}
这是一种很常见的策略模式实现方式。
9.2 注入 Map<String, T>
@Service
public class PaymentRouter {
private final Map<String, PayService> payServiceMap;
public PaymentRouter(Map<String, PayService> payServiceMap) {
this.payServiceMap = payServiceMap;
}
public PayService getServiceByBeanName(String beanName) {
return payServiceMap.get(beanName);
}
}
这里的 key 默认是 Bean 名称,value 是对应 Bean 实例。
这种能力在做:
- 策略路由
- 插件式扩展
- 多实现分发
- 规则处理链
时非常有用。
10. @Resource、@Inject 与 @Autowired 的区别
虽然本文主题是 Spring DI,但工程中还经常看到 @Resource、@Inject,有必要区分。
10.1 @Autowired
- 来自 Spring
- 核心是按类型装配
- 可与
@Qualifier、@Primary配合 - 是 Spring 体系中最常用的自动注入注解
10.2 @Resource
- 来自 JSR-250 / Jakarta
- 常见语义是先按名称,再按类型
- 更偏向 Java 标准注解风格
例如:
@Resource(name = "jdbcUserRepository")
private UserRepository userRepository;
如果你明确希望按名称注入,@Resource 很直接。
10.3 @Inject
- 来自 JSR-330 / Jakarta Inject
- 语义上与
@Autowired接近 - 更偏标准化规范
不过在 Spring 项目中,团队通常会统一一种风格。 从可读性和生态习惯来看,现代 Spring 项目仍然以构造器注入 + Spring 原生方式最常见。
版本说明
- 在较新的 Spring 6 / Spring Boot 3 环境中,Jakarta 命名空间已全面替代旧
javax.* - 如果项目升级到 Spring Boot 3,需注意
javax.annotation、javax.inject与jakarta.*包迁移问题
11. Spring 循环依赖与注入方式的关系
循环依赖是理解 DI 时绕不开的话题。
11.1 什么是循环依赖
例如:
AService依赖BServiceBService又依赖AService
@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;
}
}
这是典型的构造器循环依赖。
11.2 构造器注入下的循环依赖
这种场景通常会直接失败,因为:
- 创建
AService需要先有BService - 创建
BService又需要先有AService
容器无法完成闭环。
11.3 字段注入 / Setter 注入为什么有时“看起来能行”
历史上,Spring 通过三级缓存等机制,对单例 Bean 的部分属性级循环依赖可以做一定程度的提前暴露和解决。 但这并不意味着循环依赖是合理设计,更不意味着应该依赖这种机制。
尤其是在现代 Spring Boot 版本中,循环依赖默认策略更严格,很多项目配置下会直接禁止。
工程结论
-
不要为了“绕开循环依赖”而退回字段注入
-
真正正确的做法是重构设计
- 拆分类职责
- 引入中间协调层
- 提取公共能力
- 使用事件机制解耦
- 重新审视模块边界
构造器注入让循环依赖更早暴露,这不是缺点,而是设计质量保障。
12. 依赖注入的工程实践建议
12.1 对必须依赖,一律优先构造器注入
这是最重要的一条原则。
例如业务服务、仓储、网关、领域能力等核心依赖,应该通过构造器注入:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PaymentService paymentService;
public OrderService(OrderRepository orderRepository,
InventoryService inventoryService,
PaymentService paymentService) {
this.orderRepository = orderRepository;
this.inventoryService = inventoryService;
this.paymentService = paymentService;
}
}
12.2 对可选依赖,优先考虑 Setter 注入或延迟获取
例如扩展能力、非核心通知能力、调试型组件等,可以考虑:
- Setter 注入
Optional<T>ObjectProvider<T>- 条件装配
例如:
@Service
public class ReportService {
private final ObjectProvider<ExportService> exportServiceProvider;
public ReportService(ObjectProvider<ExportService> exportServiceProvider) {
this.exportServiceProvider = exportServiceProvider;
}
public void export() {
ExportService exportService = exportServiceProvider.getIfAvailable();
if (exportService != null) {
exportService.export();
}
}
}
这种写法比 required = false 更清晰。
12.3 避免在正式项目中大量使用字段注入
字段注入的便利性很诱人,但不应以牺牲设计质量为代价。 尤其是在以下类型代码中,更不建议字段注入:
- 领域服务
- 核心业务服务
- 复杂聚合协调器
- 需要高可测试性的组件
- 公共基础设施模块
12.4 构造器参数过多时,不要急着嫌弃构造器
当你发现一个类的构造器有 7 个、8 个参数时,第一反应不应该是“改成 @Autowired 字段更方便”,而应该是:
- 这个类是不是职责过多?
- 是否违反单一职责原则?
- 是否存在多个子流程可以拆分?
- 是否把不该耦合的能力揉在了一起?
也就是说,构造器长往往是在提醒你类设计有问题。
12.5 团队要统一注入风格
在中大型项目中,最怕的是风格混乱:
- 有人用字段注入
- 有人用 Setter 注入
- 有人写构造器 +
@Autowired - 有人用 Lombok
- 有人混用
@Resource
这会导致代码可读性和维护成本下降。 更合理的团队规范通常是:
- 默认使用构造器注入
- 单构造器省略
@Autowired - 可选依赖用 Setter 或
ObjectProvider - 多实现冲突时用
@Qualifier或@Primary - 尽量减少字段注入
13. 一个完整示例:三种注入方式对照
13.1 依赖组件
public interface UserRepository {
void save(String username);
}
@Repository
public class JdbcUserRepository implements UserRepository {
@Override
public void save(String username) {
System.out.println("save user: " + username);
}
}
@Service
public class EmailService {
public void sendWelcomeMail(String username) {
System.out.println("send email to: " + username);
}
}
13.2 字段注入版本
@Service
public class UserServiceFieldInjection {
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
public void register(String username) {
userRepository.save(username);
emailService.sendWelcomeMail(username);
}
}
特点:
- 写法最短
- 依赖隐藏在字段中
- 不利于测试与不可变设计
13.3 构造器注入版本
@Service
public class UserServiceConstructorInjection {
private final UserRepository userRepository;
private final EmailService emailService;
public UserServiceConstructorInjection(UserRepository userRepository,
EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public void register(String username) {
userRepository.save(username);
emailService.sendWelcomeMail(username);
}
}
特点:
- 依赖关系清晰
final保证状态稳定- 最适合正式项目
13.4 Setter 注入版本
@Service
public class UserServiceSetterInjection {
private UserRepository userRepository;
private EmailService emailService;
@Autowired
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Autowired
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
public void register(String username) {
userRepository.save(username);
emailService.sendWelcomeMail(username);
}
}
特点:
- 比字段注入更显式
- 适合某些后置设置或可选依赖
- 但对象完整性不如构造器注入
14. 常见误区与纠正
14.1 误区一:@Autowired 就等于字段注入
错误。 @Autowired 是自动装配注解,不等于某一种注入姿势。 它可以用于字段、构造器、Setter、方法参数。
14.2 误区二:字段注入最简洁,所以最好
“简洁”只是表面书写成本低。 从可测试性、可维护性、对象一致性、设计透明度来看,字段注入并不是最佳方案。
14.3 误区三:构造器太长,所以不适合项目开发
构造器太长说明类设计可能有问题,而不是说明构造器注入有问题。 不要用字段注入去隐藏设计缺陷。
14.4 误区四:Setter 注入已经过时
也不准确。 Setter 注入并没有过时,它在“可选依赖、后置配置、JavaBean 风格对象”中仍有实际价值。 只是对于核心必须依赖,它不是首选。
14.5 误区五:循环依赖可以靠字段注入解决,所以字段注入更灵活
这是一种危险认知。 循环依赖本质上是设计问题,不能靠注入方式“糊过去”。 真正正确的做法是重构。
15. 最终结论:实际项目该怎么选
在 Spring 项目中,DI 的核心不是“会不会写注解”,而是是否能用正确的注入方式表达对象之间的真实依赖关系。
三种方式的定位可以总结为:
构造方法注入
最推荐,适合必须依赖。 它具备最好的显式性、可测试性、不可变性和设计约束能力,是现代 Spring 项目的默认首选。
Setter 注入
适合可选依赖、后置配置、扩展点装配。 它不是主流默认方案,但在合适场景下非常合理。
属性注入 @Autowired
最常见,但不应作为正式项目首选。 它适合历史代码、简单示例、临时场景,不适合承担现代业务系统中的主要依赖声明职责。
真正成熟的 Spring 开发实践,不是盲目追求代码最短,而是让依赖关系清晰、对象状态稳定、系统结构可测试、可维护、可扩展。 从这个角度看,构造器注入之所以成为主流,并不是因为“流行”,而是因为它更符合工程本质。