为什么 Spring 更推荐构造器注入,而不是字段 `@Autowired`

为什么 Spring 更推荐构造器注入,而不是字段 @Autowired

1. 两种写法

字段注入通常写成:

java 复制代码
@Service
public class UserService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void saveUser() {
        redisTemplate.opsForValue().set("name", "Tom");
    }
}

构造器注入写成:

java 复制代码
@Service
public class UserService {
    private final RedisTemplate<String, Object> redisTemplate;

    public UserService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void saveUser() {
        redisTemplate.opsForValue().set("name", "Tom");
    }
}

这里需要准确区分:并不是"构造器注入不属于 @Autowired",而是当 Spring Bean 只有一个构造器时,Spring 可以自动使用该构造器完成依赖注入,因此通常不必再显式标注 @Autowired

2. 字段注入的执行过程

使用字段注入时,Spring 大致会执行以下过程:

text 复制代码
调用无参构造器创建 UserService
→ 此时 redisTemplate 为 null
→ Spring 发现字段上的 @Autowired
→ 从容器中查找 RedisTemplate Bean
→ 通过反射给字段赋值

因此,对象刚被创建出来时并不完整,它需要等待 Spring 后续填充字段。如果对象不是由 Spring 创建,而是开发者自己执行:

java 复制代码
UserService userService = new UserService();

那么 Spring 不会参与这个对象的创建,也不会处理字段上的 @Autowired,此时 redisTemplate 仍然是 null。调用业务方法时可能出现:

text 复制代码
java.lang.NullPointerException

3. 构造器注入的执行过程

使用构造器注入时,Spring 必须先找到构造器需要的依赖,再创建业务对象:

text 复制代码
准备创建 UserService
→ 分析构造器参数
→ 从容器中找到 RedisTemplate Bean
→ 调用 new UserService(redisTemplate)
→ 得到依赖完整的 UserService

对象一旦创建完成,必需依赖就已经存在,不会先产生一个依赖为空的"半初始化对象"。

4. 推荐构造器注入的主要原因

4.1 保证必需依赖在创建时就存在

字段注入允许创建一个依赖尚未赋值的对象:

java 复制代码
@Service
public class UserService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
}

而构造器注入强制调用者提供依赖:

java 复制代码
public UserService(RedisTemplate<String, Object> redisTemplate) {
    this.redisTemplate = redisTemplate;
}

此时不能直接执行:

java 复制代码
new UserService();

必须提供 RedisTemplate

java 复制代码
new UserService(redisTemplate);

这使无效对象更难被创建。

4.2 可以把依赖声明为 final

构造器注入允许使用:

java 复制代码
private final RedisTemplate<String, Object> redisTemplate;

final 表示该引用必须在构造阶段完成初始化,并且之后不能被重新指向其他对象。这能更清楚地表达:RedisTemplateUserService 的固定依赖,而不是运行过程中可以随意替换的普通字段。

字段注入通常不能把字段直接声明为由 Spring 后续赋值的普通 final 字段,因为 final 字段必须在构造阶段完成初始化。

4.3 依赖关系更加清晰

看到下面的构造器,就能直接知道 UserService 创建时依赖哪些对象:

java 复制代码
public UserService(
        UserRepository userRepository,
        RedisTemplate<String, Object> redisTemplate) {
    this.userRepository = userRepository;
    this.redisTemplate = redisTemplate;
}

字段注入则把依赖分散在类的不同位置:

java 复制代码
@Autowired
private UserRepository userRepository;

@Autowired
private RedisTemplate<String, Object> redisTemplate;

当一个类的依赖很多时,构造器会变得很长。这不完全是构造器注入的缺点,反而是在提醒开发者:这个类可能承担了过多职责,需要考虑拆分。

4.4 更方便进行单元测试

使用构造器注入时,可以在不启动 Spring 容器的情况下直接创建测试对象:

java 复制代码
RedisTemplate<String, Object> mockTemplate = mock(RedisTemplate.class);
UserService userService = new UserService(mockTemplate);

测试代码能够明确提供模拟依赖。

字段注入没有公开的依赖入口:

java 复制代码
UserService userService = new UserService();

此时 redisTemplate 是私有字段,测试往往需要启动 Spring 上下文、使用反射修改字段,或者使用额外的测试工具完成注入,测试成本更高。

4.5 减少对 Spring 容器的隐式依赖

字段注入写法只有在 Spring 处理对象后才正常工作:

java 复制代码
@Autowired
private RedisTemplate<String, Object> redisTemplate;

单看 Java 类本身,并不能保证字段已经赋值。构造器注入则符合普通 Java 的对象创建规则,即使离开 Spring,也可以正常创建和测试对象:

java 复制代码
UserService userService = new UserService(redisTemplate);

这使业务类与 Spring 的耦合更弱,类本身也更容易复用。

4.6 更容易发现循环依赖

假设 AService 依赖 BService,而 BService 又依赖 AService

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

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

构造器注入会直接暴露循环关系,因为创建 AService 前需要 BService,创建 BService 前又需要 AService。这通常说明类之间的职责划分存在问题。

字段注入可能让循环关系在代码中不够明显。虽然 Spring 在某些情况下曾能通过提前暴露对象处理部分循环依赖,但依赖这种机制会增加设计复杂度。尽早发现并消除循环依赖通常更合理。

5. 为什么单个构造器可以省略 @Autowired

下面两种写法在只有一个构造器时通常效果相同:

java 复制代码
@Service
public class UserService {
    private final RedisTemplate<String, Object> redisTemplate;

    @Autowired
    public UserService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}
java 复制代码
@Service
public class UserService {
    private final RedisTemplate<String, Object> redisTemplate;

    public UserService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}

Spring 发现这个类只有一个构造器后,会默认把它作为依赖注入入口,因此 @Autowired 可以省略。

如果类中存在多个构造器,Spring需要判断使用哪一个。这时可以显式标注需要用于注入的构造器,或者重新设计构造方式,避免产生歧义。

6. 多个同类型 Bean 时如何处理

构造器注入并不会消除同类型 Bean 的歧义。例如容器中存在两个 RedisTemplate Bean 时,可以使用 @Qualifier 指定名称:

java 复制代码
@Service
public class UserService {
    private final RedisTemplate<String, Object> redisTemplate;

    public UserService(
            @Qualifier("redisTemplate")
            RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}

也可以在某个 Bean 上使用 @Primary,将其设为默认候选对象。

7. 可选依赖怎么办

构造器注入更适合"没有它就无法正常工作"的必需依赖。如果某个依赖确实是可选的,可以使用 OptionalObjectProvider,或者在合适场景下使用 Setter 注入。例如:

java 复制代码
@Service
public class ReportService {
    private final ObjectProvider<CacheService> cacheServiceProvider;

    public ReportService(ObjectProvider<CacheService> cacheServiceProvider) {
        this.cacheServiceProvider = cacheServiceProvider;
    }
}

使用时再判断容器中是否存在对应 Bean:

java 复制代码
CacheService cacheService = cacheServiceProvider.getIfAvailable();

不应为了省事,把所有依赖都改成可空字段,否则会把配置错误推迟到业务运行阶段。

8. Lombok 简化写法

当项目使用 Lombok 时,可以使用 @RequiredArgsConstructor 自动生成包含所有 final 字段的构造器:

java 复制代码
@Service
@RequiredArgsConstructor
public class UserService {
    private final RedisTemplate<String, Object> redisTemplate;

    public void saveUser() {
        redisTemplate.opsForValue().set("name", "Tom");
    }
}

它大致等价于:

java 复制代码
public UserService(RedisTemplate<String, Object> redisTemplate) {
    this.redisTemplate = redisTemplate;
}

不过阅读代码时仍应理解真正完成注入的是生成后的构造器,而不是 Lombok 注解本身。

9. 三种注入方式对比

对比项 字段注入 构造器注入 Setter 注入
必需依赖是否在创建时提供
是否可以使用 final 通常不可以 可以 不可以
依赖是否清晰 一般 清晰 较清晰
单元测试是否方便 一般 方便 较方便
适合场景 简单示例、旧代码 必需依赖,推荐默认使用 可选或可替换依赖

10. 总结

Spring 更推荐构造器注入,并不是因为字段上的 @Autowired 无法工作,而是因为构造器注入能更好地保证对象完整性:

text 复制代码
字段注入:
先创建对象
→ 对象中的依赖暂时为 null
→ Spring 再通过反射填充字段

构造器注入:
先找到全部必需依赖
→ 再创建完整对象

可以用一句话记住:

必需依赖使用构造器注入,可选依赖根据实际情况使用 Setter、OptionalObjectProvider

因此,推荐写法是:

java 复制代码
@Service
public class UserService {
    private final RedisTemplate<String, Object> redisTemplate;

    public UserService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}

而不是默认使用:

java 复制代码
@Autowired
private RedisTemplate<String, Object> redisTemplate;
相关推荐
用户298698530141 小时前
Java 创建 CSV 文件的三种实用方法:从零构建、数组写入与 Excel 转换
java·后端·excel
Cry丶1 小时前
一次 FTP 上传引发 504 Gateway Timeout 的排查复盘
java·nginx·ftp·openresty·被动模式·504 gateway·生产问题排查
码农进化录1 小时前
Java 程序员的 AI 进化论 | AI 辅助 CI/CD,让 AI 帮你做代码审查和部署决策
java·spring boot·openai
zyplayer-doc1 小时前
企业文档的进化:网盘和在线文档的下一个升级方向—知识库系统
java·大数据·数据库·人工智能·ocr
吃饱了得干活1 小时前
从零设计一个高并发点赞系统:缓存+MQ异步持久化实战
java·架构
奇牙coding2 小时前
gpt-5.6-luna 调用报 400 bad_request 怎么办?同样的 messages 在 gpt-5.5 没问题——两处校验差异和修复写法
java·windows·gpt·ai
凤山老林2 小时前
从“无法创建本地线程“异常说起:深度解析线程栈与JVM线程优化指南
java·开发语言·jvm
聚美智数2 小时前
常见疾病查询-疾病症状—疾病介绍-疾病大全-疾病治疗查询API接口介绍
android·java·数据库
慧都小妮子2 小时前
Aspose.Words for JasperReports v26.6 发布,底层Java大升级
java·pdf·报表工具·aspose.words·文档转换·报表导出·jasperreports