Spring Boot 核心与事务
校招考察频度:★ 高频(核心必考·高频生死线) 一句话速记:IoC 管对象创建注入与 BeanPostProcessor 扩展,三级缓存借工厂延迟曝光 AOP 代理破循环依赖;AOP 靠动态代理链织入切面;声明式事务最怕同类自调用与异常不抛/不回滚;自动装配扫描 imports 文件配条件注解按需装载。
Spring / Spring Boot 是国内几乎所有企业级 Java 系统的“操作系统”。面试官不会停留在“怎么写 Controller”的表层,而是深挖Bean 生命周期设计模式、循环依赖底层三级缓存时序、声明式事务在生产中的失效陷阱,以及 Spring Boot 自动装配机制。
一、Spring IoC 容器与 Bean 完整生命周期
Bean 在 Spring 容器中的生命历程绝不仅仅是 new 一个对象,它是一条高度可扩展的流水线:
┌─────────────────────────────────────────────────────────────┐
│ 1. 实例化 (Instantiation): 反射推导构造器,创建原始 Bean 对象 │
└──────────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. 属性填充 (PopulateBean): 解析 @Autowired/@Value,依赖注入 │
└──────────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. 初始化 (Initialization): │
│ ① Aware 接口回调 (BeanNameAware / ApplicationContextAware)│
│ ② BeanPostProcessor.postProcessBeforeInitialization() │
│ ③ @PostConstruct 标注的方法 │
│ ④ InitializingBean.afterPropertiesSet() │
│ ⑤ 自定义 init-method 方法 │
│ ⑥ BeanPostProcessor.postProcessAfterInitialization() │
│ (★ 关键点: AOP 动态代理通常在此处生成代理对象替换原对象!) │
└──────────────────────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. 投入使用与销毁 (Destruction): │
│ @PreDestroy ➔ DisposableBean.destroy() ➔ destroy-method │
└─────────────────────────────────────────────────────────────┘面试加分点:BeanPostProcessor(后置处理器)
BeanPostProcessor是 Spring 极其精妙的开闭原则体现。Spring 的核心功能(如 AOP 代理生成、@Async异步方法包装、@Scheduled定时任务注册)几乎全是通过实现BeanPostProcessor在 Bean 初始化前后无侵入植入的。
二、Spring 三级缓存解决循环依赖底层深度推导
循环依赖是指:类 A 依赖类 B(@Autowired private B b;),而类 B 又反向依赖类 A(@Autowired private A a;)。
1. 三级缓存的定义与分工
| 缓存层级 | 变量名 | 底层数据结构 | 存放内容与状态 |
|---|---|---|---|
| 一级缓存 | singletonObjects | ConcurrentHashMap<String, Object> | 成品单例池:已经经历过完整实例化、属性填充、初始化的可用 Bean。 |
| 二级缓存 | earlySingletonObjects | HashMap<String, Object> | 半成品单例池:已实例化并提前曝光,但属性尚未填充完毕的早期 Bean。 |
| 三级缓存 | singletonFactories | HashMap<String, ObjectFactory<?>> | 单例工厂池:存放能够生产 Bean 早期引用的工厂 Lambda。 |
2. 循环依赖解决的完整时序图
Spring 三级缓存解决循环依赖调用时序
3. 灵魂拷问:两级缓存行不行?为什么必须要有第三级缓存?
- 没有 AOP 时,两级缓存完全够用:如果系统里只有普通的 Java Bean,实例化后直接把这个半成品对象塞进二级缓存,B 需要时直接拿,完全不需要第三级工厂缓存。
- 第三级缓存的存在,完全是为了优雅地解决“AOP 代理对象的循环依赖”:
- 按照 Spring 的规范设计,AOP 动态代理对象必须在 Bean 经历完属性填充和初始化之后的最后一步(
postProcessAfterInitialization)才生成。 - 如果只有两级缓存,就意味着 Spring 必须在对象刚刚
new出来的一瞬间,盲目地为所有可能被代理的类提前创建 AOP 代理对象并放入二级缓存,这严重破坏了 Bean 的生命周期原则。 - 第三级缓存的精妙之处在于“延迟计算(Lazy Evaluation)”:三级缓存存放的是一个 Lambda 工厂(
() -> getEarlyBeanReference(...))。如果没有任何循环依赖,这个工厂根本不会被调用,A 依然在最后一步正常生成 AOP 代理;只有当真正发生循环依赖时,B 才会触发该工厂提前把 A 的 AOP 代理生成出来,并缓存在二级缓存中防重!
- 按照 Spring 的规范设计,AOP 动态代理对象必须在 Bean 经历完属性填充和初始化之后的最后一步(
4. 哪些循环依赖无法被 Spring 解决?
- 基于构造器注入的循环依赖:如
A(B b)与B(A a)。- 原因:构造器注入发生在“实例化(new)”阶段,A 还没能完成实例化,根本无法把自己的工厂放入三级缓存,导致直接报
BeanCurrentlyInCreationException。 - 解决:在构造器参数前加
@Lazy注解(延迟注入代理对象)。
- 原因:构造器注入发生在“实例化(new)”阶段,A 还没能完成实例化,根本无法把自己的工厂放入三级缓存,导致直接报
- 多例(
@Scope("prototype"))的循环依赖:Spring 不对 prototype Bean 进行缓存管理,直接抛异常。
三、AOP 动态代理与切面织入
- 核心概念:
- JoinPoint(连接点):允许插入切面的方法调用点。
- Pointcut(切点):匹配具体哪些方法需要被拦截的表达式(如
@annotation(...)或execution(...))。 - Advice(通知):拦截后执行的具体逻辑(
@Before,@After,@AfterReturning,@AfterThrowing,@Around)。 - Aspect(切面):切点与通知的结合体。
- 底层执行链:Spring AOP 将目标方法的多个切面通知组装成一个拦截器链(
MethodInterceptor[]),利用**责任链模式(Chain of Responsibility)**逐层递归推进,最终调用目标方法。
四、Spring 声明式事务(@Transactional)失效的 8 大陷阱
声明式事务基于 AOP 动态代理实现。任何绕过了代理对象的调用或未按约定抛出的异常,都会导致事务静默失效,是生产中最危险的致命隐患!
1. 同类内部方法自调用(最常见!)
@Service
public class OrderService {
public void createOrder() {
// 直接调用内部方法,实际调用的是 this.doSave(),而不是代理对象的 doSave()!
this.doSave();
}
@Transactional
public void doSave() {
// 数据库操作... 此处的 @Transactional 彻底失效!
}
}- 失效原理:外部调用的是
OrderService的未加事务的createOrder,内部的this.doSave()调用的是目标对象自身的方法,根本没有经过 Spring 生成的 AOP 代理类,事务切面完全未被触发! - 正确解法:
- 解法一:将
doSave抽离到独立的外部 Service 中注入调用。 - 解法二:在当前类中自己注入自己(Spring 支持循环依赖)。
- 解法三:使用
((OrderService) AopContext.currentProxy()).doSave()强行获取当前线程的代理对象。
- 解法一:将
2. 异常被内部 try-catch 吞掉
@Transactional
public void pay() {
try {
orderDao.updateStatus();
accountDao.deductBalance(); // 发生异常
} catch (Exception e) {
log.error("支付失败", e); // 吞掉了异常,没有往外抛!
}
}- 失效原理:Spring 事务切面必须捕获到抛出的异常才能触发
rollback。业务代码自行把异常吃掉并不向外抛出,事务切面认为方法正常结束,会正常执行 commit 提交!
3. 抛出了受检异常(Checked Exception)未配置 rollbackFor
// 默认情况下,抛出 FileNotFoundException 或 SQLException 不会回滚!
@Transactional
public void importData() throws Exception {
userDao.insert();
throw new IOException("文件读取失败");
}- 失效原理:Spring 默认只在捕获到
RuntimeException或Error时才执行回滚。受检异常(Exception及其子类)默认直接 commit! - 生产铁律:所有
@Transactional注解必须显式指定rollbackFor = Exception.class!
4. 方法不是 public 的
- Spring 事务切面在扫描方法时,源码中通过
Modifier.isPublic(method.getModifiers())做了拦截判断,非public方法(如private,protected)上标注@Transactional直接被忽略。
5. 事务传播机制配置错误
- 如配置了
Propagation.NOT_SUPPORTED(以非事务方式运行,若有事务则挂起)或Propagation.NEVER。
6. 底层数据库引擎不支持事务
- 如 MySQL 表使用了 MyISAM 引擎而不是 InnoDB。
7. 多线程并发调用
- 开启新子线程操作数据库,子线程绑定的是新的数据库连接,脱离了主线程的事务上下文与本地连接绑定(
ThreadLocal),无法纳入同一个事务回滚。
8. 类没有被 Spring 容器扫描管理
- 开发者忘记在类上添加
@Service或@Component,对象是开发者手动new出来的,脱离了 Spring 容器生态。
五、Spring Boot 自动装配原理(Auto-Configuration)
┌─────────────────────────────────────────────────────────────┐
│ @SpringBootApplication │
└──────────────────────────────┬──────────────────────────────┘
│ 核心组合注解之一
▼
┌─────────────────────────────────────────────────────────────┐
│ @EnableAutoConfiguration │
└──────────────────────────────┬──────────────────────────────┘
│ 借助 @Import 导入核心选择器
▼
┌─────────────────────────────────────────────────────────────┐
│ AutoConfigurationImportSelector │
└──────────────────────────────┬──────────────────────────────┘
│ 扫描加载配置文件
▼
┌─────────────────────────────────────────────────────────────┐
│ 扫描各个依赖 jar 包中的: │
│ META-INF/spring.factories (Spring Boot 2.x) │
│ META-INF/spring/org.springframework.boot.autoconfigure... │
│ ...AutoConfiguration.imports (Spring Boot 3.x) │
└──────────────────────────────┬──────────────────────────────┘
│ 获取所有的 XxxAutoConfiguration 候选配置类
▼
┌─────────────────────────────────────────────────────────────┐
│ 结合条件注解过滤生效: │
│ @ConditionalOnClass (类路径下存在某类才装配,如 RedisClient) │
│ @ConditionalOnMissingBean (用户未自定义时才装配默认 Bean) │
│ @ConditionalOnProperty (配置文件开启了对应开关才装配) │
└─────────────────────────────────────────────────────────────┘- 一句话总结:Spring Boot 在启动时,通过
AutoConfigurationImportSelector扫描所有依赖 jar 包下特定配置路径中的自动配置类全限定名,利用各种@ConditionalOnXxx条件注解进行动态过滤,只有当所需类在 Classpath 且用户没有自定义时,才将默认的组件注入 IoC 容器。
六、校招高频面试追问
- Spring 中 Bean 的作用域(Scope)有哪些?
singleton(默认):单例,整个 IoC 容器中只存在一个共享实例。prototype:多例,每次注入或getBean()都创建一个全新的实例。- Web 环境下作用域:
request(每个 HTTP 请求一个实例)、session(每个 HTTP Session 一个实例)、application(整个 ServletContext 共享)。
- Spring 单例 Bean 是线程安全的吗?
- 不是! Spring 容器不对单例 Bean 的并发访问做任何线程同步保证。
- 如果单例 Bean 是无状态的(如大部分只包含逻辑调用的 Service/Controller),多线程只读不写,是线程安全的;若含有共享可变成员变量(有状态),在多线程下会产生并发写冲突,需使用
ThreadLocal、加锁或改为 prototype 作用域。
@Autowired和@Resource有什么区别?@Autowired:Spring 原生注解,**默认优先按类型(byType)**匹配注入。若存在多个同类型 Bean,配合@Qualifier("name")指定名称。@Resource:JDK 规范注解(JSR-250),**默认优先按名称(byName)**匹配注入。当找不到名称匹配时才退化按类型匹配。
- Spring 事务传播行为中
REQUIRED和REQUIRES_NEW的区别?PROPAGATION_REQUIRED(默认):若当前已存在事务,则加入该事务;若不存在事务,则新建一个事务。外层事务回滚会导致内层一同回滚。PROPAGATION_REQUIRES_NEW:无论当前是否存在事务,都挂起当前事务并新建一个独立事务执行。内层事务独立提交或回滚,内层的成功不会受外层后续回滚的影响。常用于独立的操作日志记录或审计留痕。