Spring构造器注入及@Autowired、lombok的@RequiredArgsConstructor的异同点说明

 更新时间:2026年03月01日 10:36:49   作者:花花鱼  
本文介绍了Spring框架中构造器注入的实现原理,包括Spring如何找到参数、构造器注入与Autowired的区别,以及为什么Spring官方推荐构造器注入,文章还强调了构造器注入中使用final关键字的好处,并提供了代码示例,帮助读者更好地理解和应用Spring的最佳实践

一、构造器注入:Spring 是如何 “找到参数” 的?

构造器SysUserController:

public SysUserController(SysUserService sysUserService, 
                         ApplicationContext applicationContext, 
                         UserAuthService userAuthService) {
    this.sysUserService = sysUserService;
    this.applicationContext = applicationContext;
    this.userAuthService = userAuthService;
}

Spring 创建 SysUserController Bean 时,找构造器参数的过程分 3 步,核心是「按类型匹配 + 按名称兜底」:

步骤 1:扫描并注册所有 Bean

Spring 启动时,会扫描标注了 @Service/@Component/@Repository 等注解的类,把它们实例化成 Bean,并存入「Spring 容器」(可以理解为一个 “Bean 字典”):

  • SysUserService:因为加了 @Service,会被注册为「类型 = SysUserService,名称 = sysUserService」的 Bean;
  • ApplicationContext:Spring 内置的核心 Bean,启动时自动注册,类型就是 ApplicationContext
  • UserAuthService:它的实现类(比如 UserAuthServiceImpl)加了 @Service,会被注册为「类型 = UserAuthService(接口),名称 = userAuthServiceImpl」的 Bean。

步骤 2:解析构造器参数,按「类型」匹配 Bean

Spring 发现 SysUserController 有且只有一个有参构造器,会:

遍历构造器的每个参数,先按「参数类型」去容器里找匹配的 Bean:

  • 第一个参数 SysUserService:容器里有且只有一个 SysUserService 类型的 Bean → 直接匹配;
  • 第二个参数 ApplicationContext:容器里只有一个 Spring 内置的 ApplicationContext Bean → 直接匹配;
  • 第三个参数 UserAuthService:容器里有 UserAuthService 类型的实现类 Bean → 直接匹配。

如果一个类型对应多个 Bean(比如有两个 UserAuthService 实现类),Spring 会再按「参数名称」匹配(比如参数名是 userAuthService,就找名称为 userAuthService 的 Bean);

把匹配到的 Bean 作为参数,调用构造器,创建 SysUserController 实例。

步骤 3:特殊情况处理(兜底逻辑)

  • 如果按类型 / 名称都找不到参数对应的 Bean → 抛出 NoSuchBeanDefinitionException(Bean 不存在);
  • 如果一个类型有多个 Bean,且参数名称也匹配不上 → 抛出 NoUniqueBeanDefinitionException(Bean 不唯一);

解决方法:用 @Qualifier("bean名称") 标注参数,指定具体要哪个 Bean,比如:

运行

public SysUserController(SysUserService sysUserService, 
                         ApplicationContext applicationContext, 
                         @Qualifier("userAuthServiceImpl") UserAuthService userAuthService) {
    // ...
}

二、构造器注入 vs @Autowired:核心本质差异

先给结论:两者最终都是从 Spring 容器找 Bean,但注入时机、强制约束、安全性完全不同,用表格对比核心差异:

表格

维度构造器注入@Autowired 字段注入
注入时机Bean 实例化时(构造阶段)Bean 实例化后(初始化阶段)
依赖强制性必须找到所有参数 Bean,否则实例化失败可选(默认 required=true,可设 false)
字段不可变性可加 final,实例化后字段不可修改不能加 final,字段可被篡改
空指针风险无(实例化时就注入,字段必不为 null)有(初始化失败时字段为 null)
依赖透明度所有依赖都在构造器参数里,一目了然依赖藏在字段里,需看注解才知道
循环依赖处理无法解决循环依赖(Spring 不支持)可以解决循环依赖(Spring 缓存半成品)
单元测试友好性直接传参创建实例,无需 Spring 容器需 Mock 容器或反射注入,繁琐

关键差异拆解(通俗版)

1. 注入时机:“先有鸡还是先有蛋”

  • 构造器注入:创建 SysUserController 实例时,必须先拿到所有参数 Bean → 实例化完成后,所有依赖都已就绪;
  • @Autowired 字段注入:先调用无参构造器创建 SysUserController 空实例 → 再通过反射给字段赋值 → 如果赋值失败,实例已经创建但字段是 null。

举个例子:如果 SysUserService Bean 不存在:

  • 构造器注入:Spring 连 SysUserController 实例都创建不出来,启动直接报错,提前暴露问题;
  • @Autowired 注入:SysUserController 实例能创建,但 sysUserService 字段是 null → 运行时调用 sysUserService 才会报空指针,问题暴露晚。

2. 不可变性:“常量 vs 变量”

  • 构造器注入的字段可以加 final,一旦赋值就不能改 → 线程安全(多线程调用 Controller 时,字段不会被篡改);
  • @Autowired 字段不能加 final(加了会编译报错)→ 字段可能被反射 / 代码篡改,有线程安全风险。

3. 循环依赖:“死循环 vs 缓兵之计”

  • 构造器注入:如果 A 的构造器依赖 B,B 的构造器又依赖 A → Spring 实例化时会陷入死循环,直接报错(强制你解耦,是好事);
  • @Autowired 字段注入:Spring 会创建 “半成品 Bean”(实例化但未初始化),暂时缓存 → 先给 A 注入 B 的半成品,再给 B 注入 A 的半成品 → 表面解决循环依赖,但会埋下运行时隐患。

三、为什么 Spring 官方推荐构造器注入?

结合你的代码场景,推荐构造器注入的核心原因:

  1. 提前暴露问题:如果 SysUserService/UserAuthService 没被注册为 Bean,启动时就报错,而不是运行时才发现;
  2. 依赖透明:看构造器就知道 SysUserController 依赖哪些 Bean,不用翻字段注解;
  3. 线程安全final 字段不可变,Controller 是单例 Bean,多线程调用时不会出问题;
  4. 测试方便:单元测试时,不用启动 Spring 容器,直接 new SysUserController(mockSysUserService, mockContext, mockUserAuthService) 就能创建实例,无需 Mock 注解。

总结:

构造器注入找参数的逻辑:Spring 先扫描注册所有 Bean → 按参数「类型」匹配 Bean → 类型不唯一时按「名称」匹配 → 匹配失败则启动报错;

和 @Autowired 的本质差异:核心是「注入时机」和「依赖强制性」—— 构造器注入是 “实例化时必须有依赖”,@Autowired 是 “实例化后可选注入”;

实战建议

  • 必需依赖(比如你的 SysUserService/UserAuthService)→ 用构造器注入 + final
  • 可选依赖(非必须的 Bean)→ 用 @Autowired(required = false)
  • 杜绝 “构造器 + @Autowired” 混用,避免依赖不透明和初始化风险。

你的这段构造器注入代码是完全符合 Spring 最佳实践的,这也是为什么我们之前要把 SysUserService 加到构造器里,而不是保留 @Autowired 的原因。

Spring 注入规范的核心细节,我会结合你的代码场景,用 “原理 + 实战” 的方式讲清楚,让你不仅知其然,还知其所以然。

四、final在构造器注入中「不是必须加,但强烈推荐加」

1.final的核心作用(不是为了 Spring,是为了你的代码)

final 修饰字段的含义是:字段一旦赋值,就不能被修改。在构造器注入中加 final 的核心价值和 Spring 注入逻辑无关,而是为了代码的「安全性 + 可读性」:

  • 线程安全:Controller 是 Spring 单例 Bean(整个应用只有一个实例),多线程调用时,final 字段不会被篡改(比如不会被反射 / 其他代码意外赋值为 null);
  • 依赖不可变:明确表示这些依赖是 Controller 运行的 “必需品”,实例化后绝不更改;
  • 编译期校验:如果构造器没有给 final 字段赋值,编译器会直接报错(比如漏写 this.sysUserService = sysUserService),提前规避低级错误。

2. 不加final会怎么样?

如果去掉 final

private SysUserService sysUserService; // 去掉final
private ApplicationContext applicationContext;
private UserAuthService userAuthService;

public SysUserController(SysUserService sysUserService, ApplicationContext applicationContext, UserAuthService userAuthService) {
    this.sysUserService = sysUserService;
    this.applicationContext = applicationContext;
    this.userAuthService = userAuthService;
}
  • Spring 依然能正常注入(构造器注入的核心是 “构造器传参”,和 final 无关);
  • 但字段变成 “可变的”,存在被篡改的风险(比如在某个方法里写 this.sysUserService = null);
  • 代码可读性下降:无法一眼看出这些依赖是 “必需且不可变” 的。

3. 总结:final的使用原则

表格

场景是否加 final原因
构造器注入的「必需依赖」必须加线程安全 + 依赖不可变 + 编译校验
构造器注入的「可选依赖」不加(实际很少见,可选依赖建议用 @Autowired (required=false))

你的代码中 SysUserService/ApplicationContext/UserAuthService 都是 Controller 的必需依赖,加 final 是完全符合最佳实践的。

五、为什么你的代码不用@RequiredArgsConstructor?

1. 先搞懂@RequiredArgsConstructor是什么

@RequiredArgsConstructor 是 Lombok 提供的注解,作用是:自动为类中所有 final 字段生成对应的构造器

@RequiredArgsConstructor // 加这个注解
@RestController
@RequestMapping("/user")
public class SysUserController {
    private final SysUserService sysUserService;
    private final ApplicationContext applicationContext;
    private final UserAuthService userAuthService;

    // 不用手动写构造器,Lombok 会自动生成下面的代码:
    // public SysUserController(SysUserService sysUserService, ApplicationContext applicationContext, UserAuthService userAuthService) {
    //     this.sysUserService = sysUserService;
    //     this.applicationContext = applicationContext;
    //     this.userAuthService = userAuthService;
    // }
}

2. 你的代码不用@RequiredArgsConstructor的核心原因

不是 “不能用”,而是 “手动写构造器更清晰”,具体分两种情况:

  • 情况 1:你想「显式控制构造器」手动写构造器可以清晰看到所有依赖,尤其是团队协作时,新人不用去查 Lombok 注解的作用,直接看构造器就知道依赖了哪些 Bean;
  • 情况 2:不需要 Lombok 简化(字段少)你的构造器只有 3 个参数,手动写的代码量很少,没必要用 Lombok 注解;如果构造器参数有 5+ 个,用 @RequiredArgsConstructor 能减少重复代码。

3. 手动构造器 vs@RequiredArgsConstructor的对比

维度手动写构造器@RequiredArgsConstructor
代码可读性高(直接看构造器)中(需知道注解作用)
代码量稍多(3 行赋值)极少(1 行注解)
编译期依赖无(纯 Java 语法)需 Lombok 插件 / 依赖
调试友好性高(断点可直接打到构造器)中(需 Lombok 反编译看生成的代码)

4. 补充:如果要用@RequiredArgsConstructor,代码怎么写?

你的代码可以改成这样(完全等价,且更简洁):

@Slf4j
@RequiredArgsConstructor // 替代手动构造器
@RestController
@RequestMapping("/user")
public class SysUserController {
    private final SysUserService sysUserService;
    private final ApplicationContext applicationContext;
    private final UserAuthService userAuthService;

    // 无需手动写构造器,Lombok 自动生成
    // ... login 方法等逻辑不变
}

总结

关于 final

  • 构造器注入中 final 不是 Spring 强制要求的,但「必需依赖」强烈推荐加;
  • 核心价值:线程安全、依赖不可变、编译期校验,和构造器注入的 “找参数逻辑” 无关。

关于 @RequiredArgsConstructor

  • 你的代码不用这个注解,是因为「手动写构造器更清晰」,而非技术上不可用;
  • 如果想简化代码,加 @RequiredArgsConstructor 完全可行,和手动构造器效果一致。

你的这段代码写法(final 字段 + 手动构造器注入)是 Spring 官方最推荐的 “标准写法”,既保证了代码的安全性,又兼顾了可读性,完全无需调整。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java利用多线程和分块实现快速读取文件

    Java利用多线程和分块实现快速读取文件

    在工作中经常会有接收文件并且读取落库的需求,读取方式都是串行读取,所以本文主要为大家介绍一下如何利用多线程和分块实现快速读取文件,希望对大家有所帮助
    2023-09-09
  • Java变量与运算符用法详解

    Java变量与运算符用法详解

    这篇文章介绍了Java中的关键字、标识符、变量、基本数据类型、变量作用域、基本数据类型间的运算、字符串、基本数据类型与字符串的运算、计算机底层数据存储、运算符的使用、赋值运算符、比较运算符、逻辑运算符、位运算符和条件运算符用法,感兴趣的朋友跟随小编看看吧
    2026-01-01
  • 浅析Spring和MyBatis整合及逆向工程

    浅析Spring和MyBatis整合及逆向工程

    这篇文章主要介绍了Spring和MyBatis整合及逆向工程的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-06-06
  • SpringMVC转发与重定向参数传递的实现详解

    SpringMVC转发与重定向参数传递的实现详解

    这篇文章主要介绍了SpringMVC转发与重定向参数传递,对于重定向,可以通过FlashMap或RedirectAttributes来在请求间传递数据,因为重定向涉及两个独立的HTTP请求,而转发则在同一请求内进行,数据可以直接通过HttpServletRequest共享,需要的朋友可以参考下
    2022-07-07
  • springboot 整合hbase的示例代码

    springboot 整合hbase的示例代码

    这篇文章主要介绍了springboot 整合hbase的示例代码,本篇详细总结了hbase的Java客户端的使用,在实际开发过程中,还需要结合自身的情况做更加细致的整合与优化,需要的朋友可以参考下
    2022-04-04
  • SpringBoot3配置文件的使用技巧分享

    SpringBoot3配置文件的使用技巧分享

    SpringBoot 的核心优势之一便是约定大于配置,无需繁琐的 XML 配置,仅通过简单的配置文件就能完成项目的个性化定制,本文将从配置文件的核心类型、基础使用、高级特性、最佳实践四个维度,全方位拆解 SpringBoot3 配置文件的使用技巧,需要的朋友可以参考下
    2026-02-02
  • java数组输出的实例代码

    java数组输出的实例代码

    这篇文章主要介绍了java数组输出的实例代码,有需要的朋友可以参考一下
    2013-12-12
  • SpringBoot实现企业级敏感词拦截检查系统的设计方案

    SpringBoot实现企业级敏感词拦截检查系统的设计方案

    本文介绍了基于SpringBoot和DFA算法的敏感词检测系统设计,涵盖业务背景、技术架构、核心模块、数据库设计、使用示例、监控与告警、安全设计、部署方案等多个方面,旨在实现高效的实时敏感词检测,保证内容合规性,提升平台稳定性,需要的朋友可以参考下
    2026-01-01
  • java计算π的多种方法

    java计算π的多种方法

    这篇文章主要介绍了使用java计算π的多种方法,代码详细,逻辑清晰,对于算法思路可能有所帮助,需要的朋友可以参考下
    2021-04-04
  • Java源码解析之HashMap的put、resize方法详解

    Java源码解析之HashMap的put、resize方法详解

    这篇文章主要介绍了Java源码解析之HashMap的put、resize方法详解,文中有非常详细的代码示例,对正在学习java的小伙伴们有很大的帮助,需要的朋友可以参考下
    2021-04-04

最新评论