引言:在面试中,对于 Java 开发者来说,掌握 Spring 框架的原理和使用是至关重要的。其中,了解 Spring 的启动流程、循环依赖问题的解决方法以及与设计模式相关的内容是常见的面试题目。

题目

面试官:Spring 启动过程是什么样的?详细讲讲你自己的理解!

推荐解析

启动流程

面试官:Spring 启动过程是什么样的?详细讲讲你自己的理解!_配置文件

1)加载配置文件

Spring 启动时会加载应用程序的配置文件,通常是 applicationContext.xml 或者通过 Java Config 配置类。这些配置文件包含了 Spring Bean 的定义、依赖关系、AOP 设置、事务管理器等信息。

2)创建并初始化 Spring 容器

一旦配置文件加载完成,Spring 将会创建并初始化一个 Spring 容器(ApplicationContext)。Spring 容器负责管理应用程序中的所有 Bean,以及它们之间的依赖关系。容器的初始化包括实例化 Bean、注入依赖、应用 AOP 等。

3)扫描组件

Spring 容器会扫描配置文件中指定的包,查找带有特定注解的组件类,如 @Component@Service@Repository@Controller。一旦找到这些组件类,Spring 将会实例化它们并将它们纳入容器管理。

4)实例化 Bean

Spring 容器会根据配置文件中的定义,实例化所有的 Bean。这些 Bean 可能是普通的 POJO 类、数据访问对象(DAO)、服务类等。

5)注入依赖

在实例化 Bean 的过程中,Spring 容器会解析 Bean 之间的依赖关系,并将依赖的 Bean 注入到需要它们的 Bean 中。这个过程可以通过构造函数注入、Setter 方法注入或者字段注入来完成。

6)应用 AOP

如果应用程序中使用了 AOP(面向切面编程),Spring 将会应用切面逻辑,为 Bean 动态生成代理,并将切面逻辑织入到相应的 Bean 中。这个过程包括创建代理对象、将切面逻辑织入到代理对象中等。

7)触发生命周期回调

Spring 容器会在 Bean 实例化、依赖注入完成以及其他初始化工作完成后,触发相应 Bean 的生命周期回调方法,如 InitializingBean 接口的 afterPropertiesSet() 方法、@PostConstruct 注解标注的方法等。

8)完成启动

一旦所有的 Bean 实例化、依赖注入和初始化工作完成,Spring 容器就会完成启动过程,应用程序就可以开始处理请求和响应了。

经典面试题

怎么知道 Spring 的 Bean 加载完成?

使用 ApplicationListener 监听容器启动事件

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;

public class MyApplicationListener implements ApplicationListener<ContextRefreshedEvent> {
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 在容器启动完成后执行操作
        System.out.println("Spring容器已启动,所有Bean加载完成");
    }
}

使用 @EventListener 注解监听容器启动事件

import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class MyEventListener {
    
    @EventListener
    public void handleContextRefresh(ContextRefreshedEvent event) {
        // 在容器启动完成后执行操作
        System.out.println("Spring容器已启动,所有Bean加载完成");
    }
}

Spring 的 IOC 用到了 哪些设计模式?

  1. 依赖注入(Dependency Injection):在Spring IOC中,通过将对象之间的依赖关系交由IOC容器来处理,实现了松耦合和高度可测试性。
  2. 工厂模式(Factory Pattern):在Spring IOC中,BeanFactory就是一个工厂类,用于创建和管理Bean实例。
  3. 单例模式(Singleton Pattern):在Spring IOC中,默认情况下所有Bean都是单例的,并且可以通过配置控制是否启用单例模式。
  4. 模板方法模式(Template Method Pattern):在Spring AOP中,通过定义抽象切面类和具体实现类来实现切面逻辑复用。
  5. 观察者模式(Observer Pattern):在Spring事件机制中,观察者监听某个事件源发生的事件,并进行相应的处理。
  6. 策略模式(Strategy Pattern):在Spring MVC中,通过定义多个HandlerMapping策略来实现URL到Controller映射。

其他补充

Spring 解决循环依赖问题的主要方法是通过三级缓存(three-level cache)和“提前暴露”(early exposure)的机制。

三级缓存

Spring 在创建 Bean 的过程中,会维护三级缓存,分别是 singletonObjects、earlySingletonObjects 和 singletonFactories。这三级缓存的作用如下:

  1. singletonObjects: 存放已经完全初始化的单例 Bean 实例。
  2. earlySingletonObjects: 存放已经实例化但未完全初始化的单例 Bean 实例。
  3. singletonFactories: 存放 Bean 工厂对象,用于解决循环依赖问题。

提前暴露

Spring 在创建 Bean 的过程中,会提前暴露尚未完全初始化的 Bean 实例,以便解决循环依赖问题。在处理循环依赖时,Spring 遵循以下步骤:

  1. 当容器需要获取 A 类型的 Bean 实例时,如果发现 A 还未创建,则创建一个 A 类型的 Bean 对象,并将其放入 earlySingletonObjects 缓存中,表示 A 已经实例化但未完全初始化。
  2. 接着创建 A 类型 Bean 的依赖 Bean,如果发现某个依赖 Bean 依赖于 A,则会从 earlySingletonObjects 缓存中提前暴露 A 的代理对象(尚未完全初始化)给依赖 Bean。
  3. 当 A 类型 Bean 的初始化完成后,将其放入 singletonObjects 缓存中,表示 A 已经完全初始化。
  4. 最后,再将 A 的代理对象替换为完全初始化的 A 实例。

这样一来,通过提前暴露尚未完全初始化的 Bean 实例,Spring 能够在循环依赖的情况下确保每个 Bean 都能够得到正确的依赖实例。

示例

假设有两个类 A 和 B,彼此相互依赖:

public class A {
    private B b;

    public A(B b) {
        this.b = b;
    }
}

public class B {
    private A a;

    public B(A a) {
        this.a = a;
    }
}

在这种情况下,Spring 会通过三级缓存和提前暴露的机制解决循环依赖问题,确保 A 和 B 都能够正确地获取到彼此的实例。

欢迎交流

本文主要讲述 Spring 启动流程,和两个经典面试题,关于 Spring 循环依赖问题,可以去看下三级缓存的源码进行具体分析,在文末还有三个问题,欢迎小伙伴在评论区留言!

1)Spring 中的 Bean 生命周期是怎样的?

2)Spring 中的依赖注入方式有哪些?它们之间有什么区别?

3)在 Spring 中如何实现事务管理?