Spring AOP和事物
目录
1AOP简介
2AOP入门案例
3AOP工作流程
4AOP切入点表达式
5AOP通知类型
问题:
6案例-业务层接口执行效率
7AOP通知获取数据
8案例-百度网盘密码数据兼容处理
9AOP总结
10Spring事物简介
11Spring事物角色
12Spring事物属性
13总结
1AOP简介





2AOP入门案例

SpringConfig要加注解@EnableAspectJAutoProxy(这个是启动@Aspect注解的)








3AOP工作流程




切入点和通知能对应上,就使用代理对象,对应不上,就使用目标对象
4AOP切入点表达式






5AOP通知类型








问题:
环绕通知需要使用.proceed()调用原方法,要不然分不清哪是前,哪是后,另外调用这个proceed有返回值,需要返回值类型
6案例-业务层接口执行效率




7AOP通知获取数据





8案例-百度网盘密码数据兼容处理



9AOP总结





10Spring事物简介






11Spring事物角色





12Spring事物属性






13总结
从 AOP 到 Spring 事务:面向切面与声明式事务的深度解析
前言:为什么要学习 AOP 和 Spring 事务?
在 Java 开发领域,AOP(面向切面编程) 和 Spring 事务 是两大核心技术基石。AOP 让我们能够以 “非侵入式” 的方式为业务代码添加通用功能(如日志、权限、事务),彻底告别了 “重复粘贴相同逻辑” 的繁琐;而 Spring 事务则是企业级应用数据一致性的保障,让复杂的事务管理变得 “声明式、零入侵”。
对于开发者而言,掌握 AOP 能大幅提升代码的可维护性和扩展性,掌握 Spring 事务能确保系统在 “增删改” 数据时的安全性和一致性。本文将从概念到实战,从原理到应用,全方位解析 AOP 和 Spring 事务的核心知识点,让你既能 “知其然”,更能 “知其所以然”。
一、AOP 简介:面向切面编程的核心概念
1. AOP 的定义与核心价值
AOP(Aspect-Oriented Programming,面向切面编程)是一种 **“横向抽取”** 编程思想:它将分散在业务逻辑中的通用功能(如日志、事务、权限) 抽取出来,形成独立的 “切面(Aspect)”,再通过 “织入” 机制将切面动态嵌入到业务代码的指定位置(如方法执行前、执行后)。
其核心价值在于 **“解耦”** —— 业务代码只需关注核心逻辑,通用功能由切面统一管理,避免了 “一处修改,多处粘贴” 的维护噩梦。
2. AOP 的核心术语
理解 AOP 需先掌握以下术语,它们是 AOP 技术的 “语法基础”:
连接点(Join Point):程序中所有可被 AOP 拦截的 “候选位置”,如方法调用、方法执行、字段赋值等。在 Spring AOP 中,连接点主要是 **“方法执行”**(简化了 AOP 的使用范围)。
切入点(Pointcut):从连接点中筛选出的、实际需要增强的位置,通过 “切入点表达式” 定义(如匹配所有Service层的方法)。
通知(Advice):切面中具体的增强逻辑,如 “方法执行前打印日志”“方法执行后提交事务”。Spring AOP 支持 5 种通知类型(后文详解)。
切面(Aspect):切入点 + 通知的组合体,是 AOP 的 “最小单元”,代表 “在哪些位置执行哪些增强逻辑”。
织入(Weaving):将切面动态嵌入到业务代码的过程。Spring AOP 在运行时通过代理实现织入,AspectJ 则支持编译期 / 类加载期织入。
目标对象(Target Object):被切面增强的业务对象(如UserService的实例)。
代理对象(Proxy Object):Spring 为目标对象生成的代理实例,实际执行时是代理对象在 “包裹” 目标对象,并嵌入切面逻辑。
3. AOP 的实现方式对比
目前主流的 AOP 实现有Spring AOP和AspectJ,二者差异显著:
维度 Spring AOP AspectJ
实现原理 基于动态代理(JDK/CGLIB) 基于字节码编织(编译期 / 类加载期修改字节码)
侵入性 无侵入(基于代理,不修改源码) 有侵入(需使用 AspectJ 语法,编译时修改字节码)
功能范围 仅支持方法级别的连接点 支持方法、字段、构造器等全类型连接点
性能 运行时代理,略低于 AspectJ 编译期织入,性能更优
使用场景 大多数业务场景(如日志、事务) 需深度字节码增强的场景(如框架底层)
Spring AOP 是日常开发的首选,它足够简洁且无侵入;AspectJ 则适合对性能和功能有极致要求的场景。
二、AOP 入门案例:快速体验切面增强
理论再多不如动手实践,我们通过一个 “日志记录” 的入门案例,快速体验 AOP 的核心流程。
1. 环境准备
技术栈:Spring Boot 2.7.x + Spring AOP + Lombok
依赖引入(Maven):
xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. 业务代码编写
创建一个简单的UserService,包含用户新增和查询方法:
java
运行
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class UserService {
public void addUser(String username) {
log.info("执行addUser方法,新增用户:{}", username);
// 模拟业务逻辑
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public String getUser(Long id) {
log.info("执行getUser方法,查询用户ID:{}", id);
return "用户" + id;
}
}
3. 切面类编写
创建LogAspect类,实现 “方法执行前后打印日志” 的增强逻辑:
java
运行
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogAspect {
// 切入点:匹配UserService中的所有方法
@Pointcut("execution(* com.example.demo.service.UserService.*(..))")
public void userServicePointcut() {}
// 前置通知:方法执行前执行
@Before("userServicePointcut()")
public void beforeAdvice() {
System.out.println("===== 前置通知:方法即将执行 =====");
}
// 后置通知:方法执行后执行(无论是否异常)
@After("userServicePointcut()")
public void afterAdvice() {
System.out.println("===== 后置通知:方法已执行 =====");
}
// 返回通知:方法返回结果后执行
@AfterReturning("userServicePointcut()")
public void afterReturningAdvice() {
System.out.println("===== 返回通知:方法已返回结果 =====");
}
// 异常通知:方法抛出异常后执行
@AfterThrowing("userServicePointcut()")
public void afterThrowingAdvice() {
System.out.println("===== 异常通知:方法抛出异常 =====");
}
// 环绕通知:方法执行前后均可增强,且可控制方法执行流程
@Around("userServicePointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("===== 环绕通知:方法执行前 =====");
// 执行目标方法
Object result = joinPoint.proceed();
System.out.println("===== 环绕通知:方法执行后,返回结果:" + result + " =====");
return result;
}
}
4. 测试与结果分析
编写测试类,调用UserService的方法:
java
运行
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class AopDemoApplicationTests {
@Autowired
private UserService userService;
@Test
void testAop() {
userService.addUser("张三");
System.out.println("---------------------");
userService.getUser(1L);
}
}
运行测试后,控制台输出如下(以addUser方法为例):
plaintext
===== 环绕通知:方法执行前 =====
===== 前置通知:方法即将执行 =====
[main] INFO com.example.demo.service.UserService - 执行addUser方法,新增用户:张三
===== 环绕通知:方法执行后,返回结果:null =====
===== 后置通知:方法已执行 =====
===== 返回通知:方法已返回结果 =====
---------------------
...(getUser方法的增强日志同理)
通知执行顺序:环绕通知(前)→ 前置通知 → 目标方法执行 → 环绕通知(后)→ 后置通知 → 返回通知(或异常通知)。
这个案例清晰展示了 AOP 的核心流程:通过切入点匹配目标方法,通过各种通知类型在方法执行的不同阶段嵌入增强逻辑,最终实现了 “业务代码无侵入,通用逻辑可复用” 的效果。
三、AOP 工作流程:从代理到织入的底层逻辑
要真正掌握 AOP,必须理解其 “代理创建” 和 “通知织入” 的底层流程。以 Spring AOP 为例,其核心是 **“动态代理 + 通知链执行”**。
1. Spring AOP 的代理创建流程
当 Spring 容器启动时,会对所有 Bean 进行 “是否需要 AOP 增强” 的判断,若需要则为其创建代理对象,流程如下:
扫描切面类:Spring 会扫描所有标注@Aspect的类,解析其中的切入点和通知。
匹配目标 Bean:对于每个业务 Bean(如UserService),检查其方法是否匹配任何切入点表达式。
选择代理方式:
若目标类实现了接口,则使用JDK 动态代理(基于接口生成代理类);
若目标类未实现接口,则使用CGLIB 动态代理(基于字节码生成子类)。
生成代理对象:代理对象会 “包裹” 目标对象,并在调用目标方法时,嵌入切面的通知逻辑。
2. 通知的执行流程(以环绕通知为例)
当调用代理对象的方法时,通知的执行流程可拆解为以下步骤:
是
否
是
否
调用代理对象方法
匹配切入点?
执行环绕通知前逻辑
执行前置通知
调用目标对象方法
方法是否异常?
执行返回通知
执行异常通知
执行环绕通知后逻辑
执行后置通知
返回结果
核心细节:
环绕通知的ProceedingJoinPoint.proceed()方法是 “目标方法执行” 的触发点,若不调用此方法,目标方法将不会执行;
前置通知、后置通知等是由 Spring 在proceed()前后自动触发的,开发者只需关注逻辑编写;
异常通知仅在目标方法抛出异常时执行,返回通知仅在方法正常返回时执行。
3. 切入点表达式的匹配原理
切入点表达式(如execution(* com.example.service.*Service.*(..)))的匹配过程,是 AOP 性能的关键:
语法解析:Spring 将切入点表达式解析为 “包路径、类名、方法名、参数” 等维度的匹配规则。
层级缓存:为了提升性能,Spring 会对切入点表达式进行 “层级缓存”,例如先匹配包路径,再匹配类名,最后匹配方法名和参数。
运行时匹配:当调用代理方法时,Spring 会根据缓存的规则快速判断该方法是否匹配切入点,从而决定是否执行通知逻辑。
理解这一流程,有助于我们写出更高效的切入点表达式(如避免过于宽泛的*..*写法,尽量精确到包和类)。
四、AOP 切入点表达式:精准匹配增强位置
切入点表达式是 AOP 的 “定位器”,它决定了哪些方法会被切面增强。Spring AOP 支持多种表达式语法,最核心的是 **execution表达式 **,此外还有within、this、target、@annotation等辅助表达式。
1. 核心execution表达式语法
execution表达式的完整语法为:
plaintext
execution([修饰符] 返回值类型 包路径.类名.方法名(参数列表) [异常类型])
其中返回值类型、包路径。类名。方法名、参数列表是必填项,其余为可选。
(1)返回值类型
*:匹配任意返回值类型(如void、String、User等);
具体类型:如java.lang.String,仅匹配返回值为String的方法。
(2)包路径与类名
com.example.service:匹配com.example.service包下的所有类;
com.example.service..:匹配com.example.service包及其所有子包下的类;
*Service:匹配类名以Service结尾的类(如UserService、OrderService);
UserService+:匹配UserService类及其所有子类 / 实现类(若UserService是接口,还包括所有实现类)。
(3)方法名
*:匹配任意方法名;
add*:匹配以add开头的方法(如addUser、addOrder);
*User:匹配以User结尾的方法(如getUser、deleteUser)。
(4)参数列表
():匹配无参数的方法;
(..):匹配任意数量、任意类型的参数;
(*):匹配一个参数,类型任意;
(java.lang.String):匹配一个参数,类型为String;
(java.lang.String, ..):匹配第一个参数为String,后续参数任意数量、任意类型。
(5)完整示例
java
运行
// 示例1:匹配com.example.service包下UserService类的所有方法,返回值任意,参数任意
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServicePointcut() {}
// 示例2:匹配com.example.service包及其子包下,类名以Service结尾的类中,
// 以add开头、返回值为void、第一个参数为String的方法
@Pointcut("execution(void com.example.service..*Service.add*(java.lang.String, ..))")
public void addServicePointcut() {}
2. 其他常用切入点表达式
除了execution,以下表达式在特定场景下非常实用:
(1)within:匹配类或包下的所有方法
java
运行
// 匹配com.example.service包下的所有类的所有方法
@Pointcut("within(com.example.service.*)")
public void servicePointcut() {}
// 匹配UserService类的所有方法
@Pointcut("within(com.example.service.UserService)")
public void userServicePointcut() {}
(2)this:匹配实现了特定接口的代理对象
java
运行
// 匹配实现了UserService接口的代理对象的所有方法
@Pointcut("this(com.example.service.UserService)")
public void userServicePointcut() {}
(3)target:匹配目标对象(而非代理对象)
java
运行
// 匹配目标对象是UserService类型的所有方法
@Pointcut("target(com.example.service.UserService)")
public void userServicePointcut() {}
(4)@annotation:匹配标注了特定注解的方法
java
运行
// 自定义一个@Log注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}
// 匹配所有标注了@Log注解的方法
@Pointcut("@annotation(com.example.demo.annotation.Log)")
public void logPointcut() {}
// 在业务方法上使用@Log
@Service
public class UserService {
@Log("新增用户")
public void addUser(String username) {
// ...
}
}
3. 切入点表达式的组合与优先级
多个切入点表达式可通过逻辑运算符(&&、||、!)组合,实现更灵活的匹配规则:
java
运行
// 匹配UserService类中,既标注了@Log注解,又返回值为String的方法
@Pointcut("execution(String com.example.service.UserService.*(..)) && @annotation(com.example.demo.annotation.Log)")
public void userServiceLogPointcut() {}
// 匹配UserService类中,方法名以add开头,或者参数数量大于2的方法
@Pointcut("execution(* com.example.service.UserService.add*(..)) || execution(* com.example.service.UserService.*(.., .., ..))")
public void userServiceAddOrMultiParamPointcut() {}
在组合表达式中,execution表达式的优先级最高,因为它是最精确的 “方法定位器”;其他表达式(如@annotation、within)通常用于辅助筛选。
五、AOP 通知类型:五种增强时机的实战解析
通知是 AOP 中 “增强逻辑的载体”,Spring AOP 支持五种通知类型,每种类型对应方法执行的不同阶段。理解它们的差异和适用场景,是写出精准切面的关键。
1. 前置通知(@Before)
执行时机:目标方法执行前执行。
适用场景:记录方法调用前的上下文信息(如请求参数、用户身份)、权限校验、参数校验等。
特点:无返回值,不能阻止目标方法执行(除非抛出异常)。
java
运行
@Before("userServicePointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
// 获取方法名
String methodName = joinPoint.getSignature().getName();
// 获取方法参数
Object[] args = joinPoint.getArgs();
System.out.println("前置通知:方法" + methodName + "即将执行,参数:" + Arrays.toString(args));
// 模拟权限校验,若不通过则抛出异常
if (args.length == 0) {
throw new IllegalArgumentException("参数不能为空");
}
}
2. 后置通知(@After)
执行时机:目标方法执行后执行(无论是否抛出异常)。
适用场景:资源释放(如关闭数据库连接、释放锁)、记录方法执行完成的通用日志。
特点:无返回值,无法获取目标方法的返回结果。
java
运行
@After("userServicePointcut()")
public void afterAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("后置通知:方法" + methodName + "已执行");
}
3. 返回通知(@AfterReturning)
执行时机:目标方法正常返回后执行(若方法抛出异常,则不执行)。
适用场景:记录方法返回结果、对返回结果进行二次处理(如加密、格式转换)。
特点:可通过returning属性获取目标方法的返回结果。
java
运行
@AfterReturning(value = "userServicePointcut()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("返回通知:方法" + methodName + "返回结果:" + result);
// 对返回结果进行二次处理(如将String转换为UpperCase)
if (result instanceof String) {
String upperResult = ((String) result).toUpperCase();
System.out.println("返回结果二次处理后:" + upperResult);
}
}
4. 异常通知(@AfterThrowing)
执行时机:目标方法抛出异常后执行(若方法正常返回,则不执行)。
适用场景:异常日志记录、异常信息封装(如转换为自定义异常)、发送异常告警。
特点:可通过throwing属性获取抛出的异常对象。
java
运行
@AfterThrowing(value = "userServicePointcut()", throwing = "ex")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
System.out.println("异常通知:方法" + methodName + "抛出异常,类型:" + ex.getClass().getName() + ",信息:" + ex.getMessage());
// 模拟发送异常告警邮件
sendAlertEmail(ex);
}
5. 环绕通知(@Around)
执行时机:目标方法执行前后均可执行,是最灵活的通知类型。
适用场景:方法执行时长统计、事务控制、缓存控制等需要 “包围” 方法执行的场景。
特点:需手动调用ProceedingJoinPoint.proceed()触发目标方法执行,可控制方法是否执行、修改参数、修改返回结果。
java
运行
@Around("userServicePointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 方法执行前逻辑:统计开始时间
long startTime = System.currentTimeMillis();
// 触发目标方法执行(可修改参数)
Object[] args = joinPoint.getArgs();
if (args.length > 0 && args[0] instanceof String) {
args[0] = ((String) args[0]).trim(); // 对第一个参数进行trim处理
}
Object result = joinPoint.proceed(args);
// 方法执行后逻辑:统计结束时间并计算耗时
long endTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
System.out.println("环绕通知:方法" + methodName + "执行耗时:" + (endTime - startTime) + "ms");
// 修改返回结果
if (result instanceof String) {
result = ((String) result) + "(已被环绕通知修改)";
}
return result;
}
6. 五种通知的执行顺序总结
当一个方法被多个通知增强时,执行顺序为:环绕通知(前)→ 前置通知 → 目标方法 → 环绕通知(后)→ 后置通知 → 返回通知 / 异常通知
若存在多个切面,执行顺序由@Order注解控制(@Order(1)优先级高于@Order(2)),同一切面内的通知顺序为:环绕(前)→ 前置 → 环绕(后)→ 后置 → 返回 / 异常。
六、案例 - 业务层接口执行效率:AOP 的性能监控实践
在实际项目中,“接口执行效率” 是性能优化的关键指标。我们可以通过 AOP 实现无侵入的接口耗时统计,为性能优化提供数据支撑。
1. 需求分析
统计所有Service层方法的执行时长;
对执行时长超过阈值(如 500ms)的方法进行告警;
将统计结果记录到日志或监控系统中。
2. 实现步骤
(1)自定义注解(可选,用于标记需要监控的方法)
java
运行
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PerformanceMonitor {
long threshold() default 500; // 耗时阈值,默认500ms
}
(2)切面类实现
java
运行
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Method;
@Aspect
@Component
@Slf4j
public class PerformanceMonitorAspect {
// 切入点:匹配所有Service层方法,或标注了@PerformanceMonitor的方法
@Pointcut("execution(* com.example.service..*Service.*(..)) || @annotation(com.example.demo.annotation.PerformanceMonitor)")
public void performancePointcut() {}
@Around("performancePointcut()")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法签名和方法对象
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String methodName = method.getDeclaringClass().getName() + "." + method.getName();
// 获取耗时阈值(优先从注解中获取,否则使用默认值)
PerformanceMonitor monitor = method.getAnnotation(PerformanceMonitor.class);
long threshold = monitor != null ? monitor.threshold() : 500;
// 统计开始时间
long startTime = System.currentTimeMillis();
try {
// 执行目标方法
Object result = joinPoint.proceed();
return result;
} finally {
// 统计结束时间并计算耗时
long endTime = System.currentTimeMillis();
long elapsedTime = endTime - startTime;
log.info("方法[{}]执行耗时:{}ms", methodName, elapsedTime);
// 耗时超过阈值,触发告警
if (elapsedTime > threshold) {
log.warn("警告:方法[{}]执行耗时超过阈值({}ms > {}ms),请排查性能问题!",
methodName, elapsedTime, threshold);
// 此处可添加告警逻辑,如发送邮件、钉钉消息等
}
}
}
}
(3)业务方法中使用(两种方式)
方式一:自动匹配所有Service层方法
java
运行
@Service
public class UserService {
public void addUser(String username) {
// 模拟耗时操作
try {
Thread.sleep(600);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
方式二:通过注解指定阈值
java
运行
@Service
public class OrderService {
@PerformanceMonitor(threshold = 800)
public void createOrder(Order order) {
// 模拟耗时操作
try {
Thread.sleep(900);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3. 效果与价值
无侵入性:业务代码无需修改,只需通过切面和注解即可实现全量或部分方法的性能监控;
数据驱动:通过日志可清晰看到每个方法的执行耗时,为性能优化提供精准依据;
灵活配置:可通过注解自定义每个方法的耗时阈值,满足不同业务的性能要求。
这个案例充分体现了 AOP 的价值 —— 将 “性能监控” 这一通用需求从业务代码中完全抽离,形成可复用的切面组件,大幅提升了代码的可维护性和可扩展性。
七、AOP 通知获取数据:JoinPoint 与 ProceedingJoinPoint
在通知中获取 “目标方法的参数、返回值、方法签名” 等数据,是 AOP 实现复杂增强逻辑的基础。Spring AOP 通过JoinPoint(普通通知)和ProceedingJoinPoint(环绕通知)提供这些数据的访问能力。
1. JoinPoint:普通通知的数据访问
JoinPoint是所有普通通知(@Before、@After、@AfterReturning、@AfterThrowing)的参数类型,提供以下核心方法:
方法名 作用
getSignature() 获取方法签名(包含方法名、所属类等信息)
getArgs() 获取方法参数列表
getTarget() 获取目标对象(业务对象本身,而非代理对象)
getThis() 获取代理对象
getKind() 获取连接点类型(如 “method-execution”)
示例:在前置通知中获取参数和方法名
java
运行
@Before("userServicePointcut()")
public void beforeAdvice(JoinPoint joinPoint) {
// 获取方法名
String methodName = joinPoint.getSignature().getName();
// 获取方法所属类
String className = joinPoint.getSignature().getDeclaringTypeName();
// 获取方法参数
Object[] args = joinPoint.getArgs();
log.info("前置通知:{}类的{}方法,参数:{}", className, methodName, Arrays.toString(args));
}
2. ProceedingJoinPoint:环绕通知的数据访问
ProceedingJoinPoint是JoinPoint的子类,仅用于@Around通知,新增了proceed()方法用于触发目标方法执行,同时也支持JoinPoint的所有数据访问方法。
示例:在环绕通知中修改参数和返回值
java
运行
@Around("userServicePointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取并修改参数
Object[] args = joinPoint.getArgs();
if (args.length > 0 && args[0] instanceof String) {
args[0] = ((String) args[0]).toUpperCase(); // 将第一个参数转为大写
}
// 执行目标方法(传入修改后的参数)
Object result = joinPoint.proceed(args);
// 修改返回值
if (result instanceof String) {
result = "增强后的结果:" + result;
}
return result;
}
3. 从返回通知中获取返回结果
@AfterReturning通知通过returning属性绑定返回结果,示例如下:
java
运行
@AfterReturning(value = "userServicePointcut()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
log.info("方法{}返回结果:{}", methodName, result);
// 对返回结果进行校验(如检查是否为null)
if (result == null) {
log.warn("方法{}返回null,可能存在业务异常", methodName);
}
}
4. 从异常通知中获取异常对象
@AfterThrowing通知通过throwing属性绑定异常对象,示例如下:
java
运行
@AfterThrowing(value = "userServicePointcut()", throwing = "ex")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
log.error("方法{}抛出异常,类型:{},信息:{}",
methodName, ex.getClass().getName(), ex.getMessage(), ex);
// 根据异常类型进行不同处理
if (ex instanceof IllegalArgumentException) {
log.info("参数异常,进行参数校验补偿逻辑...");
} else if (ex instanceof SQLException) {
log.info("数据库异常,进行事务回滚补偿逻辑...");
}
}
掌握JoinPoint和ProceedingJoinPoint的数据访问能力,能让我们在通知中实现各种复杂的增强逻辑,例如:
参数校验与修改;
返回结果校验与增强;
异常分类处理;
方法签名级别的日志记录等。
八、案例 - 百度网盘密码数据兼容处理:AOP 的业务适配实践
在系统迭代或多系统对接时,“数据格式兼容” 是常见需求。以 “百度网盘密码格式升级” 为例,旧密码是纯数字,新密码要求 “数字 + 字母 + 特殊字符”,我们可以通过 AOP 实现无侵入的密码格式兼容处理。
1. 需求分析
旧系统中用户密码是纯数字(如 “123456”);
新系统要求密码必须是 “数字 + 字母 + 特殊字符” 的组合(如 “Abc@123”);
需兼容旧密码登录,同时引导用户修改为新格式;
业务代码不能修改,需通过 AOP 实现格式转换和兼容逻辑。
2. 实现步骤
(1)自定义注解(标记需要处理的方法)
java
运行
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PasswordCompat {
boolean requireNewFormat() default true; // 是否强制要求新格式
}
(2)切面类实现
java
运行
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Method;
@Aspect
@Component
@Slf4j
public class PasswordCompatAspect {
// 切入点:匹配所有标注了@PasswordCompat的方法
@Pointcut("@annotation(com.example.demo.annotation.PasswordCompat)")
public void passwordCompatPointcut() {}
@Around("passwordCompatPointcut()")
public Object handlePasswordCompat(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
PasswordCompat compat = method.getAnnotation(PasswordCompat.class);
boolean requireNewFormat = compat.requireNewFormat();
// 获取方法参数,找到密码参数(假设第一个参数是密码)
Object[] args = joinPoint.getArgs();
if (args.length > 0 && args[0] instanceof String) {
String password = (String) args[0];
if (isOldFormat(password)) {
if (requireNewFormat) {
log.warn("检测到旧格式密码,强制要求升级,请引导用户修改密码");
throw new IllegalArgumentException("密码格式已升级,请修改为新格式(数字+字母+特殊字符)");
} else {
log.info("检测到旧格式密码,兼容处理中...");
// 此处可添加旧密码到新密码的转换逻辑(如在旧密码后拼接固定特殊字符)
args[0] = password + "!@#";
}
}
}
// 执行目标方法
Object result = joinPoint.proceed(args);
return result;
}
// 判断是否为旧格式密码(纯数字)
private boolean isOldFormat(String password) {
return password != null && password.matches("^\\d+$");
}
}
(3)业务方法中使用
java
运行
@Service
public class UserService {
@PasswordCompat(requireNewFormat = false)
public boolean login(String password, String username) {
// 模拟登录逻辑,此时password已被AOP兼容处理
log.info("登录密码:{}", password);
return "admin123!@#".equals(password) && "admin".equals(username);
}
@PasswordCompat(requireNewFormat = true)
public void changePassword(String newPassword) {
// 模拟修改密码逻辑,此时newPassword必须是新格式
log.info("新密码:{}", newPassword);
// 执行修改密码的业务逻辑
}
}
3. 效果与价值
业务无侵入:登录和修改密码的业务代码无需修改,兼容逻辑完全由 AOP 接管;
灵活控制:通过requireNewFormat属性可灵活控制是否强制要求新格式,满足不同场景需求;
可扩展性:若后续密码格式再次升级,只需修改 AOP 中的格式判断和转换逻辑,无需改动业务代码。
这个案例展示了 AOP 在业务兼容性场景下的强大能力 —— 它能在不修改核心业务逻辑的前提下,通过 “横向切入” 的方式快速实现数据格式的兼容、转换或校验,是系统迭代和多系统对接的 “利器”。
九、AOP 总结:核心价值与适用场景
经过对 AOP 概念、原理、实战的全面解析,我们可以总结出 AOP 的核心价值和适用场景,帮助你在项目中做出正确的技术选型。
1. AOP 的核心价值
解耦通用逻辑与业务逻辑:将日志、事务、权限等通用功能从业务代码中抽离,让业务代码更专注于核心逻辑;
代码复用性:通用逻辑只需编写一次,即可应用于所有匹配的连接点,大幅减少重复代码;
可维护性:通用逻辑的修改只需调整切面,无需修改所有业务代码,降低维护成本;
无侵入性:业务代码无需感知切面的存在,符合 “开闭原则”(对扩展开放,对修改关闭)。
2. AOP 的适用场景
横切关注点:所有与业务逻辑正交的通用功能,如:
日志记录:方法调用日志、参数日志、返回结果日志;
事务控制:声明式事务管理;
权限校验:方法级别的权限拦截;
性能监控:方法执行时长统计、资源消耗统计;
异常处理:全局异常捕获、异常信息封装;
缓存控制:方法结果缓存、缓存失效;
数据校验:参数格式校验、返回结果校验。
业务适配场景:如数据格式兼容、多版本接口兼容、第三方系统对接适配等。
3. AOP 的局限与避坑指南
过度使用的风险:AOP 的灵活性可能导致开发者将所有逻辑都 “切面化”,反而增加代码的理解难度。建议只对真正的横切关注点使用 AOP,业务逻辑仍保持内聚。
性能考量:Spring AOP 的动态代理会带来轻微的性能开销(毫秒级),对于绝大多数业务场景可忽略,但在超高性能要求的场景(如高频交易系统)需谨慎使用,或考虑 AspectJ。
调试难度:AOP 的增强逻辑是 “隐性” 的,出现问题时需明确代理对象和通知执行顺序,可通过日志或调试工具(如 IDEA 的 AOP 调试插件)辅助排查。
事务失效的陷阱:在同一个类中,内部方法调用可能导致事务切面失效(因为代理对象未被调用),需使用AopContext.currentProxy()获取代理对象后调用方法。
十、Spring 事务简介:数据一致性的保障
在企业级应用中,数据一致性是核心需求之一。例如用户下单时,需同时 “扣减库存” 和 “创建订单”,这两个操作必须 “同时成功或同时失败”,否则会出现数据不一致(如库存扣了但订单没创建,导致超卖)。Spring 事务就是解决这类问题的 “标准方案”。
1. 事务的基本概念
事务(Transaction)是数据库操作的最小执行单元,它具有以下四个特性(ACID):
原子性(Atomicity):事务中的操作要么全部成功,要么全部失败,不存在 “部分成功” 的中间状态;
一致性(Consistency):事务执行前后,数据必须从一个一致状态转换到另一个一致状态(如转账前后,双方余额总和不变);
隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行;
持久性(Durability):事务一旦提交,其修改的数据会永久保存到数据库,不会因系统故障丢失。
2. Spring 事务的核心优势
Spring 事务是对JDBC、Hibernate、MyBatis等持久层技术的事务管理的统一封装,其核心优势在于:
声明式事务:通过注解(@Transactional)即可实现事务管理,无需手动编写beginTransaction、commit、rollback等代码;
全局事务管理:支持多数据源、多事务管理器的全局事务;
异常驱动回滚:可根据不同的异常类型决定是否回滚事务;
与 AOP 深度集成:事务本身就是一个 “切面”,通过 AOP 机制织入到业务方法中。
3. Spring 事务的实现方式
Spring 事务支持两种实现方式:
编程式事务:通过TransactionTemplate或PlatformTransactionManager手动管理事务,灵活性高但代码侵入性强。
java
运行
@Service
public class OrderService {
@Autowired
private TransactionTemplate transactionTemplate;
public void createOrder(Order order) {
transactionTemplate.execute(status -> {
try {
// 执行扣减库存操作
inventoryService.decreaseStock(order.getProductId(), order.getQuantity());
// 执行创建订单操作
orderDao.insert(order);
return Boolean.TRUE;
} catch (Exception e) {
status.setRollbackOnly(); // 标记事务回滚
throw e;
}
});
}
}
声明式事务:通过@Transactional注解自动管理事务,是 Spring 事务的推荐方式。
java
运行
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// 执行扣减库存操作
inventoryService.decreaseStock(order.getProductId(), order.getQuantity());
// 执行创建订单操作
orderDao.insert(order);
}
}
声明式事务的简洁性使其成为日常开发的首选,下文将重点解析声明式事务的核心知识点。
十一、Spring 事务角色:理解事务的参与者
要深入理解 Spring 事务,需先明确其核心角色,这些角色是事务管理的 “基础设施”。
1. 事务管理器(PlatformTransactionManager)
PlatformTransactionManager是 Spring 事务的核心接口,它定义了事务的基本操作(获取事务、提交事务、回滚事务)。不同的持久层技术对应不同的实现类:
持久层技术 事务管理器实现类
JDBC/MyBatis DataSourceTransactionManager
Hibernate HibernateTransactionManager
JPA JpaTransactionManager
JTA JtaTransactionManager
在 Spring Boot 中,只需引入对应的持久层依赖,Spring 会自动配置事务管理器。例如引入mybatis-spring-boot-starter后,Spring 会自动配置DataSourceTransactionManager。
2. 事务定义(TransactionDefinition)
TransactionDefinition用于定义事务的属性,如隔离级别、传播行为、超时时间、是否只读等。它包含以下核心属性:
(1)隔离级别(Isolation Level)
隔离级别定义了事务之间的 “隔离程度”,解决并发事务带来的 “脏读、不可重复读、幻读” 问题。Spring 支持以下隔离级别:
隔离级别常量 含义 解决问题
ISOLATION_DEFAULT 采用数据库默认的隔离级别(如 MySQL 默认是REPEATABLE_READ) 依赖数据库配置
ISOLATION_READ_UNCOMMITTED 读未提交:一个事务可读取另一个事务未提交的数据 无
ISOLATION_READ_COMMITTED 读已提交:一个事务只能读取另一个事务已提交的数据(Oracle 默认级别) 脏读
ISOLATION_REPEATABLE_READ 可重复读:事务内多次读取同一数据,结果始终一致(MySQL 默认级别) 脏读、不可重复读
ISOLATION_SERIALIZABLE 串行化:事务串行执行,完全隔离 脏读、不可重复读、幻读
(2)传播行为(Propagation Behavior)
传播行为定义了 **“当前方法调用时,已有事务的处理方式”**,这是 Spring 事务最具特色的属性。Spring 支持以下传播行为:
传播行为常量 含义
PROPAGATION_REQUIRED 默认值:如果当前存在事务,则加入该事务;如果不存在事务,则创建新事务。
PROPAGATION_SUPPORTS 如果当前存在事务,则加入该事务;如果不存在事务,则以非事务方式执行。
PROPAGATION_MANDATORY 必须存在事务,否则抛出异常。
PROPAGATION_REQUIRES_NEW 无论当前是否存在事务,都创建新事务(原事务挂起)。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行,如果当前存在事务,则挂起该事务。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行;如果不存在事务,则创建新事务。(嵌套事务依赖数据库支持)
(3)其他属性
timeout:事务超时时间(秒),超过该时间事务自动回滚;
readOnly:是否为只读事务(对于只读操作,数据库可做性能优化);
rollbackFor:指定哪些异常触发事务回滚;
noRollbackFor:指定哪些异常不触发事务回滚。
3. 事务状态(TransactionStatus)
TransactionStatus用于表示事务的执行状态,包含以下核心方法:
isNewTransaction():是否为新事务;
setRollbackOnly():标记事务为 “仅回滚”,后续操作会触发回滚;
isRollbackOnly():判断事务是否被标记为仅回滚;
isCompleted():判断事务是否已完成(提交或回滚)。
在编程式事务中,我们可以通过TransactionStatus手动控制事务的执行流程;在声明式事务中,Spring 会自动管理TransactionStatus。
十二、Spring 事务属性:@Transactional 注解的深度解析
@Transactional是 Spring 声明式事务的 “入口”,理解其属性是正确使用 Spring 事务的关键。
1. @Transactional 的核心属性
java
运行
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
// 事务管理器的Bean名称,多事务管理器时需指定
String value() default "";
String transactionManager() default "";
// 传播行为
Propagation propagation() default Propagation.REQUIRED;
// 隔离级别
Isolation isolation() default Isolation.DEFAULT;
// 超时时间(秒)
int timeout() default -1;
// 是否只读
boolean readOnly() default false;
// 触发回滚的异常类
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
// 不触发回滚的异常类
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
2. 关键属性详解
(1)propagation(传播行为)
传播行为是@Transactional最核心的属性,决定了方法调用时事务的创建和关联方式。以下是两个典型场景:
场景一:默认传播行为(REQUIRED)
java
运行
@Service
public class OrderService {
@Autowired
private InventoryService inventoryService;
@Transactional
public void createOrder(Order order) {
// 扣减库存(InventoryService的decreaseStock方法也标注了@Transactional)
inventoryService.decreaseStock(order.getProductId(), order.getQuantity());
// 创建订单
orderDao.insert(order);
}
}
@Service
public class InventoryService {
@Transactional
public void decreaseStock(Long productId, Integer quantity) {
// 扣减库存的业务逻辑
}
}
此时createOrder和decreaseStock方法会共享同一个事务:若decreaseStock抛出异常,整个事务回滚,订单也不会创建;若createOrder抛出异常,decreaseStock的操作也会回滚。
场景二:REQUIRES_NEW(新建事务)
java
运行
@Service
public class OrderService {
@Autowired
private InventoryService inventoryService;
@Transactional
public void createOrder(Order order) {
try {
// 扣减库存(新建事务,与当前事务独立)
inventoryService.decreaseStock(order.getProductId(), order.getQuantity());
} catch (Exception e) {
// 捕获异常,不影响当前事务
log.error("扣减库存失败,但订单仍创建", e);
}
// 创建订单
orderDao.insert(order);
}
}
@Service
public class InventoryService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void decreaseStock(Long productId, Integer quantity) {
// 扣减库存的业务逻辑
throw new RuntimeException("库存不足");
}
}
此时decreaseStock方法会创建新事务,与createOrder的事务独立:decreaseStock抛出异常后,其事务回滚(库存不会扣减),但createOrder的事务仍会提交(订单会创建)。
(2)isolation(隔离级别)
隔离级别用于控制事务之间的并发干扰,需根据业务场景选择:
读已提交(ISOLATION_READ_COMMITTED):适用于 “允许不可重复读,但不允许脏读” 的场景(如普通业务查询);
可重复读(ISOLATION_REPEATABLE_READ):适用于 “要求数据一致性高,不允许不可重复读” 的场景(如金融交易);
串行化(ISOLATION_SERIALIZABLE):适用于 “数据绝对不允许并发干扰” 的场景(如库存扣减),但性能极低,需谨慎使用。
(3)rollbackFor/noRollbackFor(异常回滚规则)
Spring 事务默认仅对RuntimeException和Error触发回滚,对受检异常(如IOException、SQLException)不回滚。通过rollbackFor和noRollbackFor可自定义回滚规则:
java
运行
@Transactional(rollbackFor = {IllegalArgumentException.class, SQLException.class},
noRollbackFor = BusinessException.class)
public void doBusiness() {
// 业务逻辑
if (condition) {
throw new IllegalArgumentException("参数异常"); // 触发回滚
}
if (anotherCondition) {
throw new BusinessException("业务异常"); // 不触发回滚
}
}
(4)readOnly(只读事务)
对于 “纯查询” 的方法,标记为readOnly = true可提示数据库进行性能优化(如 MySQL 的 MVCC 优化):
java
运行
@Transactional(readOnly = true)
public List<User> listAllUsers() {
return userDao.selectList(null);
}
3. 事务失效的常见场景与解决方案
尽管@Transactional使用简单,但稍有不慎就会导致事务失效,以下是典型场景及解决方案:
(1)方法非 public
Spring 事务仅对public方法生效,若方法是private、protected或默认权限,事务会失效。解决方案:确保事务方法是public。
(2)自调用(同一类中方法互相调用)
在同一个类中,内部方法调用不会触发事务切面(因为没有通过代理对象调用)。
java
运行
@Service
public class UserService {
public void outerMethod() {
innerMethod(); // 自调用,事务失效
}
@Transactional
public void innerMethod() {
// 事务逻辑
}
}
解决方案:通过AopContext.currentProxy()获取代理对象后调用:
java
运行
import org.springframework.aop.framework.AopContext;
@Service
public class UserService {
public void outerMethod() {
((UserService) AopContext.currentProxy()).innerMethod(); // 通过代理对象调用,事务生效
}
@Transactional
public void innerMethod() {
// 事务逻辑
}
}
需注意:启用expose-proxy = true(Spring Boot 中默认启用)。
(3)异常被捕获且未抛出
若异常被捕获后未重新抛出,Spring 无法感知异常,事务不会回滚:
java
运行
@Transactional
public void createOrder(Order order) {
try {
// 业务逻辑,可能抛出异常
inventoryService.decreaseStock(order.getProductId(), order.getQuantity());
orderDao.insert(order);
} catch (Exception e) {
log.error("创建订单失败", e);
// 未重新抛出异常,事务不会回滚
}
}
解决方案:捕获异常后重新抛出,或在catch中调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly():
java
运行
@Transactional
public void createOrder(Order order) {
try {
// 业务逻辑
} catch (Exception e) {
log.error("创建订单失败", e);
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw e; // 或抛出自定义异常
}
}
(4)异常类型不匹配
若异常类型不在rollbackFor范围内,事务不会回滚:
java
运行
@Transactional(rollbackFor = RuntimeException.class)
public void createOrder(Order order) {
throw new SQLException("数据库异常"); // SQLException不是RuntimeException,事务不回滚
}
解决方案:明确指定rollbackFor包含目标异常,或使用rollbackFor = Exception.class(不推荐,过于宽泛)。
十三、总结:AOP 与 Spring 事务的协同价值
AOP 和 Spring 事务是 Spring 生态中 “相辅相成” 的两大技术:
AOP是 “横向抽取通用逻辑” 的工具,让日志、权限、事务等功能实现了 “无侵入式增强”;
Spring 事务是 AOP 的典型应用场景,通过 “事务切面” 实现了声明式事务管理,让开发者从繁琐的事务控制代码中解放出来。
掌握 AOP 和 Spring 事务,不仅能大幅提升代码的可维护性和扩展性,更能确保系统在复杂业务场景下的数据一致性和稳定性。从 “入门案例” 到 “原理剖析”,再到 “实战场景”,本文覆盖了 AOP 和 Spring 事务的核心知识点,希望能帮助你在实际项目中灵活运用这两大技术,写出更优雅、更健壮的代码。
在技术的学习路上,“知其然” 只是起点,“知其所以然” 才能让你在面对复杂问题时游刃有余。持续探索,不断实践,你终将在 Spring 的世界中找到属于自己的 “切面” 和 “事务” 的平衡。
