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 的子接口",但进一步追问就崩了。

对比维度BeanFactoryApplicationContext
加载时机懒加载,第一次 getBean 时才创建 Bean预加载,容器启动时一次性创建所有 Bean
功能基础 IoC 容器,只提供依赖注入高级容器,除了 DI 还提供 i18n、事件发布、AOP 等
国际化不支持支持
事件发布不支持支持 ApplicationEvent 事件机制
资源加载不支持支持加载 classpath、filesystem 等多种资源
Web 应用XmlBeanFactoryClassPathXmlApplicationContext、AnnotationConfigApplicationContext
校验默认延迟注册 BeanPostProcessor预注册所有 BeanPostProcessor
性能启动快、占用内存少启动慢,但运行时 getBean 无额外开销
// 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();
}

问题:

  1. 紧耦合:OrderService 直接依赖具体实现类,无法替换
  2. 测试困难:无法 mock 依赖,单元测试几乎不可能
  3. 复用性差:对象无法复用,每次 new 都是新实例
  4. 修改成本高:换一个实现类需要修改所有用到它的地方

IoC 带来的好处

  1. 松耦合:对象不负责依赖的创建,只声明需要什么,由容器注入
  2. 可测试:可以注入 mock 对象,单元测试变得简单
  3. 可复用:同一个 Bean 可以被多个对象共享
  4. 可配置:依赖关系可以在配置文件中修改,不需要改代码

2.2 三种注入方式的对比

注入方式优点缺点推荐场景
构造器注入保证依赖不为 null;支持 field final;符合 Immutable 理念构造函数参数多时很丑大多数场景,推荐使用
Setter 注入灵活,可选依赖;适合有默认值的依赖不保证依赖不为 null;可能导致对象状态不完整就被使用可选依赖、配置属性
字段注入代码最简洁无法在编译期检查;无法注入 final;无法单元测试不推荐使用
💡

阿里 Java 规范里明确禁止字段注入(@Autowired 直接打在字段上),推荐使用构造器注入。理由是:字段注入会让对象在构造时处于不完整状态,而且 IDE 无法提示哪些依赖是必须的。

2.3 @Autowired 和 @Resource 的区别

这是面试中经常被问到的问题:

对比@Autowired@Resource
来源Spring 自带JSR-250 标准
注入方式byType + byNamebyName 优先,失败则 byType
位置可以用在 setter、字段、构造函数可以用在 setter、字段
required 属性支持 required=false不支持,但可以用 JSR-330 的 @Nullable
泛型支持支持不支持
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

解决方案:

  1. 使用 @Lazy:延迟加载,打破初始化顺序
  2. 使用 Setter 注入代替构造器注入:允许循环依赖存在
  3. 重构代码:这是最正确的做法,循环依赖本身就是代码坏味道
@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

对比Spring IoCGoogle Guice
模块化功能全面,但较重轻量,只做 DI
配置方式XML、注解、JavaConfig注解 + Java 模块配置
作用域单例、原型、请求作用域等只支持单例和原型
BeanPostProcessor提供丰富的扩展点扩展性较弱
学习成本较高,概念多低,上手快

4.2 什么场景用什么注入方式

  • 核心业务组件:构造器注入,保证依赖完整性
  • 配置属性@Value + Setter 注入
  • 可选依赖:Setter 注入 + @Autowired(required = false)
  • 基础设施:构造器注入或 @PostConstruct 初始化

五、面试总结

IoC 和 DI 是 Spring 最核心的概念,也是面试中最高频的考点。能回答出基本概念只能过 P5,能说清 Spring 底层通过反射创建 Bean 才能达到 P6,能把 ApplicationContext 初始化流程和 BeanFactory 的区别讲清楚才是 P7+ 的水准。

记住,面试官不是在考你背书,是在考你"有没有真正理解过 Spring 源码"。能说出具体是哪个类、哪个方法、为什么要这么设计的候选人,才是从 80% 进化到 10% 的那一拨人。