为什么 Spring 建议使用构造器注入?

为什么 Spring 建议使用构造器注入?

Spring 官方文档里有一句话:

The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state.

翻译过来就是:Spring 团队推荐构造器注入,因为它能让组件变成不可变对象,保证必需依赖不为 null,并且返回给调用方的组件总是完全初始化好的。

下面从几个角度展开。

一、依赖不可变,用 final 修饰

构造器注入的字段可以声明为 final:

java 复制代码
@Service
public class UserService {
    private final UserDao userDao;
    private final EmailService emailService;
    
    public UserService(UserDao userDao, EmailService emailService) {
        this.userDao = userDao;
        this.emailService = emailService;
    }
}

final 意味着依赖一旦注入就不能被修改,对象天生线程安全,也避免了后续代码意外替换依赖。字段注入和 Setter 注入做不到这一点,字段只能是可变的。

二、依赖完全就绪,避免空指针

构造器注入在对象创建时就把所有依赖传进来。对象一旦创建出来,依赖一定是非 null 的:

java 复制代码
UserService service = new UserService(userDao, emailService);
service.register(user);  // userDao 和 emailService 一定可用

字段注入则不同:

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserDao userDao;  // 实例化时是 null,属性填充后才被注入
}

如果有人手动 new UserService(),userDao 就是 null,调用时直接空指针。构造器注入强制调用方提供依赖,从源头杜绝了这个问题。

三、便于单元测试

构造器注入让测试非常自然:

java 复制代码
@Test
void testRegister() {
    UserDao mockDao = mock(UserDao.class);
    EmailService mockEmail = mock(EmailService.class);
    
    // 直接传入 mock 对象
    UserService service = new UserService(mockDao, mockEmail);
    service.register(new User("张三"));
    
    verify(mockDao).save(any(User.class));
}

字段注入的测试需要启动 Spring 容器,或者用 ReflectionTestUtils.setField() 反射赋值,麻烦且不直观。构造器注入不需要 Spring 容器,纯 Java 对象测试即可。

四、依赖关系清晰,暴露设计问题

构造方法参数列表直接反映了这个类需要哪些依赖:

java 复制代码
public UserService(UserDao userDao, EmailService emailService, 
                   SmsService smsService, LogService logService,
                   CacheService cacheService, ConfigService configService) {
    // ...
}

如果参数超过 5 个,一眼就能看出这个类职责过重,需要拆分。字段注入没有这种视觉提示,依赖可以悄悄加到十几个而不被发现。

五、避免循环依赖被掩盖

构造器注入无法解决循环依赖,遇到循环依赖会直接启动失败:

java 复制代码
@Service
public class AService {
    public AService(BService bService) { }
}

@Service
public class BService {
    public BService(AService aService) { }
}

启动报 BeanCurrentlyInCreationException。这看起来是缺点,实际上是设计上的保护。循环依赖通常意味着两个类的职责边界不清晰。构造器注入强制暴露这个问题,促使开发者重构,而不是靠三级缓存"兜底"掩盖。

字段注入的循环依赖可以通过三级缓存解决,但问题本身还在,只是被隐藏了。Spring Boot 2.6 之后默认禁止循环依赖,也是这个思路。

六、与 Spring 框架的集成

Spring 4.3 之后,如果一个类只有一个构造方法,@Autowired 可以省略:

java 复制代码
@Service
public class UserService {
    private final UserDao userDao;
    
    public UserService(UserDao userDao) {  // 不需要 @Autowired
        this.userDao = userDao;
    }
}

配合 Lombok 的 @RequiredArgsConstructor,代码非常简洁:

java 复制代码
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserDao userDao;
    private final EmailService emailService;
}

编译后自动生成包含所有 final 字段的构造方法。

七、三种注入方式对比

对比维度 构造器注入 Setter 注入 字段注入
依赖不可变 ✅ final ❌ ❌
空指针风险 无 有 有
单元测试便利性 高 中 低
循环依赖支持 不支持 支持 支持
依赖过多时提示 明显 不明显 不明显
Spring 官方推荐 ✅ ⚠️ 可选 ❌ 不推荐

Setter 注入适合可选依赖:

java 复制代码
@Autowired(required = false)
public void setCacheService(CacheService cacheService) {
    this.cacheService = cacheService;
}

字段注入写起来最方便,但代价是牺牲了测试性、不可变性和设计清晰度。

八、官方文档的推荐

Spring 官方文档明确说:

  • 构造器注入用于必需依赖。
  • Setter 注入用于可选依赖。
  • 字段注入不推荐,因为无法使用 final,不利于测试,且容易隐藏依赖过多的问题。

九、总结

原因 说明
依赖不可变 字段可以 final,线程安全
依赖完全就绪 对象创建后依赖一定非 null
便于测试 直接传入 Mock,无需反射
关系清晰 构造方法参数暴露依赖数量
暴露设计问题 循环依赖直接失败,促使重构
官方推荐 Spring 团队明确提倡

Spring 建议构造器注入,本质上是在引导开发者写出不可变、依赖明确、易于测试、设计清晰 的代码。字段注入虽然写起来方便,但牺牲了这些长期收益。如果一个类只有一个构造方法,@Autowired 都可以省掉,代码反而更简洁。

相关推荐
小蒜学长14 小时前
编程语言在线学习平台的设计与实现(代码+数据库+LW)
java·spring boot·后端·编程语言在线学习平台·在线编码
Flynn Liang14 小时前
Spring AI 多模态实战(下):文生图 + TTS 语音合成(含 2.0 流式音频分片)
人工智能·spring·文生图·音视频·语音合成·tts·springai
步行cgn14 小时前
Spring MVC 执行流程详解:一次请求的完整旅程
java·spring·mvc
增量星球15 小时前
Spring AI 2.0 Tool Calling 到底怎么跑起来的:从 @Tool 到两次模型调用
java·agent·springai·ai coding
步行cgn15 小时前
Spring MVC 九大核心角色详解:请求处理链路中的每一个关键组件
java·spring·mvc
智恒百亿15 小时前
一台 5090 八卡服务器怎么分给一个团队用?多模型多任务的显存分配实战
java·大数据·人工智能
瑞码空间15 小时前
面向接口编程与开闭原则在模拟 JDBC 驱动中的应用
java·jdbc
zhoujungis15 小时前
登录态 1 小时就掉?Django + Vue 双 Token 认证实战 | 尘世栈帧
后端
Félix25115 小时前
Nacos学习笔记(SpringBoot/SpringCloud)
java·spring boot·spring
文丑颜不良啊15 小时前
LeetCode1581~1600题目解析
java·leetcode