Skip to content

Spring Boot 核心与事务

IoC 容器与 Bean 生命周期、三级缓存解决循环依赖底层时序、AOP 动态代理织入、声明式事务失效场景与自动装配原理。

Updated View as Markdown
For humans

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. 三级缓存的定义与分工

缓存层级变量名底层数据结构存放内容与状态
一级缓存singletonObjectsConcurrentHashMap<String, Object>成品单例池:已经经历过完整实例化、属性填充、初始化的可用 Bean。
二级缓存earlySingletonObjectsHashMap<String, Object>半成品单例池:已实例化并提前曝光,但属性尚未填充完毕的早期 Bean。
三级缓存singletonFactoriesHashMap<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 代理生成出来,并缓存在二级缓存中防重!

4. 哪些循环依赖无法被 Spring 解决?

  • 基于构造器注入的循环依赖:如 A(B b) 与 B(A a)。
    • 原因:构造器注入发生在“实例化(new)”阶段,A 还没能完成实例化,根本无法把自己的工厂放入三级缓存,导致直接报 BeanCurrentlyInCreationException。
    • 解决:在构造器参数前加 @Lazy 注解(延迟注入代理对象)。
  • 多例(@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 容器。

六、校招高频面试追问

  1. Spring 中 Bean 的作用域(Scope)有哪些?
    • singleton(默认):单例,整个 IoC 容器中只存在一个共享实例。
    • prototype:多例,每次注入或 getBean() 都创建一个全新的实例。
    • Web 环境下作用域:request(每个 HTTP 请求一个实例)、session(每个 HTTP Session 一个实例)、application(整个 ServletContext 共享)。
  2. Spring 单例 Bean 是线程安全的吗?
    • 不是! Spring 容器不对单例 Bean 的并发访问做任何线程同步保证。
    • 如果单例 Bean 是无状态的(如大部分只包含逻辑调用的 Service/Controller),多线程只读不写,是线程安全的;若含有共享可变成员变量(有状态),在多线程下会产生并发写冲突,需使用 ThreadLocal、加锁或改为 prototype 作用域。
  3. @Autowired 和 @Resource 有什么区别?
    • @Autowired:Spring 原生注解,**默认优先按类型(byType)**匹配注入。若存在多个同类型 Bean,配合 @Qualifier("name") 指定名称。
    • @Resource:JDK 规范注解(JSR-250),**默认优先按名称(byName)**匹配注入。当找不到名称匹配时才退化按类型匹配。
  4. Spring 事务传播行为中 REQUIRED 和 REQUIRES_NEW 的区别?
    • PROPAGATION_REQUIRED(默认):若当前已存在事务,则加入该事务;若不存在事务,则新建一个事务。外层事务回滚会导致内层一同回滚。
    • PROPAGATION_REQUIRES_NEW:无论当前是否存在事务,都挂起当前事务并新建一个独立事务执行。内层事务独立提交或回滚,内层的成功不会受外层后续回滚的影响。常用于独立的操作日志记录或审计留痕。
Navigation

Type to search…

↑↓ navigate↵ selectEsc close