Spring 用久了容易变成「会配注解,不懂原理」。一旦出现 BeanCurrentlyInCreationException 或「事务没回滚」,不懂原理就只能靠试。这篇把这几块串起来。
一、IOC:容器里到底有什么
IOC 的本质是把对象的创建与依赖组装交给容器。ApplicationContext 启动时做的事:
- 读取配置(XML / 注解 / Java Config)→ 生成
BeanDefinition BeanFactoryPostProcessor修改定义(如PropertySourcesPlaceholderConfigurer解析${})- 实例化 Bean(反射
newInstance) - 属性注入(
@Autowired/ setter) BeanPostProcessor前后增强(AOP 代理就是在这里生成的)- 初始化(
@PostConstruct→InitializingBean→init-method) - 放入单例池,容器就绪
二、循环依赖与三级缓存
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: 前缀 |
七、几条工程建议
- 构造器注入优先(配合
final字段):依赖不可变、启动期就暴露循环依赖、方便单测。 - 事务方法要短:事务里不要做 RPC、发短信、写文件。这些耗时操作会拉长事务,占用数据库连接。正确做法是「事务内只改库,提交后发 MQ 触发外部动作」。
@Transactional显式写rollbackFor = Exception.class,别依赖默认值。- 不要在大方法上加事务,尤其是循环处理批量数据的场景 —— 要么拆成小事务,要么改成「批量 + 补偿」。
一起聊聊