依赖注入详解:Spring IoC 的实现方式

依赖注入详解:Spring IoC 的实现方式

一、什么是依赖注入?

依赖注入(Dependency Injection,DI)是 Spring 实现控制反转(IoC)的具体方式。它的核心思想是:对象不再自己创建依赖,而是由外部容器在运行时将依赖注入到对象中。

java 复制代码
// 没有依赖注入
public class UserService {
    private UserDao userDao = new UserDaoImpl();  // 自己创建依赖
}

// 使用依赖注入
@Service
public class UserService {
    @Autowired
    private UserDao userDao;  // 由容器注入
}

依赖注入把"如何获取依赖"这件事从代码中移除了。UserService 不需要知道 UserDao 的具体实现是什么,也不需要知道它怎么创建。它只声明"我需要一个 UserDao",剩下的由容器负责。

二、三种注入方式

2.1 构造器注入

依赖通过构造方法参数传入。

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;
    }
}

Spring 在创建 UserService 时,会从容器中找到 UserDaoEmailService 类型的 Bean,作为参数调用构造方法。

如果类只有一个构造方法,Spring 4.3 之后可以省略 @Autowired

java 复制代码
@Service
public class UserService {
    private final UserDao userDao;
    
    public UserService(UserDao userDao) {
        this.userDao = userDao;
    }
}

如果类有多个构造方法,需要显式标注 @Autowired 指定使用哪一个。

优点:

  • 依赖不可变(final 修饰),对象创建后依赖不可更改
  • 依赖在对象创建时就完全就绪,避免空指针
  • 便于单元测试,直接传入 Mock 对象即可
  • 能明确暴露类的依赖关系,依赖过多时能及时发现设计问题

缺点:

  • 依赖较多时构造方法参数列表很长
  • 构造器注入无法解决循环依赖问题

2.2 Setter 注入

依赖通过 Setter 方法注入。

java 复制代码
@Service
public class UserService {
    private UserDao userDao;
    
    @Autowired
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

Spring 先通过无参构造方法创建对象,再调用 Setter 方法注入依赖。

优点:

  • 可以在对象创建后动态替换依赖
  • 依赖是可选时比较灵活

缺点:

  • 依赖不是不可变的,可能被意外修改
  • 对象创建后依赖可能仍为 null,存在空指针风险

2.3 字段注入

依赖直接注入到字段上。

java 复制代码
@Service
public class UserService {
    @Autowired
    private UserDao userDao;
    
    @Autowired
    private EmailService emailService;
}

Spring 通过反射直接将依赖赋值给字段,不需要 Setter 方法。

优点:

  • 代码最简洁,写起来最方便
  • 适合快速开发

缺点:

  • 无法使用 final 修饰字段
  • 单元测试时无法直接通过构造方法注入 Mock 对象,需要借助 ReflectionTestUtils 或 Spring 测试上下文
  • 依赖关系不够清晰,容易注入过多依赖
  • 可能隐藏设计问题(类承担了过多职责)

三、三种方式对比

对比维度 构造器注入 Setter 注入 字段注入
依赖可变性 不可变 可变 可变
空指针风险
单元测试便利性
循环依赖支持 不支持 支持 支持
依赖过多时提示 构造方法参数过长,容易发现 不明显 不明显
Spring 官方推荐 ✅ 推荐 ⚠️ 可选 ❌ 不推荐

四、底层原理

依赖注入的核心处理类是 AutowiredAnnotationBeanPostProcessor,它实现了 BeanPostProcessor 接口。在 Bean 实例化并填充属性阶段,该处理器会扫描所有标注了 @Autowired@Value@Inject 的字段和方法,解析依赖并注入。

简化后的流程:

scss 复制代码
1. Spring 实例化 Bean(通过构造方法反射创建对象)
2. 调用 populateBean() 填充属性
3. AutowiredAnnotationBeanPostProcessor.postProcessProperties()
   ├── 扫描字段/方法上的 @Autowired
   ├── 解析依赖类型(DefaultListableBeanFactory.resolveDependency)
   │   ├── 按类型查找 Bean
   │   ├── 找到多个时按名称匹配
   │   ├── 有 @Primary 时优先
   │   └── 有 @Qualifier 时按名称
   └── 通过反射设置字段值或调用 Setter 方法

对于构造器注入,处理时机更早------在实例化阶段,ConstructorResolver 会解析构造方法的参数类型,从容器中查找匹配的 Bean,再调用构造方法。

五、@Autowired 与 @Resource 的区别

对比 @Autowired @Resource
来源 Spring 注解 Java 标准注解(JSR-250)
匹配方式 先按类型,再按名称 先按名称,再按类型
是否支持 required 支持 required = false 不支持
是否支持 @Primary 支持 不支持
应用位置 字段、构造器、方法、参数 字段、方法
java 复制代码
// @Autowired 按类型
@Autowired
private UserDao userDao;

// @Resource 按名称
@Resource(name = "userDaoImpl")
private UserDao userDao;

六、循环依赖问题

6.1 什么是循环依赖?

两个 Bean 互相依赖对方:

java 复制代码
@Service
public class AService {
    @Autowired
    private BService bService;
}

@Service
public class BService {
    @Autowired
    private AService aService;
}

6.2 构造器注入的循环依赖

java 复制代码
@Service
public class AService {
    private final BService bService;
    public AService(BService bService) { this.bService = bService; }
}

@Service
public class BService {
    private final AService aService;
    public BService(AService aService) { this.aService = aService; }
}

这种情况会直接抛出 BeanCurrentlyInCreationException。因为创建 A 需要 B,创建 B 需要 A,两个构造方法互相等待,无法完成。

6.3 字段注入的循环依赖

字段注入(或 Setter 注入)可以解决循环依赖。Spring 通过三级缓存提前暴露对象的早期引用。

css 复制代码
创建 AService:
  ① 实例化 A(此时 bService 为 null)
  ② 将 A 的早期引用放入三级缓存 singletonFactories
  ③ 填充属性,发现需要 BService
     创建 BService:
        ① 实例化 B(aService 为 null)
        ② 将 B 的早期引用放入三级缓存
        ③ 填充属性,发现需要 AService
           从三级缓存拿到 A 的早期引用,注入
        ④ B 创建完成,放入一级缓存
  ④ 将 B 注入 A
  ⑤ A 创建完成

三级缓存分别是:

  • singletonObjects(一级缓存):完整的单例 Bean
  • earlySingletonObjects(二级缓存):提前暴露的 Bean,尚未完成属性注入
  • singletonFactories(三级缓存):Bean 工厂,用于生成早期引用

Spring 通过 ObjectFactory 和 AOP 代理的延迟创建,解决了循环依赖。但字段注入的循环依赖虽然能解决,本质上是一种"设计上不健康"的表现。更好的做法是重构代码,消除循环依赖。

七、常见问题

注入失败:NoSuchBeanDefinitionException

找不到匹配的 Bean。检查是否加了 @Component@Service 等注解,是否在扫描范围内,类型是否匹配。

注入失败:NoUniqueBeanDefinitionException

找到多个同类型 Bean。使用 @Qualifier("beanName") 指定具体名称,或使用 @Primary 标记首选 Bean。

注入失败:字段为 null

检查类是否被 Spring 管理。如果 new 出来的对象,依赖注入不会生效,因为对象不受容器管理。

静态字段无法注入

@Autowired 不能直接注入静态字段。可以在 Setter 方法上标注 @Autowired,赋值给静态变量:

java 复制代码
@Component
public class SpringContextHolder {
    private static ApplicationContext context;
    
    @Autowired
    public void setContext(ApplicationContext context) {
        SpringContextHolder.context = context;
    }
}

八、最佳实践

优先使用构造器注入。 Spring 官方推荐构造器注入,它保证了依赖的不可变性和完整性。依赖过多时,构造方法参数过长本身就是设计需要拆分的一个信号。

避免字段注入。 字段注入虽然写起来方便,但不利于测试,也容易积累过多依赖而不自知。

使用 @Qualifier 处理多实现。 当一个接口有多个实现时,用 @Qualifier 明确指定名称,避免歧义。

避免循环依赖。 循环依赖通常意味着两个类的职责边界不清晰。通过提取公共服务、调整职责划分来消除循环依赖,而不是依赖三级缓存来"兜底"。

final 修饰构造器注入的字段。 保证依赖不可变,也向调用方传达这个依赖是必须的。

依赖注入是 Spring 最核心的能力之一。它把对象从"自己管理依赖"中解放出来,让代码只关注业务逻辑本身。理解三种注入方式的区别和底层原理,是写出高质量 Spring 代码的基础。

相关推荐
平头哥AI2 小时前
Day 21 _ error 是个普通值_errors.New 与 fmt.Errorf 造出来,沿调用栈抛到 main 接住
后端·学习·golang·go
写后端的胖头鱼2 小时前
一文讲懂JVM与调优
jvm·后端·算法·架构·jvm调优
步行cgn2 小时前
Spring Boot 指定数据来源详解
spring boot·后端·python
一个有温度的技术博主2 小时前
深入理解 Spring Boot 自动装配
java·spring boot·后端
IT_陈寒2 小时前
我TM竟然被Java的空指针坑了第三次!
前端·人工智能·后端
小灰灰搞电子3 小时前
Rust suppaftp 库详解:基于 FTP 客户端实战指南
开发语言·后端·rust
Ramble_Naylor3 小时前
枚举与 match:一个值只能是几种情况之一
后端·rust
编码浪子3 小时前
Rust unsafe 与 FFI 互操作生产级实战:把危险关进笼子的四道闸门
开发语言·后端·rust
步行cgn4 小时前
Spring Boot 将配置绑定到第三方对象详解
spring boot·后端·python