
一、先说新手最容易混淆的核心点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通过类的构造方法注入依赖结合AutowiredSpring 4.3 后若类只有一个构造器可省略AutowiredService public class UserService { // 依赖的组件 private final UserMapper userMapper; private final RedisTemplateString, Object redisTemplate; // 构造器注入Spring 自动填充依赖 Autowired // 单构造器时可省略 public UserService(UserMapper userMapper, RedisTemplateString, 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方法注入依赖必须加AutowiredService public class UserService { // 依赖的组件非final private UserMapper userMapper; private RedisTemplateString, Object redisTemplate; // Setter注入 Autowired public void setUserMapper(UserMapper userMapper) { this.userMapper userMapper; } Autowired public void setRedisTemplate(RedisTemplateString, Object redisTemplate) { this.redisTemplate redisTemplate; } }3. 字段注入Field Injection直接在字段上加Autowired最简洁但问题最多Service public class UserService { // 直接在字段上注入无需构造器/setter Autowired private UserMapper userMapper; Autowired private RedisTemplateString, 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 的构造器依赖 BB 的构造器依赖 A → Spring 启动时直接报错Circular reference involving bean aSetter / 字段注入Spring 会通过 “三级缓存” 暂时存放未完全初始化的对象表面上解决了循环依赖但本质是掩盖了代码设计缺陷两个类耦合过紧。官方认为循环依赖是代码设计问题应修复而非 “绕过”构造器注入能倒逼你拆分职责、解耦代码。5. 符合 “依赖注入” 的本质显式声明依赖依赖注入的核心是 “明确声明组件的依赖”构造器注入让依赖关系可视化、显式化看构造器就能清楚知道这个服务类需要哪些依赖才能运行语义清晰字段注入的依赖隐藏在代码中不看字段定义无法知道依赖关系尤其类代码量大时。6. 兼容更多 Spring 特性无兼容性问题字段注入依赖 Spring 的AutowiredAnnotationBeanPostProcessor后置处理器若自定义 BeanPostProcessor 顺序错误可能导致注入失败构造器注入是 Spring 最基础的注入方式兼容性最好不受后置处理器、代理模式如 CGLIB/JDK 动态代理的影响。肆、不同注入方式的适用场景注入方式核心优势适用场景官方态度构造器注入不可变、非空、易测试强制依赖组件运行的必要条件强烈推荐Setter 注入可选依赖、运行时可修改可选依赖如配置类的可选属性推荐仅可选依赖字段注入代码简洁无仅临时测试 / 非生产代码不推荐最佳实践强制依赖如 Service 依赖 Mapper、Repository→ 构造器注入可选依赖如 Service 依赖一个可选的缓存组件→ Setter 注入加Autowired(required false)彻底放弃字段注入即使代码简洁也不要用在生产代码中。五、总结Spring 官方推荐构造器注入的核心原因可归纳为 3 个关键点可靠性final保证不可变构造时填充依赖保证非空启动阶段暴露问题可测试性脱离 Spring 容器即可手动构造单元测试更高效设计合理性显式声明依赖、暴露循环依赖问题倒逼代码解耦。补充Spring 4.3 后对构造器注入做了优化 —— 若类只有一个构造器可省略Autowired进一步简化代码这也体现了官方对构造器注入的主推态度。