Spring 全家桶 列表全部文章
Spring 全家桶

Spring 核心笔记:IOC、AOP、事务失效的六种场景

把 Spring 的 IOC 容器、Bean 生命周期、循环依赖、AOP 原理和声明式事务讲清楚,重点整理 @Transactional 不生效的六种真实场景(自调用、非 public、异常被吞、数据库不支持、传播行为、多线程)以及 Bean 注入相关的常见报错。

更多

Spring 用久了容易变成「会配注解,不懂原理」。一旦出现 BeanCurrentlyInCreationException 或「事务没回滚」,不懂原理就只能靠试。这篇把这几块串起来。

一、IOC:容器里到底有什么

IOC 的本质是把对象的创建与依赖组装交给容器。ApplicationContext 启动时做的事:

  1. 读取配置(XML / 注解 / Java Config)→ 生成 BeanDefinition
  2. BeanFactoryPostProcessor 修改定义(如 PropertySourcesPlaceholderConfigurer 解析 ${})
  3. 实例化 Bean(反射 newInstance)
  4. 属性注入(@Autowired / setter)
  5. BeanPostProcessor 前后增强(AOP 代理就是在这里生成的)
  6. 初始化(@PostConstruct → InitializingBean → init-method)
  7. 放入单例池,容器就绪

实例化 属性注入 AOP 代理 BeanPostProcessor @PostConstruct 可用 销毁 注意:代理是在「属性注入之后」才生成的 —— 这解释了为什么构造器注入能天然规避循环依赖。

二、循环依赖与三级缓存

Spring 用三级缓存解决 setter/字段注入的循环依赖:

缓存 存什么
一级 singletonObjects 成品 Bean
二级 earlySingletonObjects 半成品(提前暴露的引用,已实例化未初始化)
三级 singletonFactories 工厂,能生成代理对象

构造器注入的循环依赖无法解决,会直接抛 BeanCurrentlyInCreationException。这也是 Spring 官方推荐构造器注入的原因之一 —— 它让问题在启动期暴露,而不是运行期。

真出现循环依赖,优先做的是拆服务(把公共逻辑抽到第三个类),而不是加 @Lazy。@Lazy 只是把问题藏起来。

三、AOP:代理是怎么生效的

AOP 默认用 JDK 动态代理(目标类实现了接口)或 CGLIB(无接口,或 proxyTargetClass=true)。

五种通知的执行顺序(Spring 5):

@Around 前 → @Before → 目标方法 → @Around 后 → @AfterReturning / @AfterThrowing → @After

常用场景我基本只用三种:

  • @Around:日志、耗时统计、幂等、权限 —— 因为它能控制「是否执行」和「返回值」。
  • @AfterThrowing:异常告警。
  • @Around + 自定义注解:做操作日志、分布式锁、数据权限这类横切逻辑。
@Around("@annotation(rateLimit)")
public Object limit(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
    String key = pjp.getSignature().toShortString();
    if (!limiter.tryAcquire(key)) throw new BizException("操作过于频繁");
    return pjp.proceed();      // ★ 忘了 proceed 目标方法就不执行了
}

四、@Transactional 不生效的六种场景

这是面试八股,但每一种我在生产上都真见过。

# 场景 原因 解法
1 同类内部方法调用 没走代理,this.xxx() 是直接调用 注入自己 / 拆到另一个类 / AopContext.currentProxy()
2 方法不是 public JDK 代理和 CGLIB 都不代理非 public 方法 改 public
3 异常被 catch 吞了 事务切面靠异常触发回滚 重新抛出,或 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
4 抛的是受检异常但没指定 rollbackFor 默认只在 RuntimeException / Error 时回滚 @Transactional(rollbackFor = Exception.class)
5 数据库引擎不支持事务 MySQL 的 MyISAM 不支持 改 InnoDB
6 新开线程执行 事务绑定在 ThreadLocal 的 Connection 上,子线程拿不到 异步任务自己开事务,或改同步

第 1 条最隐蔽。例子:

@Service
public class OrderService {
    public void batchCreate(List<Order> orders) {
        for (Order o : orders) {
            this.createOne(o);   // ★ 自调用,@Transactional 完全不生效
        }
    }
    @Transactional
    public void createOne(Order o) { ... }
}

改法:@Autowired 注入自己的代理(OrderService self),用 self.createOne(o);或者把 createOne 拆到独立的 OrderCreator 类。我更推荐拆类,因为循环依赖风险小、语义也清晰。

五、事务传播行为(只记常用的)

传播行为 含义 典型用法
REQUIRED(默认) 有就加入,没有就新建 绝大多数场景
REQUIRES_NEW 挂起当前,新开一个事务 操作日志必须落库,即使主业务回滚
NESTED 嵌套事务,靠 savepoint 部分回滚
SUPPORTS 有就用,没有就算了 查询方法
NOT_SUPPORTED 挂起事务执行 大批量只读导出,不想占事务

日志用 REQUIRES_NEW 是刚需:主流程失败回滚了,但「谁在什么时候尝试做了什么」必须留痕。

六、常见报错速查

报错 原因 处理
NoSuchBeanDefinitionException 没扫到 / 没加注解 检查包扫描路径、@Component
NoUniqueBeanDefinitionException 多个实现 @Qualifier 或 @Primary
BeanCurrentlyInCreationException 构造器循环依赖 拆类 / 改字段注入 / @Lazy
BeanCreationException: Injection of autowired dependencies failed 依赖的 Bean 创建失败 看 Caused by,根因在下面
AopConfigException: Could not generate CGLIB subclass 类是 final 或方法 final 去掉 final / 抽接口
FileNotFoundException: class path resource [xxx] cannot be opened 资源路径写错 classpath: 前缀

七、几条工程建议

  1. 构造器注入优先(配合 final 字段):依赖不可变、启动期就暴露循环依赖、方便单测。
  2. 事务方法要短:事务里不要做 RPC、发短信、写文件。这些耗时操作会拉长事务,占用数据库连接。正确做法是「事务内只改库,提交后发 MQ 触发外部动作」。
  3. @Transactional 显式写 rollbackFor = Exception.class,别依赖默认值。
  4. 不要在大方法上加事务,尤其是循环处理批量数据的场景 —— 要么拆成小事务,要么改成「批量 + 补偿」。
— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人