IoC 与 DI 核心概念
本章定位:深入理解 Spring 容器的两大基石——IoC(控制反转)与 DI(依赖注入)。掌握 BeanFactory 与 ApplicationContext 的区别,以及三种注入方式的优劣与适用场景。
IoC:控制反转
IoC(Inversion of Control,控制反转) 是一种设计原则,指将对象的创建和依赖关系的管理从程序内部转移到外部容器。
在传统编程中,对象自己负责创建它所依赖的其他对象:
// 传统方式:对象自己控制依赖
public class StudentService {
private StudentDao studentDao = new StudentDao(); // 自己 new
}
在 IoC 模式下,对象不再自己创建依赖,而是由外部容器负责创建并注入:
// IoC 方式:容器控制依赖
public class StudentService {
private final StudentDao studentDao;
public StudentService(StudentDao studentDao) { // 容器传入
this.studentDao = studentDao;
}
}
关键转变:控制权从"对象自身"反转给了"外部容器"。
DI:依赖注入
DI(Dependency Injection,依赖注入) 是 IoC 的具体实现方式。指容器在创建对象时,自动将其依赖的其他对象注入进来。
Spring IoC 容器的两个核心接口:
| 接口 | 定位 | 特点 |
|---|---|---|
| BeanFactory | 基础 IoC 容器 | 延迟初始化(lazy-init),资源占用少,功能精简 |
| ApplicationContext | 高级 IoC 容器 | 继承自 BeanFactory,提供 AOP 集成、消息资源、事件传播、应用层上下文 |
实际开发中几乎总是使用 ApplicationContext,它是 Spring 推荐的入口点。
BeanFactory vs ApplicationContext
BeanFactory:最简容器
// 使用 BeanFactory 手动加载 XML 配置
BeanFactory factory = new XmlBeanFactory(
new ClassPathResource("application-context.xml")
);
StudentService service = factory.getBean(StudentService.class);
BeanFactory 采用延迟初始化策略:只有在调用 getBean() 时,才会实例化对应的 Bean。这种方式资源占用少,但功能有限。
ApplicationContext:企业级容器
// 使用 ApplicationContext 加载注解配置
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
StudentService service = context.getBean(StudentService.class);
ApplicationContext 在容器启动时就会预实例化所有单例 Bean(除非标记 @Lazy),并提供以下扩展能力:
- 国际化支持:
MessageSource接口 - 事件传播:
ApplicationEvent发布/监听机制 - 资源加载:统一访问 URL、文件、classpath 资源
- 自动后处理:自动检测并注册
BeanPostProcessor和BeanFactoryPostProcessor
三种依赖注入方式
Spring 支持三种注入方式:
| 注入方式 | 实现机制 | 优点 | 缺点 |
|---|---|---|---|
| 构造器注入(Constructor Injection) | 通过构造方法参数注入 | 依赖不可变、必填依赖保证完整、利于单元测试 | 参数过多时构造方法臃肿 |
| Setter 注入(Setter Injection) | 通过 setter 方法注入 | 可选依赖灵活、可在创建后重新注入 | 对象可能处于不完整状态 |
| 字段注入(Field Injection) | @Autowired 直接标注字段 | 代码简洁 | 无法声明 final、不利于测试、隐藏依赖关系 |
Spring 官方推荐构造器注入。从 Spring 4.x 开始,如果类只有一个构造方法,可以省略
@Autowired注解。
完整示例:飞翔科技公司的学生管理系统
场景简述
飞翔科技技术部后端开发小崔正在实现"学生管理系统"的核心服务层。架构师白歌要求团队统一使用构造器注入,并对比三种注入方式的差异。
操作前:无 IoC 的紧耦合代码
// 操作前:StudentService.java —— 手动创建依赖,无法替换实现
public class StudentService {
private StudentDao studentDao = new StudentDao(); // 死耦合
private EmailSender emailSender = new EmailSender(); // 死耦合
private SmsSender smsSender = new SmsSender(); // 死耦合
public void enrollStudent(Student student) {
studentDao.save(student);
emailSender.send("admin@learnto.cn", "新学生:" + student.getName());
smsSender.send(student.getPhone(), "入学成功");
}
}
问题:
- 单元测试时无法 mock
StudentDao,必须连接真实数据库 - 如果想把
EmailSender换成WechatSender,必须修改StudentService源码 - 如果
SmsSender初始化很慢(需要连接短信网关),StudentService的创建也会被拖慢
操作后:三种注入方式对比
方式一:构造器注入(推荐)
@Service
public class StudentService {
private final StudentDao studentDao;
private final EmailSender emailSender;
private final SmsSender smsSender;
// Spring 4.3+ 单构造器可省略 @Autowired
public StudentService(StudentDao studentDao,
EmailSender emailSender,
SmsSender smsSender) {
this.studentDao = studentDao;
this.emailSender = emailSender;
this.smsSender = smsSender;
}
@Transactional
public void enrollStudent(Student student) {
studentDao.save(student);
emailSender.send("admin@learnto.cn", "新学生:" + student.getName());
smsSender.send(student.getPhone(), "入学成功");
}
}
构造器注入的优势:
final字段保证依赖不可变- 对象创建时所有依赖必须齐全,不会出现"半初始化"状态
- 单元测试可以直接
new StudentService(mockDao, mockEmail, mockSms)
方式二:Setter 注入
@Service
public class StudentService {
private StudentDao studentDao;
private EmailSender emailSender;
private SmsSender smsSender;
@Autowired
public void setStudentDao(StudentDao studentDao) {
this.studentDao = studentDao;
}
@Autowired(required = false) // 可选依赖
public void setEmailSender(EmailSender emailSender) {
this.emailSender = emailSender;
}
@Autowired(required = false) // 可选依赖
public void setSmsSender(SmsSender smsSender) {
this.smsSender = smsSender;
}
}
Setter 注入的适用场景:
- 依赖是可选的(如
emailSender可能未配置) - 需要在对象创建后动态替换依赖
- 循环依赖场景下(虽然应优先重构避免循环依赖)
方式三:字段注入(不推荐)
@Service
public class StudentService {
@Autowired
private StudentDao studentDao; // 无法声明 final
@Autowired
private EmailSender emailSender;
@Autowired
private SmsSender smsSender;
}
字段注入的问题:
- 无法声明
final,依赖可能被意外修改 - 单元测试需要依赖 Spring 容器或反射工具(如
ReflectionTestUtils) - 隐藏了类的真实依赖关系,阅读代码时无法从构造器签名看出依赖
容器启动与运行结果
// 启动容器
@Configuration
@ComponentScan("com.feixiang.student")
public class AppConfig {
}
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
StudentService service = ctx.getBean(StudentService.class);
Student student = new Student("小崔", 22, "计算机科学与技术");
service.enrollStudent(student);
}
}
运行输出:
[main] INFO o.s.c.a.AnnotationConfigApplicationContext -
Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@6d06d69c
[main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -
Creating shared instance of singleton bean 'studentService'
[main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -
Autowiring by type from bean name 'studentService' via constructor to
bean named 'studentDao'
[main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -
Autowiring by type from bean name 'studentService' via constructor to
bean named 'emailSender'
[main] DEBUG o.s.b.f.s.DefaultListableBeanFactory -
Autowiring by type from bean name 'studentService' via constructor to
bean named 'smsSender'
[main] INFO c.f.s.service.StudentService - 学生小崔入学成功,已发送邮件和短信通知
核心原理:容器启动与依赖注入流程
关键阶段说明:
- 加载配置类:读取
@Configuration类,解析其中的@Bean方法 - 组件扫描:
@ComponentScan扫描指定包路径,将@Component、@Service、@Repository等类注册为BeanDefinition - 注册 BeanDefinition:所有 Bean 的元数据(类名、作用域、依赖等)被存储到
BeanFactory的beanDefinitionMap中 - BeanFactoryPostProcessor:在 Bean 实例化前,允许修改 BeanDefinition(如
@PropertySource加载的属性值替换) - 实例化:调用构造方法创建 Bean 对象
- 属性填充:根据
BeanDefinition中的依赖信息,通过构造器、Setter 或字段注入依赖 - Aware 回调:如果 Bean 实现了
BeanNameAware、ApplicationContextAware等接口,注入相应资源 - BeanPostProcessor:在初始化前后提供扩展点(如 AOP 代理在此阶段创建)
- 初始化:执行
@PostConstruct或InitializingBean.afterPropertiesSet() - 就绪:单例 Bean 放入
singletonObjects缓存,后续getBean()直接返回
易错场景与面试考点
反例一:字段注入导致单元测试困难
小崔最初使用字段注入,写单元测试时陷入困境:
// ❌ 错误:字段注入的类,测试时需要反射或 Spring 容器
public class StudentServiceTest {
@Test
public void testEnrollStudent() {
StudentService service = new StudentService(); // 编译通过,但运行时空指针!
// studentDao / emailSender / smsSender 都是 null
service.enrollStudent(new Student("测试", 20, "测试班"));
}
}
纠正:使用构造器注入后,测试变得简单直接:
// ✅ 正确:构造器注入的类,测试时直接传入 mock 对象
public class StudentServiceTest {
@Test
public void testEnrollStudent() {
StudentDao mockDao = Mockito.mock(StudentDao.class);
EmailSender mockEmail = Mockito.mock(EmailSender.class);
SmsSender mockSms = Mockito.mock(SmsSender.class);
StudentService service = new StudentService(mockDao, mockEmail, mockSms);
service.enrollStudent(new Student("测试", 20, "测试班"));
Mockito.verify(mockDao).save(Mockito.any(Student.class));
Mockito.verify(mockEmail).send(Mockito.anyString(), Mockito.anyString());
}
}
反例二:循环依赖的构造器注入陷阱
小崔在开发"班级-学生"双向关联时,不小心造成了循环依赖:
// ❌ 错误:构造器注入的循环依赖,容器启动失败
@Service
public class ClassService {
private final StudentService studentService;
public ClassService(StudentService studentService) { // 循环依赖
this.studentService = studentService;
}
}
@Service
public class StudentService {
private final ClassService classService;
public StudentService(ClassService classService) { // 循环依赖
this.classService = classService;
}
}
启动报错:
BeanCurrentlyInCreationException: Error creating bean with name 'classService':
Requested bean is currently in creation: Is there an unresolvable circular reference?
原理分析:
Spring 解决循环依赖依赖三级缓存机制,但仅对 singleton 作用域且通过构造器注入以外方式创建的 Bean 有效。构造器注入的循环依赖无法自动解决,因为构造器调用时必须传入完整的依赖对象,而此时依赖对象自身也在创建中。
解决方案:
- 重构代码:消除双向依赖,通过 ID 查询替代直接引用
- Setter 注入:将其中一个改为 Setter 注入,允许 Spring 通过三级缓存解决
@Lazy延迟注入:在构造器参数上加@Lazy,注入的是代理对象而非真实对象
// ✅ 方案三:@Lazy 延迟注入
@Service
public class ClassService {
private final StudentService studentService;
public ClassService(@Lazy StudentService studentService) {
this.studentService = studentService;
}
}
面试高频题
Q1:IoC 和 DI 有什么区别?
IoC(控制反转)是一种设计原则,指将对象控制权从程序内部转移到外部容器;DI(依赖注入)是 IoC 的具体实现方式之一,通过构造器、Setter 或字段将依赖注入对象。DI 是实现 IoC 的最常用手段,但 IoC 还可以通过依赖查找(Dependency Lookup)等其他方式实现。
Q2:BeanFactory 和 ApplicationContext 有什么区别?
BeanFactory 是基础容器,延迟初始化,资源占用少;ApplicationContext 继承自 BeanFactory,提供 AOP 集成、国际化、事件传播、应用层特定上下文(如 WebApplicationContext),默认立即初始化单例 Bean。实际开发中几乎总是使用 ApplicationContext。
Q3:Spring 官方为什么推荐构造器注入?
① 依赖不可变,可声明
final;② 对象创建时依赖必须齐全,避免"半初始化"状态;③ 不依赖 Spring 容器即可进行单元测试;④ 依赖关系显式暴露在构造器签名中,代码可读性高。
Q4:字段注入有哪些隐患?
① 无法声明
final,依赖可能被修改;② 隐藏依赖关系,类的真实依赖不从 API 层面可见;③ 单元测试需要依赖 Spring 容器或反射;④ 无法在创建后重新注入(无 setter);⑤ 与 Java 的不可变对象设计理念相悖。