为什么 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 都可以省掉,代码反而更简洁。

相关推荐
Nefu_lyh1 小时前
【工作杂谈_20261012】数据平台与工程治理: Spark OOM 排查:Java Heap、Executor 与代码内存模型
java·大数据·spark
小蒜学长1 小时前
基于Vue.js + Spring Boot的酒店客房预订系统设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·酒店客房预订系统
打工仔折腾 AI1 小时前
鸿蒙平板远控电脑实测:ToDesk蓝牙键鼠、双模式鼠标与跨端剪贴板落地记录
人工智能·后端·python·性能优化·计算机外设·电脑·harmonyos
真恋寄语枫秋1 小时前
【Java零基础入门28】Java包机制详解:package包定义、import导入、权限访问、项目分层管理
java
豆角焖肉1 小时前
SpringBoot + Redis 缓存实战
java·spring boot·redis·后端·缓存·菜单缓存
西柚不烦恼1 小时前
jenkins版本和java版本的依赖
java·开发语言·jenkins
牧瀬クリスだ1 小时前
Ajax入门:从原生XHR到axios实战
java·spring boot·ajax·okhttp
IT_陈寒1 小时前
深入理解Redis:核心原理与最佳实践
前端·人工智能·后端
打工仔折腾 AI1 小时前
鸿蒙端ToDesk远控电脑实测:蓝牙键鼠、双模式鼠标与跨端剪贴板实践
后端·python·性能优化·计算机外设·电脑·harmonyos·ai agent 实战