IoC 与 DI 原理
候选人小张坐在字节跳动的面试间,面试官翻到简历上"熟练使用 Spring"这一行,开口问道:
"IoC 和 DI 是什么关系?"
小张说:"IoC 是控制反转,DI 是依赖注入,它们是两个不同的概念..."
面试官点点头:"那 Spring 是怎么实现 IoC 的?"
小张说:"通过 BeanFactory..."面试官打断:"BeanFactory 和 ApplicationContext 有什么区别?"
小张停顿了两秒,开始硬扛:"ApplicationContext 是 BeanFactory 的子接口..."
面试官追问:"那 BeanFactory 是怎么创建 Bean 的?"
小张彻底卡住了。
【面试官心理】
这道题我其实在试探他三层:第一层,IoC 和 DI 的概念能不能说清楚;第二层,Spring 底层是怎么通过反射实现控制反转的;第三层,他有没有看过源码、知道 Bean 是怎么被实例化出来的。知道结论的人很多,能解释为什么的才是真正理解的。
一、核心问题 🔴
1.1 问题拆解
IoC(Inversion of Control,控制反转)和 DI(Dependency Injection,依赖注入)是 Spring 最核心的概念,也是面试中绕不开的经典问题。面试官通常会用以下追问链来层层递进:
第一层:概念理解
- "IoC 是什么?为什么需要 IoC?"
- "DI 是什么?和 IoC 有什么区别?"
第二层:底层实现
- "Spring 是怎么实现 IoC 的?"
- "Bean 是怎么被创建出来的?反射还是别的方式?"
第三层:源码细节
- "BeanFactory 和 ApplicationContext 的区别是什么?"
- "ApplicationContext 和 BeanFactory 加载 Bean 的时机有什么区别?"
第四层:工程实践
- "你在项目中是怎么用 IoC 的?有什么最佳实践?"
- "IoC 容器初始化的时候都做了哪些事?"
1.2 ❌ 错误示范
候选人原话 A:"IoC 就是把对象交给 Spring 管理,DI 就是 Spring 给我们注入依赖。"
问题诊断:
- 把 Spring 的特性当成了概念本身,没有理解核心思想
- "交给 Spring 管理" 这种表述太笼统,等于没说
- 完全不理解控制权到底反转了什么
候选人原话 B:"IoC 和 DI 是一样的,Spring 通过 XML 配置或者注解来实现依赖注入。"
问题诊断:
- 把两个概念混为一谈
- 只停留在使用层面,不懂底层原理
- 说明没有看过任何源码
候选人原话 C:"IoC 是一种设计模式,用来解耦。"
问题诊断:
- 这个回答本身没错,但太抽象
- 面试官追问"具体怎么实现的",马上就露馅
- 说明只知道皮毛,不知道 Spring 源码是怎么做的
1.3 标准回答
P5 回答:基本概念
IoC(控制反转):
传统程序设计中,对象的创建和依赖关系是由程序本身在代码中主动控制的。比如 A 需要用到 B,就要在 A 内部 new B()。IoC 的核心思想是:把对象的创建和依赖关系的维护,从程序内部反转到外部容器中。容器负责创建对象、管理对象的生命周期、组装对象之间的依赖关系。
打个比方:传统方式是你自己去菜市场买菜、回家洗菜、切菜、炒菜。IoC 就像你去了一个餐厅,你只管点菜,后厨的一切流程都由餐厅(容器)来完成。
DI(依赖注入):
DI 是实现 IoC 的一种具体手段。指容器在创建对象时,自动把对象所需的依赖注入到该对象中,而不是由对象自己主动去获取依赖。
注入方式有三种:
- 构造器注入:通过构造函数注入
- Setter 注入:通过 setter 方法注入
- 字段注入:通过
@Autowired 或 @Resource 直接注入字段
// 构造器注入(推荐)
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
// Setter 注入
public class OrderService {
private OrderRepository orderRepository;
@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
💡
面试官问"IoC 和 DI 的关系",标准答案是:DI 是 IoC 的一种具体实现方式。IoC 是一种思想,DI 是这种思想的实现手段之一。另一个实现手段是 DL(Dependency Lookup,依赖查找),但 Spring 主要用的是 DI。
1.4 追问升级
追问 1:Spring 底层是怎么通过反射创建 Bean 的?
当我们说"容器创建 Bean",Spring 底层到底在做什么?核心流程如下:
// Spring 底层创建 Bean 的简化流程
public Object createBean(String beanName, BeanDefinition bd) {
// 1. 根据 BeanDefinition 中的信息,确认使用哪个构造函数
Constructor<?> constructorToUse = resolveConstructor(bd);
// 2. 通过反射调用构造函数,实例化对象
Object bean = constructorToUse.newInstance(constructorArgs);
// 3. 通过反射注入属性(autowire)
populateBean(bean, bd);
// 4. 初始化 Bean(调用 init-method、BeanPostProcessor 等)
initializeBean(bean, beanName);
return bean;
}
关键源码在 AbstractAutowireCapableBeanFactory#createBeanInstance():
// JDK 1.8 AbstractAutowireCapableBeanFactory.java
private BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) {
// 如果存在 Supplier,则使用 Supplier 创建
if (mbd.getSupplier() != null) {
return resolveBeanFromSupplier(mbd, beanName);
}
// 如果有工厂方法,则使用工厂方法创建
if (mbd.hasFactoryMethod()) {
return resolveFactoryMethod(mbd, args);
}
// 解析构造函数(用于构造器注入)
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(mbd, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == Constructor) {
// 使用构造器自动装配
return autowireConstructor(beanName, mbd, ctors, args);
}
// 使用默认构造函数
return instantiateBean(beanName, mbd);
}
然后是 instantiateBean 方法,它使用 CGLIB 或 JDK 反射来实例化:
// AbstractAutowireCapableBeanFactory.java
protected BeanWrapper instantiateBean(String beanName, RootBeanDefinition mbd) {
Object bean;
try {
// 获取类对象
Class<?> beanClass = resolveBeanClass(mbd, beanName);
// 如果是 CGLIB 代理类,则使用 CGLIB 创建
if (mbd.hasBeanClass() && mbd.getEnforceMethodOverride()) {
return instantiateWithMethodInjection(mbd, beanName);
}
// 普通的 Bean,使用 JDK 反射或 CGLIB 创建实例
bean = getInstantiationStrategy().instantiate(mbd, beanName, this);
BeanWrapper bw = new BeanWrapperImpl(bean);
initBeanWrapper(bw);
return bw;
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex);
}
}
默认策略是 CglidSubclassingInstantiationStrategy,使用 CGLIB 的 Enhancer 来创建子类实例。
【面试官心理】
我问他反射创建 Bean,其实不是想听他背代码。我是想看他有没有亲手看过 Spring 源码,能不能理解为什么需要 CGLIB、什么时候用 JDK 反射、什么时候用 CGLIB。知道"通过反射创建"只是基本操作,能说出具体哪个类、哪个方法、哪个策略的才是加分的。
追问 2:BeanFactory 和 ApplicationContext 有什么区别?
这是面试中的高频追问,80% 的候选人会回答"ApplicationContext 是 BeanFactory 的子接口",但进一步追问就崩了。
// BeanFactory 懒加载示例
BeanFactory factory = new XmlBeanFactory(new ClassPathResource("beans.xml"));
// 此时所有 Bean 都没有被创建
OrderService orderService = factory.getBean(OrderService.class); // 第一次调用时才创建
// ApplicationContext 预加载示例
ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
// 容器启动时,所有 Bean 都已经被创建好了
OrderService orderService = ctx.getBean(OrderService.class); // 直接使用
⚠️
这里有个大坑:ApplicationContext 的预加载机制在开发环境很方便,但在生产环境可能导致启动变慢。更重要的是,如果你的 Bean 里有循环依赖,BeanFactory 模式下可能只有在运行时才发现,而 ApplicationContext 会在启动时就暴露出来。所以有些团队会故意用 BeanFactory 来"提前发现问题"。
追问 3:ApplicationContext 启动时都做了什么?
这是 P6/P7 拉开差距的关键追问。完整流程非常长,但面试中能说清楚核心步骤就够了:
1. 容器初始化
└─ 创建 BeanFactory(DefaultListableBeanFactory)
2. BeanDefinition 注册
└─ 扫描 classpath → 解析 @Component/@Service/@Repository/@Controller
└─ 解析 XML 配置
└─ 解析 @Configuration 类中的 @Bean 方法
└─ 调用 BeanDefinitionRegistry.registerBeanDefinition() 注册到 BeanFactory
3. BeanPostProcessor 注册
└─ 注册 ConfigurationPropertiesBindingPostProcessor
└─ 注册 AutowiredAnnotationBeanPostProcessor
└─ 注册 CommonAnnotationBeanPostProcessor
4. Bean 实例化(预加载)
└─ 按照依赖顺序创建单例 Bean
└─ 执行 BeanPostProcessor.postProcessBeforeInitialization()
└─ 执行 init-method
└─ 执行 BeanPostProcessor.postProcessAfterInitialization()
5. 容器就绪
└─ 发布 ContextRefreshedEvent 事件
└─ 通知 ApplicationListener
核心源码在 AbstractApplicationContext#refresh():
// AbstractApplicationContext.java
public void refresh() throws BeansException {
synchronized (this.startupShutdownMonitor) {
// 1. 准备 BeanFactory
prepareBeanFactory(beanFactory);
// 2. 允许子类注册 BeanDefinition(扩展点)
postProcessBeanFactory(beanFactory);
// 3. 调用所有 BeanFactoryPostProcessor
invokeBeanFactoryPostProcessors(beanFactory);
// 4. 注册 BeanPostProcessor
registerBeanPostProcessors(beanFactory);
// 5. 初始化消息源(国际化)
initMessageSource();
// 6. 初始化事件广播器
initApplicationEventMulticaster();
// 7. onRefresh 钩子(留给子类实现,如 Web 项目启动)
onRefresh();
// 8. 注册 ApplicationListener
registerListeners();
// 9. 实例化所有剩余的单例 Bean
finishBeanFactoryInitialization(beanFactory);
// 10. 发布 ContextRefreshedEvent
finishRefresh();
}
}
这个 refresh() 方法是 Spring 容器初始化的总入口,面试中能把这个流程说清楚的,基本都是 P6+。
二、延伸问题 🟡
2.1 为什么需要 IoC?
传统方式的痛点:
// 不用 Spring 的写法
public class OrderService {
private OrderRepository orderRepository = new OrderRepository();
private PaymentService paymentService = new PaymentService();
private NotificationService notificationService = new NotificationService();
}
问题:
- 紧耦合:OrderService 直接依赖具体实现类,无法替换
- 测试困难:无法 mock 依赖,单元测试几乎不可能
- 复用性差:对象无法复用,每次 new 都是新实例
- 修改成本高:换一个实现类需要修改所有用到它的地方
IoC 带来的好处:
- 松耦合:对象不负责依赖的创建,只声明需要什么,由容器注入
- 可测试:可以注入 mock 对象,单元测试变得简单
- 可复用:同一个 Bean 可以被多个对象共享
- 可配置:依赖关系可以在配置文件中修改,不需要改代码
2.2 三种注入方式的对比
💡
阿里 Java 规范里明确禁止字段注入(@Autowired 直接打在字段上),推荐使用构造器注入。理由是:字段注入会让对象在构造时处于不完整状态,而且 IDE 无法提示哪些依赖是必须的。
2.3 @Autowired 和 @Resource 的区别
这是面试中经常被问到的问题:
public class OrderService {
// @Autowired byType,相同类型有多个 Bean 时需要 @Qualifier
@Autowired
@Qualifier("jdbcOrderRepository")
private OrderRepository orderRepository;
// @Resource byName,直接按字段名匹配 Bean
@Resource
private OrderRepository jdbcOrderRepository;
// @Autowired 构造器注入(推荐)
private final PaymentService paymentService;
@Autowired
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
三、生产避坑
3.1 循环依赖导致的启动失败
循环依赖是 IoC 容器中最常见的生产问题之一:
@Service
public class A {
@Autowired
private B b;
}
@Service
public class B {
@Autowired
private A a;
}
这种场景下 Spring 启动时会报错:BeanCurrentlyInCreationException。
解决方案:
- 使用 @Lazy:延迟加载,打破初始化顺序
- 使用 Setter 注入代替构造器注入:允许循环依赖存在
- 重构代码:这是最正确的做法,循环依赖本身就是代码坏味道
@Service
public class A {
private B b;
@Autowired
public void setB(@Lazy B b) {
this.b = b;
}
}
3.2 Bean 创建顺序导致的问题
Spring 中 Bean 的创建顺序遵循依赖图拓扑排序。如果你的代码依赖了某个 Bean,但这个 Bean 还没被创建,就会出问题。
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource();
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) { // 依赖 dataSource
return new JdbcTemplate(dataSource);
}
}
3.3 线上 Bean 初始化顺序排查
线上排查 Bean 初始化顺序问题,推荐使用以下方式:
# 开启 Spring 启动日志,打印 Bean 创建顺序
--logging.level.org.springframework.beans=DEBUG
--logging.level.org.springframework.context=DEBUG
或者在 Bean 上加 @Order 注解来控制顺序:
@Component
@Order(1) // 数值越小优先级越高
public class FirstInitializer implements ApplicationRunner { }
@Component
@Order(2)
public class SecondInitializer implements ApplicationRunner { }
四、工程选型
4.1 Spring IoC vs Google Guice
4.2 什么场景用什么注入方式
- 核心业务组件:构造器注入,保证依赖完整性
- 配置属性:
@Value + Setter 注入
- 可选依赖:Setter 注入 +
@Autowired(required = false)
- 基础设施:构造器注入或
@PostConstruct 初始化
五、面试总结
IoC 和 DI 是 Spring 最核心的概念,也是面试中最高频的考点。能回答出基本概念只能过 P5,能说清 Spring 底层通过反射创建 Bean 才能达到 P6,能把 ApplicationContext 初始化流程和 BeanFactory 的区别讲清楚才是 P7+ 的水准。
记住,面试官不是在考你背书,是在考你"有没有真正理解过 Spring 源码"。能说出具体是哪个类、哪个方法、为什么要这么设计的候选人,才是从 80% 进化到 10% 的那一拨人。