面试追问地图

主问题必讲关键点下一层追问
IoC/DIBeanDefinition、容器创建、依赖注入构造器 vs 字段注入、FactoryBean
容器启动refresh() 十二步、BFPP → BPP → 单例实例化onRefresh 与内嵌 Tomcat、启动失败销毁
@AutowiredAutowiredAnnotationBPP、属性填充阶段注入byType/byName、@Resource 区别、字段注入缺点
Bean 生命周期实例化、属性填充、Aware、前后处理器BeanPostProcessor 执行时机、代理何时生成
AOP切点、通知、代理调用JDK/CGLIB 选择、自调用为何失效
事务代理、传播、隔离、回滚规则checked 异常、this 调用、事务边界
循环依赖三级缓存、提前暴露引用构造器循环、prototype、代理对象一致性
作用域singleton/prototype/request单例 Bean 线程安全吗
FactoryBean容器接口 vs 工厂 Bean、& 前缀SqlSessionFactoryBean、ProxyFactoryBean
扩展点BPP、BFPP、ImportSelectorstarter 和自动配置如何使用
事件机制事件发布、@EventListener、广播器同步 vs 异步、@TransactionalEventListener
设计模式工厂、单例、代理、模板Spring 核心组件对应模式、设计原则落地
SpringMVCDispatcherServlet 调度链、doDispatch参数解析、返回值处理、异常处理

Spring 问题通常会沿“容器启动 → Bean 创建 → 代理生成 → 方法调用”连续追问,应保持时间顺序清晰。


一、Spring IoC 和 DI

控制反转、依赖注入、依赖倒置的区别?

概念含义
控制反转(IoC)程序执行流程的控制权从程序员反转到框架。对象的创建、初始化、销毁由容器管理
依赖注入(DI)一种编码技巧,不通过 new 在类内部创建依赖对象,而是外部创建后通过构造器/Setter/字段注入
依赖倒置原则高层模块不依赖低层模块,共同依赖抽象;抽象不依赖具体实现,具体实现依赖抽象

三种依赖注入方式的区别?

  • 构造器注入:通过构造函数传递依赖,保证初始化时依赖已就绪,推荐方式
  • Setter 注入:通过 Setter 设置依赖,灵活性高但依赖可能未完全初始化
  • 字段注入@Autowired 直接注字段,代码简洁但隐藏依赖关系,不推荐生产使用

IoC 的实现机制?

  • 反射:运行时动态加载类、创建实例、调用方法
  • 工厂模式:BeanFactory/ApplicationContext 作为工厂管理 Bean 创建和生命周期
  • 依赖注入:容器负责管理组件间依赖关系

BeanFactory 和 FactoryBean 的区别?

频次 ★★★★ · 难度 🟡

是什么:名字只差一个词,但职责完全不同——BeanFactory 是 IOC 容器FactoryBean 是 Bean 的一种

BeanFactoryFactoryBean
本质容器接口,管理所有 Bean 的生命周期工厂 Bean,让 Spring 容器管理”用工厂方式创建的对象”
典型实现DefaultListableBeanFactoryApplicationContextSqlSessionFactoryBeanProxyFactoryBeanGsonFactoryBean
创建对象负责实例化/装配/管理容器中注册的 Bean调用 getObject() 返回的对象才注册到容器,而非 FactoryBean 本身
Bean 名规则beanName 对应具体 Bean默认 FactoryBean 的 beanName 指向 getObject() 返回的对象;&beanName 取 FactoryBean 本身
接口方法无(BeanFactory 作为一个容器 SPI,不规定数据如何创建)getObject()getObjectType()isSingleton()

为什么需要 FactoryBean?有些 Bean 的创建逻辑太复杂,Spring 的声明式装配表达不了:比如 SqlSessionFactoryBean 需要在 Java 代码里建立 Configuration、解析 XML、注册 Mapper——这个过程没法用 @Bean 注解的方法做到,所以把创建工作封装在 FactoryBean.getObject() 里,由 Spring 管理它的生命周期和依赖注入。

@Bean 方法的对比@Bean 方法本质上也是工厂方法,Spring 内部会对每个 @Bean 方法生成一个 ConfigurationClassBeanDefinition,走的是标准 Bean 创建流程。FactoryBean 则在 Bean 创建流程中作为一个特殊分支AbstractBeanFactory.getBean()doGetBean() 中检测 isFactoryBean() → 若是则从 getObject() 获取实际对象),可以用更灵活的编程方式创建对象。

常见追问

  • 怎么取到 FactoryBean 本身?→ ctx.getBean("&sqlSessionFactory")& 前缀是 Spring 的约定
  • @Bean 方法和 FactoryBean 怎么选?→ 配置属性简单用 @Bean;需要读取配置文件和资源、建立复杂对象图的用 FactoryBean(如 SqlSessionFactoryBean 要读所有 Mapper XML 路径)
  • FactoryBean 本身是否受 Bean 生命周期管理?→ 是。FactoryBean 自己也经历实例化→属性填充→初始化的完整生命周期,只是 getObject() 返回的对象不受容器管理(除非指定为单例只调一次)

容器启动 refresh() 做了什么?(源码级)

难度 🔴

是什么AbstractApplicationContext#refresh() 是 Spring 容器启动的总入口,十二步模板方法把”读配置 → 准备容器 → 执行扩展点 → 实例化单例 → 发布就绪事件”串成固定流程。

源码骨架(Spring Framework 5.3+,AbstractApplicationContext.java,只留主干):

public void refresh() throws BeansException {
    synchronized (this.startupShutdownMonitor) {
        // 1. 准备刷新:记录启动时间、初始化占位符属性源、校验必需属性
        prepareRefresh();
 
        // 2. 获取 BeanFactory:XML 版在这里解析配置创建新工厂;
        //    注解版(GenericApplicationContext)工厂早已创建,这里只做校验
        ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
 
        // 3. 准备 BeanFactory:设置类加载器、SpEL 解析器、
        //    注册环境 Bean(environment/systemProperties)、忽略 Aware 接口的自动装配
        prepareBeanFactory(beanFactory);
 
        try {
            // 4. 子类扩展点(空实现):Web 容器在此注册 request/session 作用域
            postProcessBeanFactory(beanFactory);
 
            // 5. 执行 BeanFactoryPostProcessor:
            //    ConfigurationClassPostProcessor 在这里解析 @Configuration/
            //    @ComponentScan/@Import,把扫到的类注册成 BeanDefinition
            invokeBeanFactoryPostProcessors(beanFactory);
 
            // 6. 注册 BeanPostProcessor(只注册不执行):
            //    AutowiredAnnotationBeanPostProcessor、AOP 的
            //    AnnotationAwareAspectJAutoProxyCreator 都在此就位
            registerBeanPostProcessors(beanFactory);
 
            // 7/8. 初始化国际化(MessageSource)和事件广播器
            initMessageSource();
            initApplicationEventMulticaster();
 
            // 9. 子类扩展点(空实现):
            //    SpringBoot 的 ServletWebServerApplicationContext 重写此方法,
            //    在这里创建并启动内嵌 Tomcat —— 所以"Tomcat 在单例实例化之前启动"
            onRefresh();
 
            // 10. 注册事件监听器
            registerListeners();
 
            // 11. 实例化所有非懒加载单例 Bean(最重的一步):
            //     preInstantiateSingletons() 循环 getBean(),
            //     走完"实例化→属性填充→初始化"完整生命周期(见第一章)
            finishBeanFactoryInitialization(beanFactory);
 
            // 12. 收尾:初始化 LifecycleProcessor、发布 ContextRefreshedEvent
            finishRefresh();
        } catch (BeansException ex) {
            destroyBeans();      // 失败时销毁已创建的单例
            cancelRefresh(ex);
            throw ex;
        }
    }
}

答题要点:十二步不用逐一背,抓住三个关键锚点——第 5 步执行 BeanFactoryPostProcessor(@ComponentScan 扫描发生在这里,操作的是 BeanDefinition 而非 Bean 实例);第 6 步只注册 BeanPostProcessor,真正执行在第 11 步创建 Bean 时;第 11 步 finishBeanFactoryInitialization 才开始实例化业务 Bean。能说清”先改图纸(BeanDefinition),再装流水线(BPP),最后生产(getBean)“就是理解而非背题。

与 SpringBoot 的衔接(高频追问):SpringApplication.run() 最终调用的就是 refresh()(见SpringBoot启动流程一节)。内嵌 Tomcat 在第 9 步 onRefresh() 创建,但 Connector 真正开始接收请求是在第 12 步之后的 finishRefreshWebServerStartStopLifecycle——保证流量进来时单例已全部就绪。

常见追问

  • BeanFactoryPostProcessor 和 BeanPostProcessor 的区别?→ 前者第 5 步执行、加工 BeanDefinition(配置元数据);后者第 6 步注册、第 11 步创建每个 Bean 时执行、加工 Bean 实例(AOP 代理就在这生成)
  • 为什么 AOP 代理创建器要在第 6 步就注册?→ 它必须在任何业务 Bean 创建之前就位,否则先创建的 Bean 会错过代理时机
  • refresh() 失败会怎样?→ catch 中 destroyBeans() 销毁已创建单例并取消刷新,这就是启动报错时看到”应用上下文关闭”日志的原因

通用概念:refresh() 是模板方法模式的教科书实现——父类定死骨架顺序,postProcessBeanFactory/onRefresh 留空给子类扩展。同一模式:Servlet 容器的生命周期回调、JVM 类加载器的 loadClass 骨架(双亲委派也是模板方法)。

@Autowired 是怎么完成注入的?

难度 🔴

是什么@Autowired 的注入不是”容器启动时统一扫一遍”,而是由 AutowiredAnnotationBeanPostProcessor(一个 BeanPostProcessor)在每个 Bean 创建过程中的属性填充阶段完成的。

源码时机(Spring Framework 5.3+,两步走):

// 第一步:实例化后、属性填充前,收集注入点(哪些字段/方法标了 @Autowired)
// AutowiredAnnotationBeanPostProcessor#postProcessMergedBeanDefinition
//   → buildAutowiringMetadata() 反射扫描字段和方法,缓存为 InjectionMetadata
 
// 第二步:属性填充阶段(populateBean),真正执行注入
// AutowiredAnnotationBeanPostProcessor#postProcessProperties
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
    InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
    metadata.inject(bean, beanName, pvs);  // 对每个注入点:
    // 1. resolveDependency() 按类型从容器找候选 Bean
    // 2. 多个候选 → @Primary → @Priority → 按字段名匹配 beanName
    // 3. field.setAccessible(true); field.set(bean, value) 反射赋值
    return pvs;
}

@Autowired vs @Resource(必考对比):

维度@Autowired@Resource
来源Spring 自带JSR-250 标准注解(JDK/Jakarta)
处理器AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor
匹配顺序先 byType,多候选再按 @Primary/@Qualifier/字段名先 byName(默认取字段名),找不到才 byType
指定名称配合 @Qualifier("name")直接 @Resource(name = "xxx")
可标位置构造器、字段、setter、方法参数字段、setter(不能标构造器)

字段注入为什么不推荐(IDEA 会警告,高频追问):

  • 隐藏依赖:依赖不出现在构造器签名里,类的协作关系不可见,容易演变成一个类注入十几个依赖而无察觉
  • 脱离容器不可用:单测想 new 出来手动塞依赖只能靠反射;构造器注入直接传参即可
  • 无法声明 final:不能表达”依赖不可变”,存在 NPE 窗口期(对象已构造但尚未注入)
  • 掩盖循环依赖:构造器注入的循环依赖启动即报错(暴露设计问题),字段注入靠三级缓存”救活”(见下文循环依赖一章),坏味道被隐藏

常见追问

  • @Autowired 标在构造器上还需要 AutowiredAnnotationBeanPostProcessor 吗?→ 需要,但走的是另一个入口 determineCandidateConstructors()(SmartInstantiationAwareBeanPostProcessor 接口),在实例化阶段就介入,比属性填充更早;单构造器时注解可省略
  • byType 找到多个候选又没有 @Primary 会怎样?→ 尝试按字段名匹配 beanName,仍无法唯一则抛 NoUniqueBeanDefinitionException
  • static 字段能注入吗?→ 不能,buildAutowiringMetadata 里显式跳过 static 成员(类级状态不该绑定到单个实例)

通用概念:注入的本质是依赖解析 + 反射赋值,“先收集元数据并缓存、再统一执行”的两段式也是通用优化模式——对照 MyBatis 启动时解析 XML 缓存 MappedStatement、JVM JIT 先 profile 再编译。


三、AOP 面向切面编程

AOP 的实现方式(JDK 动态代理 vs CGLIB)?

难度 🔴

快答

特性JDK 代理CGLIB 代理
原理动态生成 $Proxy 类继承目标类
条件必须实现接口任何类
性能创建快,调用快创建慢,调用快
能代理 static
能代理 final
Spring 选择优先用没接口才用

Spring Boot 2.0+ 默认全局用 CGLIB(spring.aop.proxy-target-class=true),不再优先 JDK 代理——统一代理方式,避免同一个 Bean 因是否实现接口而出现两种代理行为不一致的坑。

为什么同类方法内部调用会导致 AOP 失效?(自调用问题)

频次 ★★★★ · 难度 🟡

是什么:AOP 通知(事务、日志、权限)只在通过代理对象调用目标方法时生效。同一个类里 methodA() 直接调用 this.methodB(),这个 this原始对象不是代理对象——调用完全绕开代理,methodB 上的 @Transactional/@Async/自定义切面统统不触发。

为什么会这样:Spring AOP 基于动态代理,代理对象包裹在目标对象外层;容器里放的是代理对象,外部注入拿到的也是代理对象,但目标对象内部并不知道自己被代理了,方法内的 this 永远指向原始对象。这是动态代理(区别于 AspectJ 编译期织入)的固有限制——字节码层面没有改写方法体,只是在外面包了一层。

三种规避方式

// 方式 1:注入自身代理(最常用)
@Service
public class UserService {
    @Autowired
    private UserService self;   // Spring 允许注入自己的代理
 
    public void methodA() {
        self.methodB();          // ✅ 走代理,事务生效
    }
 
    @Transactional
    public void methodB() {}
}
 
// 方式 2:AopContext 拿当前代理(需 exposeProxy=true)
@EnableAspectJAutoProxy(exposeProxy = true)
public class UserService {
    public void methodA() {
        ((UserService) AopContext.currentProxy()).methodB();  // ✅
    }
    @Transactional
    public void methodB() {}
}
 
// 方式 3:拆分到另一个 Bean(最推荐,顺便也是更好的设计)
// methodB 挪到 OrderService,UserService 注入 OrderService 后正常跨 Bean 调用即可触发代理

常见追问

  • 方式 1 和方式 2 怎么选?→ 注入自身代理更直观、无需额外配置 exposeProxyAopContext.currentProxy() 是 Spring 内部实现方式的暴露,属于更 hack 的手段,一般只在无法改动依赖注入结构时用
  • 拆到另一个 Bean 为什么是”更好的设计”?→ 需要自调用绕过代理,本身常常提示这两个方法属于不同职责,拆分往往顺带改善了单一职责

四、@Transactional 详解

@Transactional(
    value = "transactionManager",  // 事务管理器
    transactionManager = "txManager",
    propagation = Propagation.REQUIRED,  // 事务传播
    isolation = Isolation.DEFAULT,       // 隔离级别
    timeout = -1,                        // 超时时间(秒)
    readOnly = false,                    // 只读优化
    rollbackFor = Exception.class,       // 回滚异常
    noRollbackFor = IOException.class    // 不回滚异常
)
public void method() {}

七种传播行为(按「当前有事务时的表现」记忆):

传播行为当前有事务当前无事务
REQUIRED(默认)加入新建
REQUIRES_NEW挂起当前,新建独立事务新建
NESTED创建 savepoint 嵌套执行新建
SUPPORTS加入非事务执行
NOT_SUPPORTED挂起当前,非事务执行非事务执行
MANDATORY加入抛异常
NEVER抛异常非事务执行

REQUIRES_NEW vs NESTED(高频场景题)

@Transactional
public void placeOrder(Order order) {
    orderMapper.insert(order);          // 外层事务
    try {
        logService.saveLog(order);      // 记日志失败不能影响下单
    } catch (Exception ignored) { }
    pointsService.addPoints(order);     // 加积分失败要整体回滚吗?看业务
}
REQUIRES_NEWNESTED
实现挂起外层,新连接新事务同一事务里打 savepoint(JDBC Connection.setSavepoint
内层失败只回滚自己,外层可 catch 后继续回滚到 savepoint,外层可继续
外层失败内层已提交,不回滚(日志留下了)内层跟着全部回滚
提交时机内层方法结束立即提交跟外层一起提交
  • 记操作日志、发通知——失败不影响主流程且主流程失败日志也要保留 → REQUIRES_NEW
  • 批量处理中单条失败只跳过该条,但整批取消时全部回滚 → NESTED

源码:事务是怎么“传播”起来的

调用链:TransactionInterceptor.invoke()invokeWithinTransaction()AbstractPlatformTransactionManager.getTransaction()

// AbstractPlatformTransactionManager#getTransaction(Spring 6,节选简化)
public final TransactionStatus getTransaction(TransactionDefinition def) {
    Object transaction = doGetTransaction();     // 从 ThreadLocal 取当前连接
    if (isExistingTransaction(transaction)) {
        // 已有事务 → 传播行为的分歧点全在这里
        return handleExistingTransaction(def, transaction);
    }
    // 没有事务:MANDATORY 直接抛异常;REQUIRED/REQUIRES_NEW/NESTED 开新事务
    if (def.getPropagationBehavior() == PROPAGATION_MANDATORY) {
        throw new IllegalTransactionStateException("...");
    }
    ...
}
 
private TransactionStatus handleExistingTransaction(TransactionDefinition def, Object transaction) {
    if (def.getPropagationBehavior() == PROPAGATION_NEVER) {
        throw new IllegalTransactionStateException("...");
    }
    if (def.getPropagationBehavior() == PROPAGATION_REQUIRES_NEW) {
        SuspendedResourcesHolder suspended = suspend(transaction); // 挂起:把连接从 ThreadLocal 摘下来存好
        return startTransaction(def, transaction, ...);            // 拿新连接开新事务
    }
    if (def.getPropagationBehavior() == PROPAGATION_NESTED) {
        DefaultTransactionStatus status = prepareTransactionStatus(def, transaction, ...);
        status.createAndHoldSavepoint();                           // 同一连接上打 savepoint
        return status;
    }
    return prepareTransactionStatus(def, transaction, false, ...); // REQUIRED 等:直接加入
}

三个关键点:

  1. “当前事务”= 绑定在 ThreadLocal 上的数据库连接TransactionSynchronizationManager)——这就是多线程/异步方法里事务失效的根源:新线程的 ThreadLocal 里没有连接
  2. 挂起(suspend)= 把连接从 ThreadLocal 摘下暂存,恢复时再放回;REQUIRES_NEW 会占用第二个连接,嵌套层数深时可能耗尽连接池造成自死锁
  3. NESTED 不换连接,靠 JDBC savepoint 部分回滚——所以要求 DataSource 支持 savepoint,且 JTA 分布式事务下不可用

通用概念:传播行为本质是上下文传递策略的枚举——“跟随现有上下文 / 新开上下文 / 拒绝”。同样的模式出现在线程上下文(InheritableThreadLocal)、Go 的 context、分布式链路追踪的 span 传递中。

Spring 事务什么时候会失效?

频次 ★★★★★ · 难度 🔴

根因只有一个:事务靠 AOP 代理实现,凡是没走代理对象调用代理层判断不满足回滚条件,事务就不生效。7 种场景对应两类根因:

场景根因正确做法
非 public 方法TransactionInterceptor 底层的 AbstractFallbackTransactionAttributeSource 只解析 public 方法上的注解改为 public,或用 AspectJ 编译期织入(不受此限制)
同类方法内部调用自调用绕开代理,见上文”自调用问题”注入自身代理 / AopContext.currentProxy() / 拆到另一个 Bean
异常被 try-catch 吞掉代理只在方法抛出异常时触发回滚,方法内部 catch 了就等于”正常返回”不要空 catch;确实要处理就在 catch 里手动 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
异常类型不匹配默认只回滚 RuntimeException/Error,checked 异常默认不回滚显式 rollbackFor = Exception.class
数据库引擎不支持事务MyISAM 没有事务能力,@Transactional 无从谈起确认表引擎是 InnoDB
标在 static 方法上代理基于实例方法,static 方法不属于任何实例,代理机制覆盖不到不要用,或改造成实例方法
新起线程执行”当前事务”绑定在发起线程的 ThreadLocal 上(见上文源码),新线程读不到事务方法内不要开新线程;确需异步,先提交事务再发起异步任务

常见追问

  • 这 7 种场景能归成几类?→ 两类:代理没生效(非 public、自调用、static)和代理生效但回滚条件不满足(异常被吞、异常类型不对、引擎不支持、跨线程丢上下文)。答题时先归类比背 7 条更体现理解。
  • 只读事务 readOnly = true 有什么用?→ 提示 JDBC 驱动和数据库做只读优化(如 MySQL 主从场景可能路由到从库、部分驱动跳过事务日志),本身不阻止写操作,别指望它做权限控制。

五、循环依赖与三级缓存

Spring 如何解决循环依赖?

难度 🔴

快答:仅解决 单例 + Setter/字段注入 的循环依赖。通过三级缓存 + 提前暴露未完全初始化的引用。

三级缓存(DefaultSingletonBeanRegistry):

缓存类型作用
singletonObjects(一级)Map<String, Object>完全初始化好的 Bean
earlySingletonObjects(二级)Map<String, Object>早期引用(可能是代理对象)
singletonFactories(三级)Map<String, ObjectFactory<?>>工厂对象,按需生成早期引用

解决过程(A 依赖 B,B 依赖 A):

  1. 创建 A 实例 → 放入三级缓存(ObjectFactory)
  2. A 属性注入时发现需要 B → 创建 B 实例 → 放入三级缓存
  3. B 属性注入时发现需要 A → 从三级缓存获取 A 的工厂 → 调用 getObject() 生成早期引用 → 放入二级缓存 → 注入 B
  4. B 完成初始化 → 升一级缓存
  5. A 继续注入 B → 完成初始化 → 升一级缓存

为什么必须用三级缓存?二级不够?

核心是为了 正确处理 AOP 代理的 Bean

如果只有二级缓存,B 注入 A 时拿到的是原始对象,但 A 最终生成的是代理对象 → 同一个 Bean 出现两个实例,违反单例。

三级缓存中的 ObjectFactory 可以智能判断:需要代理就提前生成代理放入二级缓存,不需要就返回原始对象。本质是 “按需延迟生成正确引用”

哪些情况 Spring 无法解决循环依赖?

  • 构造器注入的循环依赖(实例化前就需要依赖,死锁)
  • 原型(prototype)模式下的循环依赖
  • 非单例模式的循环依赖

六、Bean 作用域与生命周期

Spring Bean 的作用域?

作用域说明
singleton默认,容器内唯一实例
prototype每次请求创建新实例
request每个 HTTP 请求一个实例(Web)
session每个用户会话一个实例(Web)
applicationServletContext 内唯一(Web)
websocketWebSocket 会话内唯一

Singleton vs Prototype 生命周期区别?

阶段SingletonPrototype
创建时机容器启动时(或首次)每次请求时
初始化完整执行生命周期完整执行到初始化
销毁容器关闭时自动销毁容器不管理销毁,需手动释放

Spring Bean 的完整生命周期?

频次 ★★★★★ · 难度 🔴

是什么AbstractAutowireCapableBeanFactory 按固定顺序执行 11 个阶段:

1.  实例化(Instantiation)→ createBeanInstance,走构造器
2.  属性赋值(Populate)→ 依赖注入(DI),走 @Autowired 等(见第一章)
3.  BeanNameAware.setBeanName()
4.  BeanClassLoaderAware.setBeanClassLoader()
5.  BeanFactoryAware.setBeanFactory()
6.  BeanPostProcessor.postProcessBeforeInitialization()(@PostConstruct 在这里触发)
7.  InitializingBean.afterPropertiesSet()
8.  自定义 init-method
9.  BeanPostProcessor.postProcessAfterInitialization()(AOP 代理常在这一步生成)
10. 可用状态(ready to use)
11. 销毁:@PreDestroy → DisposableBean.destroy() → 自定义 destroy-method

扩展方式对照

时机注解接口XML/注解配置
初始化前@PostConstruct
初始化InitializingBean.afterPropertiesSet()init-method / @Bean(initMethod=...)
销毁前@PreDestroy
销毁DisposableBean.destroy()destroy-method / @Bean(destroyMethod=...)

常见追问

  • @PostConstructafterPropertiesSet() 谁先执行?→ @PostConstruct 先——它由 CommonAnnotationBeanPostProcessor 实现,挂在第 6 步 postProcessBeforeInitializationafterPropertiesSet() 是第 7 步,在所有 postProcessBeforeInitialization 跑完之后才调用。两者常被误认为”同时”或顺序搞反,这是高频追问陷阱。
  • AOP 代理是什么时候生成的?→ 通常在第 9 步 postProcessAfterInitializationAbstractAutoProxyCreator 的实现),所以 Bean 生命周期前半段(属性填充、@PostConstruct)操作的都是原始对象,只有从第 9 步之后拿到的才是代理对象——这也是为什么 AOP 代理无法拦截构造器和早期初始化阶段的调用。
  • 为什么销毁顺序是 @PreDestroy → DisposableBean → destroy-method,和初始化对称?→ Spring 内部实现上两者确实对称设计(DestructionAwareBeanPostProcessor 与初始化的 BeanPostProcessor 对应),保持”先注解、再接口、再配置”的统一优先级心智模型。

七、Spring 扩展点

常用扩展点:

扩展点作用
BeanFactoryPostProcessor容器实例化 Bean 之前修改 Bean 定义
BeanPostProcessorBean 实例化、初始化 前后额外处理(AOP 代理的核心)
ImportSelector根据条件动态注册 Bean 定义(自动装配核心)
ImportBeanDefinitionRegistrar动态注册 Bean 定义
HandlerInterceptor拦截 MVC 请求
ControllerAdvice全局异常处理、数据绑定

八、事件机制

Spring 事件机制如何工作?

频次 ★★★ · 难度 🟡

是什么:Spring 事件机制是观察者模式在容器内的实现——Bean 可以发布事件,其他 Bean 监听并响应,调用方和监听方完全解耦。

三个组成部分

角色接口/注解说明
事件ApplicationEvent(继承它)或 PayloadApplicationEvent(直接包装任意荷载)携带上下文数据
发布者ApplicationEventPublishercontext.publishEvent()容器本身即是发布器
监听器@EventListener / ApplicationListener<T> 接口接收并处理事件

执行流程(源码级):

publisher.publishEvent(event)

AbstractApplicationContext.publishEvent()

ApplicationEventMulticaster.multicastEvent()
  ↓ 遍历所有注册的 ApplicationListener
  ↓ 筛选出能处理该事件类型的 listener
listener.onApplicationEvent(event)

事件广播器 ApplicationEventMulticasterrefresh() 第 8 步 initApplicationEventMulticaster() 初始化,默认为 SimpleApplicationEventMulticaster(同步调用,即发布线程也是监听线程)。

@EventListener 原理(高频追问):它不是反射逐一检查每个 Bean 的方法,而是由 EventListenerMethodProcessor(实现 SmartInitializingSingleton)在所有单例 Bean 实例化完成后,扫描每个 Bean 中标了 @EventListener 的方法,为每个方法注册一个 ApplicationListenerAdapter 到广播器。这个”后处理”时机确保了所有监听器在容器启动的最后阶段注册完毕。

常见追问

  • 事件默认同步还是异步?→ 同步。SimpleApplicationEventMulticastermulticastEvent() 在发布者线程中逐一调用 listener。需要异步就在 @EventListener 上加 @Async,或替换 ApplicationEventMulticaster 为带 Executor 的实现
  • @TransactionalEventListener 有什么用?→ 在事务的特定阶段触发(AFTER_COMMIT / AFTER_ROLLBACK / AFTER_COMPLETION / BEFORE_COMMIT),确保监听器只在事务成功提交后才执行——避免事务回滚了但事件已经被处理了的场景
  • 监听器抛出异常会影响其他监听器吗?→ 默认同步模式下,先注册的监听器抛异常会导致后续监听器收不到事件;异步模式则互不影响

九、设计模式

Spring 中用到的设计模式

模式应用
工厂模式BeanFactory、ApplicationContext
代理模式AOP(JDK/CGLIB)
单例模式Bean 默认单例
模板方法JdbcTemplate、RestTemplate
观察者模式事件驱动(ApplicationEvent/Listener)
适配器模式HandlerAdapter 适配不同 Controller
策略模式AOP 选择 JDK/CGLIB 代理策略
装饰器模式TransactionAwareCacheDecorator

十、SpringMVC

Servlet、Tomcat、Nginx 和完整 Web 请求链见 Web容器与Nginx

SpringMVC 处理流程?(doDispatch 源码级)

频次 ★★★★ · 难度 🔴

是什么:所有请求经过 DispatcherServletdoDispatch() 方法调度,核心骨架如下(Spring 6.x,DispatcherServlet.java):

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    ModelAndView mv = null;
 
    try {
        // 1. 检查是否是 multipart 请求(文件上传)
        processedRequest = checkMultipart(request);
 
        // 2. HandlerMapping 获取执行链(Handler + 拦截器链)
        mappedHandler = getHandler(processedRequest);
        if (mappedHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
        }
 
        // 3. HandlerAdapter 获取能处理该 Handler 的适配器
        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
 
        // 4. 前置拦截器:执行所有拦截器的 preHandle()
        if (!mappedHandler.applyPreHandle(processedRequest, response)) {
            return;  // 任一拦截器返回 false,请求终止
        }
 
        // 5. HandlerAdapter.handle() 真正执行 Controller 方法
        //    — 参数解析器在此工作(HandlerMethodArgumentResolver)
        //    — 调用方法后返回值处理器工作(HandlerMethodReturnValueHandler)
        mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
 
        // 6. 后置拦截器:执行所有拦截器的 postHandle()
        mappedHandler.applyPostHandle(processedRequest, response, mv);
    } catch (Exception ex) {
        // 7. 异常解析链:HandlerExceptionResolver 链处理
        dispatchException = ex;
    }
 
    // 8. 视图渲染:processDispatchResult()
    //    — 异常模式下调用 HandlerExceptionResolver 解析视图
    //    — 正常模式下 ViewResolver 解析视图 → View.render()
    //    — 无论异常/正常,最终执行 afterCompletion()
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

五个扩展点及其在源码中的位置

扩展点对应接口doDispatch 中位置典型实现
参数解析HandlerMethodArgumentResolver第 5 步 ha.handle() 内部RequestParamMethodArgumentResolverPathVariableMethodArgumentResolver
返回值处理HandlerMethodReturnValueHandler第 5 步 ha.handle() 内部ResponseBodyMethodReturnValueHandler@ResponseBody)、ModelMethodReturnValueHandler
异常解析HandlerExceptionResolver第 7-8 步ExceptionHandlerExceptionResolver@ExceptionHandler)、DefaultHandlerExceptionResolver
视图解析ViewResolver第 8 步 processDispatchResultInternalResourceViewResolverContentNegotiatingViewResolver
拦截器HandlerInterceptor第 4/6/8 步鉴权、日志、性能监控

面试答题策略:先画流程图(5 步链),再亮源码骨架 doDispatch,然后追问往深扩展——“参数解析器怎么知道参数类型”答 HandlerMethodArgumentResolver.supportsParameter()resolveArgument();“@ResponseBody 的原理”答 RequestResponseBodyMethodProcessor(既是参数解析器又是返回值处理器)。

HandlerMapping 和 HandlerAdapter 的作用?

  • HandlerMapping:将 URL 请求映射到对应的 Controller(根据 URL、参数等找到处理器)
  • HandlerAdapter:适配不同类型的 Controller 并调用(适配器模式),因为 Controller 可能有不同接口类型