一文讲透 Spring AOP、Bean 作用域与事务:从代理机制到事务失效
Spring AOP、Bean 作用域和事务看似是三个知识点,实际上都与 Spring 容器和代理机制密切相关:容器管理 Bean 的生命周期,AOP 为 Bean 创建代理,声明式事务则是 AOP 的典型应用。
一、Spring AOP 是什么
AOP,即面向切面编程,用于抽取日志、权限、事务、监控等分散在多个业务类中的横切逻辑,使业务方法只关注自身功能。
Spring AOP 的核心概念如下:
连接点(Join Point):程序执行过程中可以被增强的位置。在 Spring AOP 中主要表示方法执行。
切点(Pointcut):用于筛选连接点,决定哪些方法需要增强。
通知(Advice):切点命中后要执行的动作,包括
@Before、@After、@AfterReturning、@AfterThrowing和@Around。切面(Aspect):切点与通知的组合,表示“在哪里执行什么增强逻辑”。
目标对象(Target):真正执行业务逻辑的对象。
代理对象(Proxy):Spring 创建的包装对象,负责在调用目标方法前后执行通知。
下面是一个简单的日志切面:
@Aspect @Component public class LogAspect { @Pointcut("execution(* com.example.service..*(..))") public void serviceMethods() {} @Around("serviceMethods()") public Object record(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); try { return joinPoint.proceed(); } finally { System.out.println("耗时:" + (System.currentTimeMillis() - start)); } } }execution表达式是切点,表示匹配service包及其子包中的所有方法;@Around是环绕通知;两者共同组成切面。
二、Spring AOP 的具体实现
Spring 容器创建 Bean 时,自动代理创建器会判断该 Bean 的方法是否被切点匹配。若需要增强,Spring 不直接返回原对象,而是返回代理对象。
调用过程可以简化为:
调用代理对象 → 匹配切点 → 执行通知链 → 调用目标方法 → 执行后续通知 → 返回结果目标类实现接口时,Spring Framework 通常可以使用 JDK 动态代理;没有接口时使用 CGLIB 创建目标类的子类,也可以通过配置强制使用 CGLIB。由于 CGLIB 依赖继承和方法重写,final类、final方法和private方法不能按普通方式被增强。
Spring AOP 属于运行时代理,只能拦截经过代理对象的方法调用。理解这一点,是理解事务自调用失效的关键。
三、Spring Bean 的作用域
Spring 常用 Bean 作用域包括:
| 作用域 | 含义 |
|---|---|
singleton | 默认值,一个容器中同一 Bean 定义共享一个实例 |
prototype | 每次向容器请求时创建新实例 |
request | 每个 HTTP 请求一个实例 |
session | 每个 HTTP Session 一个实例 |
application | 每个ServletContext一个实例 |
websocket | 每个 WebSocket 会话一个实例 |
singleton只保证容器内共享同一实例,并不保证线程安全。无共享可变状态的 Service 通常可以安全地使用单例;如果单例 Bean 保存会被多个线程修改的成员变量,就可能产生数据竞争。
四、prototype 是否为了解决线程安全
prototype每次从容器获取时都会创建新对象,适合保存独立状态的 Bean。不同线程如果各自获得不同实例,共享状态确实会减少,因此它可以用于部分有状态场景。
但不能简单地说“prototype 就是为了解决 singleton 线程不安全而出现的”,原因有三点:
它解决的是实例创建和状态隔离问题,不保证 Bean 内部使用的数据库、静态变量等共享资源安全;
Spring 创建 prototype Bean 后通常不再负责其完整销毁流程;
prototype Bean 直接注入 singleton Bean 时,依赖通常只在单例创建时解析一次,之后可能一直使用同一个 prototype 实例。
如果单例每次调用都需要新的 prototype 实例,应使用ObjectProvider、方法注入或作用域代理。更推荐的设计仍是让 Service 保持无状态,将请求数据放在参数和局部变量中;必要时再使用原子类、并发容器或锁。
五、Spring 事务与 ACID
Spring 通过PlatformTransactionManager统一抽象 JDBC、JPA 等事务技术,并提供编程式事务和声明式事务。实际开发最常见的是使用@Transactional的声明式事务。
数据库事务具有 ACID 特性:
原子性(Atomicity):事务中的操作全部成功或全部回滚;
一致性(Consistency):事务执行前后数据满足业务约束;
隔离性(Isolation):并发事务之间相互隔离;
持久性(Durability):事务提交后结果能够持久保存。
Spring 本身不是数据库,不直接实现 ACID。它负责划定事务边界、选择事务管理器并调用提交或回滚,真正的 ACID 主要由数据库事务机制保障。
@Transactional的底层也是 AOP。外部调用事务代理时,事务拦截器先开启或加入事务,再执行目标方法;方法正常结束就提交,出现符合规则的异常则回滚。默认情况下,未捕获的RuntimeException和Error会触发回滚,受检异常默认不会。
六、A 调用 B,B 的事务会生效吗
假设同一个类中存在两个方法:
@Service public class OrderService { public void methodA() { methodB(); } @Transactional public void methodB() { // 数据库操作 } }在 Spring 默认代理模式下,外部调用代理对象的methodA(),代理发现 A 没有事务注解,于是直接调用目标对象。A 内部调用 B,本质是:
this.methodB();这次调用发生在目标对象内部,没有重新经过 Spring 代理,所以 B 上的@Transactional通常不会生效。这就是事务的自调用失效。
需要注意:如果 A 本身已经开启事务,并采用默认的REQUIRED传播行为,B 中的数据库操作仍会运行在 A 已有的事务里;但这不是 B 的注解通过自调用生效,而是外层事务本来就存在。
七、如何解决事务自调用
常见方案有三种:
拆分类职责:把 B 移到另一个 Spring Bean,由 A 注入并调用它。这是最清晰、最推荐的方式。
通过代理调用:注入自身代理或从容器获取代理后调用,但会增加代码与容器的耦合,应谨慎使用。
使用 AspectJ 模式:通过编译期或加载期织入直接修改字节码,不依赖代理调用,因此能处理自调用,但配置更复杂。
也可以将@Transactional放到由外部调用的 A 上,让 A 统一作为事务边界,但要确认这符合业务原子性和传播要求。
八、总结
Spring AOP 通过切点选中方法,通过通知定义增强逻辑,并以 JDK 动态代理或 CGLIB 创建代理对象。Bean 作用域决定实例的创建和共享范围,prototype适合有状态对象,但不能代替正确的并发设计。Spring 声明式事务是 AOP 的典型应用,数据库负责 ACID,Spring 负责事务边界和提交回滚。
参考资料:Spring AOP 概念、Spring Bean 作用域、Spring 事务注解。