【后端新手谈 04】Spring 依赖注入所有方式 + 构造器注入成官方推荐的原因

一、先说新手最容易混淆的核心点:

private UserMapper userMapper; 只是声明了一个变量,并没有创建 / 初始化这个对象 ------ 变量和对象的区别,正是理解依赖注入的关键。

一、先看反例:只写 private UserMapper userMapper; 会发生什么?

复制代码
@Service
public class UserService {
    // 只声明变量,不做任何注入
    private UserMapper userMapper;

    public User getUserById(Long id) {
        // 调用 userMapper 的方法
        return userMapper.selectById(id);
    }
}

当你运行这段代码,调用 getUserById 时,会直接抛出 NullPointerException(空指针异常)。

核心原因:

private UserMapper userMapper; 只是告诉 JVM:"我需要一个叫 userMapper 的变量,类型是 UserMapper",但 JVM 只会给这个变量分配一个 "空引用"(值为 null),并没有创建 UserMapper 的实例对象

就像:

  • 你说 "我需要一辆车"(声明变量 private Car car;);
  • 但你手里并没有真的车(变量值为 null);
  • 你试图 "开车"(调用 car.drive()),自然会失败。

二、为什么必须 "注入"?注入的本质是什么?

UserMapper 不是普通的类 ------ 它是 MyBatis 生成的代理类,需要 Spring 容器创建、初始化(比如绑定 SQL 会话、数据源),你自己无法通过 new UserMapper() 创建可用的实例 (因为 UserMapper 是接口,不是具体类)。

注入的本质:

把 Spring 容器中已经创建好的、可用的 UserMapper 对象,赋值给 UserService 中的 userMapper 变量 ,让这个变量从 null 变成一个真实的、能干活的对象。

简单来说:声明变量 = 画饼,注入 = 把饼拿到手,只画饼是吃不饱的


在 Spring Boot 项目中,依赖注入(DI)是控制反转(IoC)的核心实现方式,而构造器注入是 Spring 官方明确推荐的最佳实践。下面从注入方式对比、官方推荐原因、实际应用场景三个维度,用新手能理解的方式讲透。

二、Spring 中 3 种核心依赖注入方式

先明确:依赖注入的本质是让 Spring 容器帮我们创建对象时,自动填充对象的依赖属性 ,而非手动 new 依赖对象。

1. 构造器注入(Constructor Injection)

通过类的构造方法注入依赖,结合 @Autowired(Spring 4.3+ 后,若类只有一个构造器,可省略 @Autowired):

复制代码
@Service
public class UserService {
    // 依赖的组件
    private final UserMapper userMapper;
    private final RedisTemplate<String, Object> redisTemplate;

    // 构造器注入(Spring 自动填充依赖)
    @Autowired // 单构造器时可省略
    public UserService(UserMapper userMapper, RedisTemplate<String, Object> redisTemplate) {
        this.userMapper = userMapper;
        this.redisTemplate = redisTemplate;
    }

    // 业务方法
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
}

@Service
public class UserService {
    // 依赖的组件
    private final UserMapper userMapper;

    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    // 业务方法
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
}

2. Setter 注入(Setter Injection)

通过 setter 方法注入依赖,必须加 @Autowired:

复制代码
@Service
public class UserService {
    // 依赖的组件(非final)
    private UserMapper userMapper;
    private RedisTemplate<String, Object> redisTemplate;

    // Setter注入
    @Autowired
    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    @Autowired
    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}

3. 字段注入(Field Injection)

直接在字段上加 @Autowired,最简洁但问题最多:

复制代码
@Service
public class UserService {
    // 直接在字段上注入(无需构造器/setter)
    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
}

仨、Spring 官方推荐构造器注入的核心原因

Spring 官方文档(Core Technologies)明确指出:构造器注入是强制依赖的最佳选择 ,核心原因可总结为 6 点,从基础到进阶逐步拆解:

1. 保证依赖不可变(Immutable),符合 "不可变优先" 设计原则

  • 构造器注入的依赖可声明为 final 关键字(如上例中 private final UserMapper userMapper),一旦赋值就无法修改;
  • Setter / 字段注入的依赖不能用 final,运行时可能被意外篡改(如反射、手动调用 setter),存在线程安全风险;
  • 不可变对象是线程安全的,且语义更清晰:"这个依赖是服务类运行的必要条件,创建时必须指定,且不能变更"。

2. 保证依赖非空(NonNull),避免空指针异常

  • 构造器注入时,Spring 必须在创建对象时就填充所有依赖,若依赖不存在(如 Bean 未定义),会直接抛出 NoSuchBeanDefinitionException,启动阶段就暴露问题
  • Setter / 字段注入:对象创建和依赖注入是两步操作,若依赖缺失,对象创建成功但依赖为 null,运行时调用依赖方法才会抛出空指针,问题暴露延迟(可能上线后才发现)。

示例对比:

  • 构造器注入:启动时就报错 No qualifying bean of type 'UserMapper' available,提前发现问题;
  • 字段注入:启动正常,调用 getUserById 时才抛 NullPointerException,排查成本高。

3. 代码可测试性更强(脱离 Spring 容器)

单元测试时,构造器注入无需 Spring 容器,直接手动 new 即可注入 Mock 依赖:

复制代码
// 单元测试(无需Spring上下文)
@Test
public void testGetUserById() {
    // 模拟依赖
    UserMapper mockMapper = Mockito.mock(UserMapper.class);
    RedisTemplate mockRedis = Mockito.mock(RedisTemplate.class);
    
    // 手动构造对象(构造器注入的优势)
    UserService userService = new UserService(mockMapper, mockRedis);
    
    // 测试逻辑
    User user = userService.getUserById(1L);
    Assert.assertNotNull(user);
}

而字段注入必须依赖 Spring Test 上下文(@SpringBootTest)或反射才能注入 Mock,测试代码更复杂、运行更慢:

复制代码
// 字段注入的测试(依赖Spring)
@SpringBootTest
public class UserServiceTest {
    @Autowired
    private UserService userService;

    @MockBean // 需Spring容器支持
    private UserMapper userMapper;

    @Test
    public void testGetUserById() {
        // 测试逻辑
    }
}

4. 避免循环依赖问题(提前暴露)

Spring 能解决 Setter / 字段注入的循环依赖,但构造器注入会直接暴露循环依赖问题 ,强制你优化代码设计:

  • 循环依赖示例:A 的构造器依赖 B,B 的构造器依赖 A → Spring 启动时直接报错 Circular reference involving bean 'a';
  • Setter / 字段注入:Spring 会通过 "三级缓存" 暂时存放未完全初始化的对象,表面上解决了循环依赖,但本质是掩盖了代码设计缺陷(两个类耦合过紧)。

官方认为:循环依赖是代码设计问题,应修复而非 "绕过",构造器注入能倒逼你拆分职责、解耦代码。

5. 符合 "依赖注入" 的本质(显式声明依赖)

依赖注入的核心是 "明确声明组件的依赖",构造器注入让依赖关系可视化、显式化

  • 看构造器就能清楚知道:这个服务类需要哪些依赖才能运行,语义清晰;
  • 字段注入的依赖隐藏在代码中,不看字段定义无法知道依赖关系(尤其类代码量大时)。

6. 兼容更多 Spring 特性,无兼容性问题

  • 字段注入依赖 Spring 的 AutowiredAnnotationBeanPostProcessor 后置处理器,若自定义 BeanPostProcessor 顺序错误,可能导致注入失败;
  • 构造器注入是 Spring 最基础的注入方式,兼容性最好,不受后置处理器、代理模式(如 CGLIB/JDK 动态代理)的影响。

肆、不同注入方式的适用场景

注入方式 核心优势 适用场景 官方态度
构造器注入 不可变、非空、易测试 强制依赖(组件运行的必要条件) 强烈推荐
Setter 注入 可选依赖、运行时可修改 可选依赖(如配置类的可选属性) 推荐(仅可选依赖)
字段注入 代码简洁 无(仅临时测试 / 非生产代码) 不推荐

最佳实践

  1. 强制依赖(如 Service 依赖 Mapper、Repository)→ 构造器注入;
  2. 可选依赖(如 Service 依赖一个可选的缓存组件)→ Setter 注入(加 @Autowired(required = false));
  3. 彻底放弃字段注入(即使代码简洁,也不要用在生产代码中)。

五、总结

Spring 官方推荐构造器注入的核心原因可归纳为 3 个关键点:

  1. 可靠性 :final 保证不可变,构造时填充依赖保证非空,启动阶段暴露问题;
  2. 可测试性 :脱离 Spring 容器即可手动构造,单元测试更高效;
  3. 设计合理性 :显式声明依赖、暴露循环依赖问题,倒逼代码解耦。

补充:Spring 4.3+ 后对构造器注入做了优化 ------ 若类只有一个构造器,可省略 @Autowired,进一步简化代码,这也体现了官方对构造器注入的主推态度。

相关推荐
英英_1 小时前
MATLAB MapReduce 从入门到实战:大数据处理完整教程
开发语言·matlab·mapreduce
Anastasiozzzz1 小时前
深度解析 Java 单例模式
java·开发语言
NGC_66111 小时前
G1收集器
java·开发语言·jvm
森林里的程序猿猿2 小时前
垃圾收集器ParNew&CMS与底层标记三色标记算法
java·jvm·算法
进击的小头2 小时前
第12篇:开环系统伯德图设计控制器
python·算法
t_hj2 小时前
腾讯QClaw深度试用:一句话创建专业级网络爬虫
开发语言·python
老毛肚2 小时前
八股框架篇
java·开发语言
大黄说说2 小时前
Rust 入门到实战:构建安全、高性能的下一代系统
开发语言·安全·rust
毅炼2 小时前
Spring 总结(1)
java·开发语言·spring