面试追问地图
| 主问题 | 必讲关键点 | 下一层追问 |
|---|---|---|
| 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 等:直接加入
}三个关键点:
- “当前事务”= 绑定在 ThreadLocal 上的数据库连接(
TransactionSynchronizationManager)——这就是多线程/异步方法里事务失效的根源:新线程的 ThreadLocal 里没有连接 - 挂起(suspend)= 把连接从 ThreadLocal 摘下暂存,恢复时再放回;REQUIRES_NEW 会占用第二个连接,嵌套层数深时可能耗尽连接池造成自死锁
- 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):
- 创建 A 实例 → 放入三级缓存(ObjectFactory)
- A 属性注入时发现需要 B → 创建 B 实例 → 放入三级缓存
- B 属性注入时发现需要 A → 从三级缓存获取 A 的工厂 → 调用 getObject() 生成早期引用 → 放入二级缓存 → 注入 B
- B 完成初始化 → 升一级缓存
- 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 可能有不同接口类型