跳转至

Spring

面试追问地图

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

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


一、Spring IoC 和 DI

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

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

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

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

IoC 的实现机制?

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

BeanFactory 和 FactoryBean 的区别?

频次 ★★★★ · 难度 🟡

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

BeanFactory FactoryBean
本质 容器接口,管理所有 Bean 的生命周期 工厂 Bean,让 Spring 容器管理"用工厂方式创建的对象"
典型实现 DefaultListableBeanFactory、ApplicationContext SqlSessionFactoryBean、ProxyFactoryBean、GsonFactoryBean
创建对象 负责实例化/装配/管理容器中注册的 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 步之后的 finishRefresh → WebServerStartStopLifecycle——保证流量进来时单例已全部就绪。

常见追问: - 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)
处理器 AutowiredAnnotationBeanPostProcessor CommonAnnotationBeanPostProcessor
匹配顺序 先 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 怎么选?→ 注入自身代理更直观、无需额外配置 exposeProxy;AopContext.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_NEW NESTED
实现 挂起外层,新连接新事务 同一事务里打 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)
application ServletContext 内唯一(Web)
websocket WebSocket 会话内唯一

Singleton vs Prototype 生命周期区别?

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

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=...)

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


七、Spring 扩展点

常用扩展点:

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

八、事件机制

Spring 事件机制如何工作?

频次 ★★★ · 难度 🟡

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

三个组成部分:

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

执行流程(源码级):

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

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

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

常见追问 - 事件默认同步还是异步?→ 同步。SimpleApplicationEventMulticaster 的 multicastEvent() 在发布者线程中逐一调用 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 源码级)

频次 ★★★★ · 难度 🔴

是什么:所有请求经过 DispatcherServlet 的 doDispatch() 方法调度,核心骨架如下(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() 内部 RequestParamMethodArgumentResolver、PathVariableMethodArgumentResolver
返回值处理 HandlerMethodReturnValueHandler 第 5 步 ha.handle() 内部 ResponseBodyMethodReturnValueHandler(@ResponseBody)、ModelMethodReturnValueHandler
异常解析 HandlerExceptionResolver 第 7-8 步 ExceptionHandlerExceptionResolver(@ExceptionHandler)、DefaultHandlerExceptionResolver
视图解析 ViewResolver 第 8 步 processDispatchResult InternalResourceViewResolver、ContentNegotiatingViewResolver
拦截器 HandlerInterceptor 第 4/6/8 步 鉴权、日志、性能监控

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

HandlerMapping 和 HandlerAdapter 的作用?

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