AOP简介
Spring框架有两大核心:IOC/DI (控制反转/依赖注入)和AOP(面向切面编程)。
我们已经系统学习了IOC/DI,现在来学习AOP。AOP的核心思想是:在不修改原有代码的前提下,对程序功能进行增强。
AOP(Aspect Oriented Programming,面向切面编程)是一种编程范式,指导开发者如何组织程序结构
- OOP(Object Oriented Programming,面向对象编程)也是一种编程范式
- 两者是不同的编程思想,分别从不同角度解决软件设计问题
AOP的作用:在不修改原始代码的基础上,为程序添加新的功能。
实际上,我们之前学过的代理模式就可以实现这样的功能,Spring的AOP底层正是基于代理模式实现的。
AOP核心概念详解
为了更好地理解AOP,我们先看一个例子:
java
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
//记录程序当前执行执行(开始时间)
Long startTime = System.currentTimeMillis();
//业务执行万次
for (int i = 0;i
**观察与思考**:
* 只有`save()`方法有计算执行时间的代码
* 但运行后发现,`update()`和`delete()`方法也具备了计算时间的功能
* `select()`方法却没有这个功能
这就是AOP的魔力:在不改动原有代码的情况下,选择性地为某些方法添加新功能!
**Spring是如何实现的**:\ <img src="https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d67eb945a0b643b3915d10af9e5e1088~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6YOh5p2w:q75.awebp?rk3s=f64ab15b&x-expires=1765985902&x-signature=C9ZBbkcu2HchOCCkBePTgtqtdp0%3D" alt="image.png" width="100%">
**AOP核心概念**
1. **连接点**
* 定义:程序执行过程中**可以插入额外功能的点**
* 实质:**可被增强的方法**
* 理解与例子:供插入的点,`BookDaoImpl`中的`save()`、`update()`、`delete()`、`select()`方法都是连接点
2. **切入点**
* 定义:实际**被增强了功能的连接点**
* 实质:**已被增强的方法**
* 与连接点关系:**切入点⊆连接点**(切入点一定是连接点,但连接点不一定是切入点)
* 理解与例子:已插入的点,`update()`和`delete()`方法被增强了,它们就是切入点;`select()`方法没有被增强,它只是连接点
3. **通知**
* 定义:要添加的**额外功能**
* 实质:**存放增强功能的方法**
* 例子:计算方法执行时间的功能就是一个通知
4. **通知类/切面类**
* 定义:存放通知方法的类
* 特点:通知方法不能独立存在,必须放在某个类中
5. **切面**
* 定义:描述通知和切入点之间的对应关系,建立"什么地方"和"做什么事"的映射关系
* 实质:描述哪些方法增强了哪些功能
# AOP入门案例
## 需求分析
**场景设定**:我们需要测量接口方法的执行效率,但为了入门学习,我们先简化需求:在方法执行前输出当前系统时间。
**技术选择**:Spring AOP支持XML配置和注解两种方式,由于注解方式更简洁、现代,我们选择使用**注解方式**开发。
**最终目标**:使用Spring AOP注解,在不修改原有方法代码的前提下,在方法执行前打印当前系统时间。
## 实现思路
让我们将整个实现过程分解为清晰的步骤:
1. **导入依赖** - 在pom.xml中添加必要的Spring AOP依赖
2. **准备连接点** - 创建Dao接口和实现类(原始业务代码)
3. **制作通知** - 创建通知类和通知方法(增强功能)
4. **定义切入点** - 指定哪些方法需要被增强
5. **绑定关系** - 通过切面将通知和切入点关联起来
## 环境准备
**第一步:创建Maven项目并添加依赖**
```xml
org.springframework
spring-context
5.2.10.RELEASE
org.springframework
spring-aspects
5.2.10.RELEASE
第二步:创建业务接口和实现类(连接点)
BookDao接口:
java
public interface BookDao {
void save();
void update();
}
BookDaoImpl实现类:
java
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
// 原始业务逻辑
System.out.println("book dao save ...");
}
public void update() {
// 原始业务逻辑
System.out.println("book dao update ...");
}
}
第三步:创建Spring配置类
java
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy // 开启AOP注解支持
public class SpringConfig {
}
第四步:创建测试运行类
java
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
// 测试方法调用
bookDao.save();
bookDao.update();
}
}
最终创建好的项目结构如下: \ 
当前状态分析
当前运行效果:
save()方法:只输出"book dao save ..."update()方法:只输出"book dao update ..."
我们的目标:
- 在不修改
BookDaoImpl代码的前提下 - 让这两个方法在执行前都输出系统时间
- 实现"无侵入式"的功能增强
AOP实现步骤
步骤1:添加AOP依赖
在pom.xml中添加AspectJ依赖:
xml
org.springframework
spring-context
5.2.10.RELEASE
org.aspectj
aspectjweaver
1.9.4
重要说明:
spring-context已经包含了spring-aop,无需重复导入- 我们使用AspectJ作为AOP的具体实现,因为它功能更强大、使用更简单
步骤2:确认业务类(连接点)
我们的BookDaoImpl已经准备好,包含两个方法:
java
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
public void update() {
System.out.println("book dao update ...");
}
}
步骤3:创建通知类和通知方法
创建通知类,定义要添加的增强功能(即定义通知):
java
public class MyAdvice {
// 通知方法:打印当前系统时间
public void printCurrentTime() {
System.out.println("当前时间:" + System.currentTimeMillis());
}
}
步骤4:在通知类中定义切入点
使用@Pointcut注解定义切入点(指定要增强的方法):
java
public class MyAdvice {
// 定义切入点:匹配BookDao的update方法
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void updateMethodPointcut() {}
public void printCurrentTime() {
System.out.println("当前时间:" + System.currentTimeMillis());
}
}
切入点说明:
execution(void com.itheima.dao.BookDao.update())表示匹配:- 返回类型:
void - 完整方法名 (
包名.实现类名.接口名()):com.itheima.dao.BookDao.update()
- 返回类型:
- 切入点方法
updateMethodPointcut()仅用于标记,本身没有实际逻辑
@Pointcut注解的意义
java
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void updateMethodPointcut() {}
-
最简单的理解:@Pointcut定义 "监控哪个方法"
text"我要监控 com.itheima.dao.BookDao.update() 这个方法" -
各部分分解:
execution= "执行时监控"void= "返回类型是void的方法"com.itheima.dao.BookDao.update()= "具体监控哪个方法"updateMethodPointcut= "给这个监控点起个名字"- 完整意思 :当 以返回类型为
void的方式,调用com.itheima.dao.BookDao类中的update方法时,就会触发后续的通知。
-
为什么需要一个空方法?
这个空方法
updateMethodPointcut()的作用就是:
给监控规则及其方法起个名字,方便其他代码引用,相当于"取别名"、"贴标签"-
如果没有这个名字,每次都要写完整的监控规则:
java@Before("execution(void com.itheima.dao.BookDao.update())") @After("execution(void com.itheima.dao.BookDao.update())") @Around("execution(void com.itheima.dao.BookDao.update())") -
有了名字,就简单多了:
java@Before("updateMethodPointcut()") @After("updateMethodPointcut()") @Around("updateMethodPointcut()")
-
步骤5:在通知类中制作切面(绑定通知和切入点)
使用@Before注解将通知方法与切入点绑定:
java
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void updateMethodPointcut() {}
// 在切入点监控的方法执行前执行通知
@Before("updateMethodPointcut()")
public void printCurrentTime() {
System.out.println("当前时间:" + System.currentTimeMillis());
}
}
定义通知的执行时机:
@Before:在切入点方法监控的方法执行之前执行通知方法- 还有其他时机:
@After(切入点之后)、@Around(环绕)等
步骤6:配置通知类为切面类
添加注解,让Spring识别这是一个切面类:
java
@Component // 交给Spring容器管理
@Aspect // 标识这是一个切面类
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void updateMethodPointcut() {}
@Before("updateMethodPointcut()")
public void printCurrentTime() {
System.out.println("当前时间:" + System.currentTimeMillis());
}
}
步骤7:开启AOP注解支持
在Spring配置类中启用AOP功能:
java
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy // 开启AOP注解自动代理
public class SpringConfig {
}
步骤8:测试运行
运行程序查看AOP效果:
java
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
// 调用update方法,会触发AOP增强
bookDao.update();
// 调用save方法,不会触发AOP增强
bookDao.save();
}
}
预期运行结果

注解汇总
| 名称 | @EnableAspectJAutoProxy |
|---|---|
| 类型 | 配置类注解 |
| 位置 | 配置类定义上方 |
| 作用 | 开启注解格式AOP功能 |
| 名称 | @Aspect |
|---|---|
| 类型 | 类注解 |
| 位置 | 切面类定义上方 |
| 作用 | 设置当前类为AOP切面类 |
| 名称 | @Pointcut |
|---|---|
| 类型 | 方法注解 |
| 位置 | 切入点方法定义上方 |
| 作用 | 设置切入点方法 |
| 属性 | value(默认):切入点表达式 |
| 名称 | @Before |
|---|---|
| 类型 | 方法注解 |
| 位置 | 通知方法定义上方 |
| 作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行 |
AOP工作流程
AOP工作流程
为了更好地理解AOP的工作流程,我们可以把它想象成一个"智能安保系统"的工作过程:
流程1:Spring容器启动 - 系统初始化
- Spring容器启动时,扫描并识别所有需要管理的类
- 识别两类重要角色:
- 目标类 :需要被增强的类(如:
BookServiceImpl) - 通知类 :包含增强功能的类(如:
MyAdvice)
- 目标类 :需要被增强的类(如:
- 注意:此时只是识别了这些类,还没有创建具体的对象实例
流程2:读取切面配置 - 制定监控规则
- Spring读取所有
@Aspect切面类中的 实际被使用的@Pointcut切入点(有通知方法引用的)配置 - 未被引用的切入点不会被处理
流程3:创建Bean并判断是否需要增强 - 对象检查

当Spring创建每个bean实例时,会进行重要判断:
- 情况A:匹配失败 → 创建原始对象
- 如果类中的方法没有匹配 任何切入点,Spring直接创建原始对象,后续调用方法时直接执行,无任何拦截
- 例子:
UserDao类中的方法都不需要增强
- 情况B:匹配成功 → 创建代理对象
- 如果类中的方法匹配到 切入点规则,Spring创建代理对象(而不是直接使用原始对象),后续所有方法调用都通过代理对象进行
- 目标对象:原始的、需要被增强的对象
- 代理对象:Spring生成的、包含增强逻辑的"替身"
流程4:获取Bean执行方法 - 实际行为监控
获取到原始对象时:
java
// 直接调用方法,无任何拦截
userDao.someMethod(); // 直接执行原始逻辑
获取到代理对象时:
java
// 通过代理对象调用方法
bookDao.update();
// 实际执行过程:
// 1. 代理对象拦截方法调用
// 2. 执行前置增强(如:打印时间)
// 3. 调用原始对象的update()方法
// 4. 返回结果
AOP代理对象验证
验证Spring AOP的工作机制:
- 当方法需要增强时,容器中存储的是代理对象
- 当方法不需要增强时,容器中存储的是原始对象本身
验证步骤
步骤1:修改测试类,获取对象类型信息
java
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
// 打印对象引用和具体类型
System.out.println("对象引用: " + bookDao);
System.out.println("实际类型: " + bookDao.getClass());
}
}
步骤2:场景一:方法不被增强(不匹配切入点)
修改通知类,使切入点不匹配update方法:
java
@Component
@Aspect
public class MyAdvice {
// 切入点指向不存在的update1方法,update方法不会被增强
@Pointcut("execution(void com.itheima.dao.BookDao.update1())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
运行结果:
kotlin
对象引用: com.itheima.dao.impl.BookDaoImpl@xxxxxx
实际类型: class com.itheima.dao.impl.BookDaoImpl
分析:
- 显示的是原始类名
BookDaoImpl - 说明容器中存储的是原始对象本身
- 因为
update方法不匹配切入点,不需要增强
步骤3:场景二:方法被增强(匹配切入点)
修改通知类,使切入点匹配update方法:
java
@Component
@Aspect
public class MyAdvice {
// 切入点正确指向update方法,该方法会被增强
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
运行结果:
kotlin
对象引用: com.itheima.dao.impl.BookDaoImpl@xxxxxx
实际类型: class com.sun.proxy.$ProxyXX
分析:
- 显示的是代理类名
$ProxyXX(具体编号可能不同) - 说明容器中存储的是代理对象
- 因为
update方法匹配切入点,需要增强
关于toString()方法的注意事项
两次运行都打印了相同的对象引用信息,这是因为:
- 代理对象重写了toString()方法,使其显示与原始对象相同
- 直接打印对象无法区分是否为代理对象
- 必须通过getClass()方法才能看到真实的类型信息
AOP核心概念
目标对象 (Target Object)
定义:
- 是(未增强)原始业务类的对象
- 在AOP中,这是需要被增强的对象,不包含增强功能
准确理解:
- 目标对象是完整的、可以独立运行的对象
- 它包含了设计时预期的所有业务功能
- 只是在AOP的视角下,它"缺少"某些横切关注点功能(如日志、事务等)
示例说明:
java
// BookDaoImpl 实例就是目标对象
// 它本身完全可以正常工作,执行save()、update()等业务方法
BookDao bookDao = new BookDaoImpl(); // 目标对象
bookDao.update(); // 可以正常执行
代理对象 (Proxy Object)
定义:
- Spring为需要增强的目标对象创建的替代对象
- 在目标对象的基础上已添加了额外功能
准确理解/代理模式的本质:
- 代理对象不修改目标对象,而是创建新对象 来包装原始对象
- 代理对象内部持有目标对象的引用
- 当调用代理对象的方法时,会执行增强逻辑 与 目标对象的原始方法,执行顺序取决于使用的通知类型
逻辑实现:
java
// Spring内部大致这样工作:
public class ProxyBookDao implements BookDao {
private BookDao target; // 持有目标对象引用
public void update() {
// 执行前置增强(如:打印时间)
System.out.println(System.currentTimeMillis());
// 调用目标对象的原始方法
target.update();
}
}
AOP配置管理
AOP切入点表达式
切入点:需要进行功能增强的具体方法
切入点表达式:用来描述和匹配切入点的表达式语言
切入点描述的两种方式
对于切入点的描述,我们其实是有两种方式的,先来看下前面的例子:

方式一:基于接口描述
java
execution(void com.itheima.dao.BookDao.update())
描述:
- 执行
com.itheima.dao包下的BookDao接口中的无参数update方法 - 匹配所有实现了
BookDao接口的类的update()方法
方式二:基于实现类描述
java
execution(void com.itheima.dao.impl.BookDaoImpl.update())
描述:
- 执行
com.itheima.dao.impl包下的BookDaoImpl类中的无参数update方法 - 只匹配
BookDaoImpl这一个具体类的update()方法
两种方式区别与选择
- 接口方式:适用于所有实现该接口的类,更通用,面向抽象,推荐在生产代码中使用
- 实现类方式:只针对特定的实现类,更具体,面向具体,适合特定场景
- 两种方式都能匹配到实际的方法调用
语法格式
切入点表达式的标准语法格式:
scss
动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
语法分解说明
以具体例子说明:
java
execution(public User com.itheima.service.UserService.findById(int))
| 组成部分 | 说明 | 说明 |
|---|---|---|
| 动作关键字 | 描述切入点的行为 | execution(执行到指定切入点) |
| 访问修饰符 | 方法的访问权限 | public(还可以是public、private等,省略默认public) |
| 返回值 | 方法的返回类型 | User,写返回值类型 |
| 包名 | 类的完整包路径 | com.itheima.service,多级包使用点连接 |
| 类/接口名 | 具体的类或接口 | UserService |
| 方法名 | 具体的方法名称 | findById |
| 参数 | 方法的参数类型 | int,多个类型用逗号隔开 |
| 异常名 | 方法抛出的异常 | (可省略) |
通配符使用
为了简化切入点表达式的编写,一次匹配一类需要增强的方法,可以使用通配符:
*通配符
-
相当于单个任意元素
-
可以独立出现,也可以作为名的前缀部分或者后缀部分出现
-
可用于包名、类名、方法名等位置
java// 匹配com.itheima包下任意子包中的UserService类中所有find开头的方法 execution(public * com.itheima.*.UserService.find*(*))
..通配符
-
相当于多个连续的任意元素
-
常用于包路径和参数列表
java// 匹配com包下任意多级包中的UserService类的findById方法,参数任意 execution(public User com..UserService.findById(..))
+通配符
-
+写在接口名后面,接口名+表示该接口的所有后代类型 -
+号的作用是向下展开整个继承与实现树(包括实现类的子类、子接口的实现类)。它不匹配该类型本身,而是匹配其所有后代类型。 -
使用频率较低
java// 匹配所有以Service结尾的接口的子类,*Service+表示所有以Service结尾的接口的子类 execution(* *..*Service+.*(..))
常用表达式示例
java
// 精确匹配示例
// 匹配BookDao接口的update方法
execution(void com.itheima.dao.BookDao.update())
// 匹配BookDaoImpl类的update方法
execution(void com.itheima.dao.impl.BookDaoImpl.update())
// 通配符匹配示例
// 匹配任意返回值的update方法
execution(* com.itheima.dao.impl.BookDaoImpl.update())
// 匹配com包下任意三层包中的任意类的update方法
execution(void com.*.*.*.update())
// 匹配任意包下方法名为update的任意方法
execution(void *..update())
// 匹配项目中所有类的所有方法(不推荐)
execution(* *..*(..))
// 匹配所有以u开头的方法
execution(* *..u*(..))
// 匹配所有以e结尾的方法
execution(* *..*e(..))
// 实用业务场景示例
// 匹配所有业务层中以find开头的方法
execution(* com.itheima.*.*Service.find*(..))
// 匹配所有业务层中以save开头的方法
execution(* com.itheima.*.*Service.save*(..))
书写技巧
- 所有代码务遵循统一的命名规范,按照标准规范开发
- 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了。
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)。
- 返回值类型对于增删改类使用精准类型 加速匹配,对于查询类使用
*通配快速描述。 - 包名 书写尽量不使用
..通配符 ,效率过低,常用*做单个包描述匹配,或精准匹配。 - 接口名/类名书写 与模块相关的采用
*匹配 ,例如UserService书写成*Service,绑定所有业务层接口。 - 方法名书写,当方法执行的是同一类操作,但操作对象不同时,动词采用精准匹配,名词采用
*匹配,例如getById写成getBy*匹配所有"getByXxx"格式的方法,selectAll书写成select*匹配所有所有选择相关方法。 - 参数规则较为复杂,根据业务方法灵活调整。
- 通常不使用异常作为匹配规则。
AOP通知类型
类型介绍
AOP通知类型:
- 前置通知(Before):在切入点方法执行之前执行
- 后置通知(After):在切入点方法执行之后执行,无论方法是否抛出异常都会执行
- 环绕通知(Around):在切入点方法执行前后执行,可以控制方法执行时机,并可以处理返回值和异常
- 返回后通知(AfterReturning):在切入点方法正常执行完毕后执行,如果方法抛出异常则不执行
- 抛出异常后通知(AfterThrowing):在切入点方法抛出异常后执行
逻辑理解 :\ 
环境准备
第一步:创建项目并添加依赖
在pom.xml文件中配置必要的依赖:
xml
org.springframework
spring-context
5.2.10.RELEASE
org.aspectj
aspectjweaver
1.9.4
第二步:创建业务接口和实现类
BookDao接口:
java
public interface BookDao {
void update(); // 无返回值方法
int select(); // 有返回值方法
}
BookDaoImpl实现类:
java
@Repository
public class BookDaoImpl implements BookDao {
public void update() {
System.out.println("book dao update ...");
}
public int select() {
System.out.println("book dao select is running ...");
return 100; // 返回固定值100
}
}
第三步:配置Spring容器
SpringConfig配置类:
java
@Configuration // 标识这是配置类
@ComponentScan("com.itheima") // 扫描指定包下的组件
@EnableAspectJAutoProxy // 启用AOP注解支持
public class SpringConfig {
}
第四步:创建通知类(暂未绑定)
MyAdvice通知类:
java
@Component // 让Spring管理这个类
@Aspect // 标识这是切面类
public class MyAdvice {
// 定义切入点:匹配BookDao的update方法
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){} // 切入点方法,仅作为标记
// 各种通知方法(暂未绑定到切入点)
public void before() {
System.out.println("before advice ...");
}
public void after() {
System.out.println("after advice ...");
}
public void around() {
System.out.println("around before advice ...");
System.out.println("around after advice ...");
}
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
第五步:创建测试运行类
App测试类:
java
public class App {
public static void main(String[] args) {
// 创建Spring容器
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
// 从容器获取BookDao实例
BookDao bookDao = ctx.getBean(BookDao.class);
// 调用update方法
bookDao.update();
}
}
最终创建好的项目结构如下:\ 
通知类型的使用
前置通知 @Before
在通知方法上添加@Before注解
java
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
// 使用@Before注解绑定到切入点
@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
}
当调用bookDao.update()方法时,执行顺序为:
less
1. 执行 before() 方法 → 输出 "before advice ..."
2. 执行 update() 方法 → 输出 "book dao update ..."
@Before("pt()")也可以写成@Before("MyAdvice.pt()"),但不推荐,因为更冗长
后置通知 @After
在通知方法上添加@After注解
java
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
// 使用@After注解绑定到切入点
@After("pt()")
public void after() {
System.out.println("after advice ...");
}
}
当调用bookDao.update()方法时,执行顺序为:
less
1. 执行 before() 方法 → 输出 "before advice ..."
2. 执行 update() 方法 → 输出 "book dao update ..."
3. 执行 after() 方法 → 输出 "after advice ..."
后置通知无论目标方法是否抛出异常都会执行
环绕通知 @Around
基本使用
注意 :环绕通知方法中必须显式调用原始方法,否则原始方法不会执行。
正确实现:
java
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){}
@Around("pt()")
public void 环绕方法名(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ..."); //前置逻辑
// 调用原始方法
pjp.proceed();
System.out.println("around after advice ..."); //后置逻辑
}
}
运行结果:
erlang
around before advice ...
book dao update ...
around after advice ...
关键点:
ProceedingJoinPoint pjp和pjp.proceed()是环绕通知的固定组成部分,任何环绕通知都必须这样写、调用。- 必须声明抛出
Throwable异常。
环绕通知方法必须声明
throws Throwable,因为这是Spring框架的强制要求,确保能够处理原始方法可能抛出的所有类型的异常。
有返回值的方法的环绕通知处理
有返回值的方法的环绕通知必须:
- 必须设置返回类型为
Object - 必须接收
pjp.proceed()的返回值 - 必须返回原始方法的返回值(或修改后的值)
正确实现:
java
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(int com.itheima.dao.BookDao.select())")
private void pt2(){}
@Around("pt2()")
public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
// 接收原始方法的返回值
Object ret = pjp.proceed();
System.out.println("around after advice ...");
// 返回原始方法的返回值
return ret;
}
}
测试代码:
java
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
int num = bookDao.select();
System.out.println(num); // 输出: 100
}
}
运行结果:
erlang
around before advice ...
book dao select is running ...
around after advice ...
100
无论是否有返回值,仍然建议所有环绕通知都必须遵循这个固定模式:
java@Around("...") public Object 方法名(ProceedingJoinPoint pjp) throws Throwable { // 前置逻辑 Object result = pjp.proceed(); // 后置逻辑 return result; }
返回后通知 @AfterReturning
定义 :在目标方法正常执行完成后被触发执行的通知。
java
@AfterReturning("pt2()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
执行条件:
- 目标方法必须正常执行完毕(没有抛出异常)
- 如果目标方法执行过程中出现异常,此通知将不会执行
应用场景:记录方法成功执行的日志、处理方法的返回值等。
异常后通知 @AfterThrowing
定义 :在目标方法抛出异常时被触发执行的通知。
java
@AfterThrowing("pt2()") // 注意:原示例中注解名称有误,应为@AfterThrowing
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
执行条件:
- 目标方法抛出异常时
- 如果目标方法正常执行,此通知将不会执行
应用场景:异常处理、错误日志记录、异常监控等。
环绕通知的实现原理
环绕通知(@Around)通过控制目标方法的调用时机,可以模拟其他所有通知类型的功能:
java
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object ret;
try {
// 前置通知位置
System.out.println("前置通知");
// 调用目标方法
ret = pjp.proceed();
// 返回后通知位置
System.out.println("返回后通知");
} catch (Throwable e) {
// 异常后通知位置
System.out.println("异常后通知");
throw e;
} finally {
// 后置通知位置
System.out.println("后置通知");
}
return ret;
}
环绕通知的灵活性:
- 前置通知 :在
proceed()调用之前 - 返回后通知 :在
proceed()调用之后且无异常时 - 异常后通知:在catch块中
- 后置通知:在finally块中
这种设计使得环绕通知成为功能最强大的通知类型,可以完全控制目标方法的执行流程。
业务层接口执行效率的监控
需求理解
核心目标:监控每个业务层方法的执行时间,找出性能瓶颈以便优化。
实现思路:
- 方法执行前记录开始时间
- 方法执行后记录结束时间
- 计算时间差得到执行时长
- 要在方法执行的前后添加业务,采用环绕通知`。
- 由于单次执行时间可能太短,需要执行多次来计算平均时间
环境准备
在pom.xml中添加必要的依赖:
xml
org.springframework
spring-context
5.2.10.RELEASE
org.springframework
spring-jdbc
5.2.10.RELEASE
org.springframework
spring-test
5.2.10.RELEASE
org.aspectj
aspectjweaver
1.9.4
mysql
mysql-connector-java
5.1.47
com.alibaba
druid
1.1.16
org.mybatis
mybatis
3.5.6
org.mybatis
mybatis-spring
1.3.0
junit
junit
4.12
test
数据模型与业务接口
账户实体类(Account类):
java
public class Account implements Serializable {
private Integer id;
private String name;
private Double money;
//setter...getter...toString方法省略
}
数据访问层接口(AccountDao类):
java
public interface AccountDao {
@Insert("insert into tbl_account(name,money)values(#{name},#{money})")
void save(Account account);
@Delete("delete from tbl_account where id = #{id}")
void delete(Integer id);
@Update("update tbl_account set name = #{name}, money = #{money} where id = #{id}")
void update(Account account);
@Select("select * from tbl_account")
List findAll();
@Select("select * from tbl_account where id = #{id}")
Account findById(Integer id);
}
业务层接口与实现(AccountService接口、AccountServiceImpl类):
java
// 业务接口
public interface AccountService {
void save(Account account);
void delete(Integer id);
void update(Account account);
List findAll();
Account findById(Integer id);
}
// 业务实现
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
public void save(Account account) {
accountDao.save(account);
}
public void update(Account account) {
accountDao.update(account);
}
public void delete(Integer id) {
accountDao.delete(id);
}
public Account findById(Integer id) {
return accountDao.findById(id);
}
public List findAll() {
return accountDao.findAll();
}
}
配置文件
数据库连接配置(resources下的jdbc.properties):
properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root
Spring主配置类:
java
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})
@EnableAspectJAutoProxy // 启用AOP自动代理
public class SpringConfig {
}
JDBC配置类:
java
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
DruidDataSource ds = new DruidDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(userName);
ds.setPassword(password);
return ds;
}
}
MyBatis配置类:
java
public class MybatisConfig {
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
ssfb.setTypeAliasesPackage("com.itheima.domain");
ssfb.setDataSource(dataSource);
return ssfb;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer msc = new MapperScannerConfigurer();
msc.setBasePackage("com.itheima.dao");
return msc;
}
}
编写Spring整合Junit的测试类
java
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
@Autowired
private AccountService accountService;
@Test
public void testFindById(){
Account ac = accountService.findById(2);
}
@Test
public void testFindAll(){
List all = accountService.findAll();
}
}
最终创建好的项目结构如下:

功能开发
1. 开启Spring AOP注解支持
在Spring的主配置文件SpringConfig类中添加注解以启用AOP功能:
java
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy // 关键:添加注解以启用AOP注解支持
public class SpringConfig {
}
作用:告诉Spring框架要处理AOP相关的注解,让切面类能够正常工作。
2. 创建AOP切面类
切面类,即@Aspect注解的整个Java类,包含:切入点定义(要拦截哪些方法)、通知定义(拦截后要做什么)、其他AOP相关配置
添加注解
- 该类要被Spring管理,需要添加@Component
- 要标识该类是一个AOP的切面类,需要添加@Aspect
- 配置切入点表达式,需要添加一个方法,并添加@Pointcut
java
@Component // 让Spring管理这个类
@Aspect // 声明这是一个切面类
public class ProjectAdvice {
// 定义切入点:拦截所有业务层方法
@Pointcut("execution(* com.itheima.service.*Service.*(..))") //匹配com.itheima.service包下所有以Service结尾的类中的所有方法。
private void servicePt(){}
// 环绕通知方法(待完善)
public void runSpeed(){
}
}
3. 实现环绕通知
基础环绕通知
在runSpeed()方法上添加@Around
java
@Around("servicePt()") // 引用上面定义的切入点
public Object runSpeed(ProceedingJoinPoint pjp) throws Throwable {
Object ret = pjp.proceed(); // 调用原始方法
return ret;
}
说明:此时只是调用了原始方法,还没有任何增强功能。
添加性能监控逻辑( 记录万次执行的时间)
java
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis(); // 记录开始时间
// 执行10000次来计算平均时间
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
long end = System.currentTimeMillis(); // 记录结束时间
System.out.println("业务层接口万次执行时间: " + (end - start) + "ms");
}
为什么执行10000次:
- 单次方法执行时间可能太短(几毫秒甚至更少)
- 执行多次取平均值,结果更准确可靠
- 尽可能避免因系统波动导致的测量误差
4. 优化输出信息
存在的问题 :
当前输出:业务层接口万次执行时间: 156ms
- 不知道是哪个类的方法
- 不知道是哪个具体方法
- 多个方法测试时无法区分
解决方案:获取方法签名信息
java
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
// 获取方法签名信息
Signature signature = pjp.getSignature();
String className = signature.getDeclaringTypeName(); // 获取声明类型的名称(通常是接口名)
String methodName = signature.getName(); // 获取方法名
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
long end = System.currentTimeMillis();
System.out.println("万次执行:" + className + "." + methodName + " ---> " + (end - start) + "ms");
}
签名信息详解
ProceedingJoinPoint pjp:连接点对象,包含被拦截方法的所有信息signature.getDeclaringTypeName():获取声明该方法的类型 的完整名称
- 在Spring默认的JDK动态代理中,获取的是接口名
- 如果使用CGLIB代理,获取的是实现类名
signature.getName():获取被拦截的方法名称
输出示例:
lua
万次执行:com.itheima.service.AccountService.findById ---> 234ms
万次执行:com.itheima.service.AccountService.findAll ---> 567ms
5. 完整代码实现
java
@Component
@Aspect
public class ProjectAdvice {
/**
* 切入点配置:拦截service包下所有Service结尾的类的所有方法
*/
@Pointcut("execution(* com.itheima.service.*Service.*(..))")
private void servicePt(){}
/**
* 环绕通知:性能监控
*/
@Around("servicePt()")
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
// 获取方法签名信息
Signature signature = pjp.getSignature();
String className = signature.getDeclaringTypeName();
String methodName = signature.getName();
// 记录开始时间
long start = System.currentTimeMillis();
// 执行10000次目标方法
for (int i = 0; i < 10000; i++) {
pjp.proceed();
}
// 记录结束时间并计算耗时
long end = System.currentTimeMillis();
long costTime = end - start;
// 输出性能监控结果
System.out.println("万次执行:" + className + "." + methodName + " ---> " + costTime + "ms");
}
}
6. 测试与结果分析
执行相应的单元测试,控制台会输出类似信息:
lua
万次执行:com.itheima.service.AccountService.findById ---> 156ms
万次执行:com.itheima.service.AccountService.findAll ---> 423ms
万次执行:com.itheima.service.UserService.save ---> 89ms
结果说明
- 这些时间是理论值,不是实际业务场景下的执行时间
- 实际性能受数据库连接、网络、数据量等多种因素影响
- 本案例主要目的是学习AOP的使用,不是精确的性能测试
AOP通知获取数据
目前我们写AOP仅仅是在原始方法前后追加一些操作,接下来我们要说说AOP中数据相关的内容。
在AOP中,我们可以从切入点方法中获取三种重要数据:
- 参数 - 方法调用时传入的值
- 返回值 - 方法执行后返回的结果
- 异常 - 方法执行过程中抛出的异常信息
各通知类型的数据获取能力
数据获取能力总览
| 通知类型 | 获取参数 | 获取返回值 | 获取异常 |
|---|---|---|---|
| 前置通知(@Before) | ✅ 可以 | ❌ 不可以 | ❌ 不可以 |
| 后置通知(@After) | ✅ 可以 | ❌ 不可以 | ❌ 不可以 |
| 返回后通知(@AfterReturning) | ✅ 可以 | ✅ 可以 | ❌ 不可以 |
| 异常后通知(@AfterThrowing) | ✅ 可以 | ❌ 不可以 | ✅ 可以 |
| 环绕通知(@Around) | ✅ 可以 | ✅ 可以 | ✅ 可以 |
详细说明
1. 获取参数的能力
- 所有通知类型都可以获取方法的参数
- 使用方式:
JoinPoint对象:适用于前置、后置、返回后、异常后通知
ProceedingJoinPoint对象:适用于环绕通知(它是JoinPoint的子类)
2. 获取返回值的能力
- 只有两种通知可以获取返回值:返回后通知(@AfterReturning)、环绕通知(@Around)
3. 获取异常信息的能力
- 只有两种通知可以获取异常信息:异常后通知(@AfterThrowing)、环绕通知(@Around)
环境准备
Maven依赖配置
在pom.xml中添加必要的依赖:
xml
org.springframework
spring-context
5.2.10.RELEASE
org.aspectj
aspectjweaver
1.9.4
依赖说明:
spring-context:Spring核心容器,提供IoC和AOP基础功能aspectjweaver:AspectJ切面编织器,让Spring能够处理AOP注解
添加数据访问层组件
接口:
java
public interface BookDao {
public String findName(int id);
}
实现类:
java
@Repository // 标识为数据访问层组件,由Spring管理
public class BookDaoImpl implements BookDao {
public String findName(int id) {
System.out.println("id:" + id); // 打印传入的参数
return "itcast"; // 返回固定字符串
}
}
组件说明:
@Repository:Spring注解,标记该类为数据访问层组件- 简单的
findName方法:接收一个id参数,返回字符串"itcast"
创建Spring配置类
java
@Configuration // 声明这是Spring配置类
@ComponentScan("com.itheima") // 扫描指定包下的组件
@EnableAspectJAutoProxy // 启用AOP自动代理功能
public class SpringConfig {
}
配置说明:
@Configuration:替代XML配置文件,用Java代码配置Spring@ComponentScan:自动扫描并注册指定包中的Spring组件@EnableAspectJAutoProxy:关键配置,启用AOP注解支持
编写AOP切面类
java
@Component // 让Spring管理这个切面类
@Aspect // 声明这是一个切面类
public class MyAdvice {
// 定义切入点:拦截BookDao的findName方法
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
// 前置通知 - 在目标方法执行前执行
@Before("pt()")
public void before() {
System.out.println("before advice ...");
}
// 后置通知 - 在目标方法执行后执行(无论成功或异常)
@After("pt()")
public void after() {
System.out.println("after advice ...");
}
// 环绕通知 - 最强大的通知类型,可以控制目标方法执行
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object ret = pjp.proceed(); // 执行目标方法
return ret;
}
// 返回后通知 - 在目标方法成功返回后执行
@AfterReturning("pt()")
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
// 异常后通知 - 在目标方法抛出异常后执行
@AfterThrowing("pt()")
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
应用程序入口App运行类
java
public class App {
public static void main(String[] args) {
// 1. 创建Spring容器,加载配置类
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
// 2. 从容器中获取BookDao实例(已经是代理对象)
BookDao bookDao = ctx.getBean(BookDao.class);
// 3. 调用方法,观察AOP通知的执行
String name = bookDao.findName(100);
// 4. 打印方法返回值
System.out.println(name);
}
}
获取参数
在AOP中,获取方法参数是指在通知方法中访问要传入被拦截方法的参数值。这让我们能够在方法执行前后检查和操作参数。
非环绕通知获取参数
使用JoinPoint对象来获取参数,适用于前置、后置、返回后、异常后通知:
java
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
@Before("pt()")
public void before(JoinPoint jp) {
// 获取方法的所有参数
Object[] args = jp.getArgs();
System.out.println("方法参数: " + Arrays.toString(args));
System.out.println("before advice ...");
}
}
多参数示例:
因为参数的个数是不固定的,所以使用数组更通配些。如果将参数改成两个会是什么效果呢:
修改BookDao接口和实现类:
java
public interface BookDao {
public String findName(int id, String password); // 添加第二个参数
}
@Repository
public class BookDaoImpl implements BookDao {
public String findName(int id, String password) {
System.out.println("id:" + id + ", password:" + password);
return "itcast";
}
}
修改App测试类:
java
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
String name = bookDao.findName(100, "itheima"); // 传入两个参数
System.out.println(name);
}
}
运行结果:
makefile
方法参数: [100, itheima]
before advice ...
id:100, password:itheima
itcast
关键说明
- 必须将
JoinPoint jp作为通知方法的参数jp.getArgs()方法:返回Object[]数组,包含所有参数值- 为什么用数组:因为方法参数个数不固定,数组可以通用地处理任意数量的参数
环绕通知获取参数
基本获取方式
环绕通知使用ProceedingJoinPoint类型参数(它是JoinPoint的子类):
java
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 获取参数
Object[] args = pjp.getArgs();
System.out.println("环绕通知获取参数: " + Arrays.toString(args));
// 执行原始方法
Object ret = pjp.proceed();
return ret;
}
}
修改参数
环绕通知不仅可以获取参数,还可以修改参数值:
java
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 1. 获取原始参数
Object[] args = pjp.getArgs();
System.out.println("原始参数: " + Arrays.toString(args));
// 2. 修改参数(例如将第一个参数改为666)
if (args.length > 0) {
args[0] = 666; // 修改第一个参数的值
}
// 3. 使用修改后的参数执行方法
Object ret = pjp.proceed(args); // 注意:这里传入修改后的参数数组
return ret;
}
执行结果:
bash
原始参数: [100, itheima]
id:666, password:itheima // 注意:id变成了666
itcast
proceed()方法的两种用法
无参数调用:当原始方法有参数,会在调用的过程中自动传入参数,适用于不需要修改参数的情况
java
Object ret = pjp.proceed(); // 自动使用原始参数
带参数调用:当需要修改原始方法的参数时,就只能采用带有参数的方法,适用于需要修改或过滤参数的情况
java
Object ret = pjp.proceed(modifiedArgs); // 使用修改后的参数
修改参数注意事项
- 修改参数时要确保参数类型和数量与原始方法匹配
- 参数数组索引从0开始,对应方法的第一个参数
- 在处理参数前最好进行空值检查和类型检查
- 修改参数要谨慎,避免破坏原始业务逻辑
获取返回值
在AOP中,获取返回值是指在通知方法中访问 被拦截方法的执行结果。只有两种通知类型具备这种能力,因为:
- 环绕通知(@Around):完全控制方法执行,自然可以获取返回值
- 返回后通知(@AfterReturning):专门为获取返回值设计
- 其他通知类型在方法执行时点无法或不应该获取返回值
环绕通知获取返回值
环绕通知通过ProceedingJoinPoint.proceed()的返回值直接获取:
java
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666;
Object ret = pjp.proceed(args); // 执行目标方法并获取返回值
return ret; // 返回获取到的值
}
}
环绕通知修改返回值
环绕通知的强大之处在于可以修改返回值:
java
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
Object[] args = pjp.getArgs();
System.out.println("原始参数: " + Arrays.toString(args));
args[0] = 666;
// 1. 执行原始方法并获取返回值
Object ret = pjp.proceed(args); // 假设原始方法返回"itcast"
// 2. 在环绕通知中修改返回值
System.out.println("原始返回值: " + ret);
// 3. 修改返回值(例如添加前缀)
if (ret != null) {
//ret.toString()将ret对象转换为字符串,.toUpperCase()将字符串转换为大写字母
ret = "修改后的返回值: " + ret.toString().toUpperCase();
}
System.out.println("修改后的返回值: " + ret);
return ret; // 返回修改后的值
}
}
返回后通知获取返回值
返回后通知使用@AfterReturning注解的returning属性来绑定返回值:
java
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
// returning属性指定参数名,该参数将接收返回值
@AfterReturning(value = "pt()", returning = "ret")
public void afterReturning(Object ret) {
System.out.println("afterReturning获取返回值: " + ret);
}
}
AfterReturning的三个重要细节
-
参数名必须匹配
@AfterReturning中的returning值必须与通知方法的参数名一致:
-
参数类型建议使用Object
java// 建议:使用Object类型,可以接收任何类型的返回值 @AfterReturning(value = "pt()", returning = "ret") public void afterReturning(Object ret) { System.out.println("返回值: " + ret); } // 也可以指定具体类型,但只能接收该类型或子类 public void afterReturning(String ret) { // 只能处理返回String类型的方法} -
如果通知方法有多个参数,
JoinPoint参数必须在第一位:
两种方式的对比
| 特性 | 环绕通知(@Around) | 返回后通知(@AfterReturning) |
|---|---|---|
| 获取返回值 | Object ret = pjp.proceed() |
使用returning属性绑定 |
| 修改返回值 | ✅ 可以修改返回值 | ❌ 只能读取,不能修改 |
| 控制方法执行 | ✅ 可以控制是否及何时执行原始方法 | ❌ 不能控制原始方法执行 |
| 参数要求 | 必须有ProceedingJoinPoint参数 |
可选JoinPoint参数,必须有返回值参数 |
| 异常处理 | 可以在try-catch中处理异常 | 方法正常返回才会执行 |
获取异常
只有两种通知类型可以获取方法的异常信息:
- 环绕通知(@Around):通过try-catch捕获异常
- 异常后通知(@AfterThrowing) :通过
throwing属性绑定异常
环绕通知获取异常
基本实现方式
环绕通知使用try-catch块来捕获和处理异常:
java
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Object[] args = pjp.getArgs();
System.out.println(Arrays.toString(args));
args[0] = 666;
Object ret = null;
try {
// 尝试执行原始方法
ret = pjp.proceed(args);
} catch (Throwable throwable) {
// 捕获到异常,可以获取异常信息
throwable.printStackTrace(); // 打印异常堆栈信息
}
return ret;
}
}
关键要点
- try-catch结构 :将
pjp.proceed()调用放在try块中- 异常捕获 :catch块可以捕获
Throwable(所有异常的父类)- 异常处理:捕获异常后可以进行日志记录、异常转换等操作
异常后通知获取异常
异常后通知使用@AfterThrowing注解的throwing属性来绑定异常:
java
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(* com.itheima.dao.BookDao.findName(..))")
private void pt(){}
@AfterThrowing(value = "pt()", throwing = "t")
public void afterThrowing(Throwable t) {
System.out.println("afterThrowing advice ..." + t);
}
}
AfterThrowing的三个重要细节
- 参数名必须匹配
@AfterThrowing注解中的throwing值必须与通知方法的参数名一致:\
- 参数类型建议使用Throwable,可以捕获所有类型的异常,也可以指定具体异常类型(如
NullPointerException) - 如果有
JoinPoint参数,必须放在异常参数之前:
两种方式的对比
| 特性 | 环绕通知(@Around) | 异常后通知(@AfterThrowing) |
|---|---|---|
| 获取异常方式 | 通过try-catch捕获 | 通过throwing属性绑定 |
| 能否阻止异常传播 | ✅ 可以(catch后不重新抛出) | ❌ 不能(执行后异常仍会传播) |
| 能否继续执行 | ✅ 可以在catch后继续执行其他逻辑 | ❌ 只是执行通知,异常仍会抛出 |
| 执行时机 | 在目标方法执行时捕获异常 | 在目标方法抛出异常后执行 |
| 能否获取参数 | ✅ 可以 | ✅ 可以(通过JoinPoint) |
百度网盘密码数据兼容处理
需求分析
问题背景 :接收方复制提取网盘密码时,可能无意间复制到多余空格(如:"1234 "或" 1234"),多输入一个空格可能会导致项目的功能无法正常使用,此时我们就想能不能将输入的参数先帮用户去掉空格再操作呢?
需求 :对百度网盘分享链接输入密码时,自动去除密码字符串前后的空格,避免因格式问题导致的访问失败。
\ 
技术实现
- 选择AOP(面向切面编程) :因为多个业务方法可能需要相同的处理,使用AOP可以避免代码重复,实现统一处理。
- 使用环绕通知:因为需要在方法执行前修改参数,然后使用修改后的参数调用原始方法,环绕通知可以满足这一需求。
实现步骤
- 定义切面,拦截目标方法。
- 在环绕通知中,在方法执行前,获取原始方法的参数。
- 遍历参数,对每个字符串类型的参数执行trim()操作(
string.trim():移除字符串string开头和结尾的所有空白字符)。 - 在环绕通知中,使用处理后的参数调用原始方法并返回其结果。
环境准备
-
创建一个Maven项目
-
在
pom.xml中添加必要的依赖:xmlorg.springframework spring-context 5.2.10.RELEASE org.aspectj aspectjweaver 1.9.4 -
添加ResourcesService,ResourcesServiceImpl,ResourcesDao和ResourcesDaoImpl
数据访问层(DAO)
ResourcesDao:javapublic interface ResourcesDao { /** * 验证资源访问权限 * @param url 资源链接 * @param password 提取密码 * @return 验证结果 */ boolean readResources(String url, String password); }ResourcesDaoImpl:
java@Repository public class ResourcesDaoImpl implements ResourcesDao { public boolean readResources(String url, String password) { // 模拟数据库存储的正确密码 String correctPassword = "root"; // 直接比较密码 return password.equals(correctPassword); } }业务逻辑层(Service)
ResourcesService:javapublic interface ResourcesService { /** * 打开资源链接 * @param url 资源链接 * @param password 用户输入的密码 * @return 是否成功访问 */ boolean openURL(String url, String password); }ResourcesServiceImpl:
java@Service public class ResourcesServiceImpl implements ResourcesService { @Autowired private ResourcesDao resourcesDao; public boolean openURL(String url, String password) { // 调用DAO层验证密码 return resourcesDao.readResources(url, password); } } -
创建Spring的配置类
java@Configuration @ComponentScan("com.example") // 扫描包路径下的组件 public class SpringConfig { // 基础配置,暂时不需要额外配置 } -
编写App运行类
javapublic class App { public static void main(String[] args) { // 1. 加载Spring配置类,创建Spring容器 ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); // 2. 从容器中获取ResourcesService类型的Bean ResourcesService resourcesService = ctx.getBean(ResourcesService.class); // 3. 调用业务方法:尝试打开指定URL,使用密码"root" boolean flag = resourcesService.openURL("http://pan.baidu.com/haha", "root"); // 4. 打印验证结果 System.out.println(flag); } }
最终创建好的项目结构如下:

现在项目的效果是,当输入密码为"root"控制台打印为true,如果密码改为"root "控制台打印的是false
我们需要在不修改现有业务代码的前提下,通过AOP解决这个问题。
具体实现
-
在Spring配置类中启用AOP注解功能:
java@Configuration @ComponentScan("com.example") // 扫描项目包 @EnableAspectJAutoProxy // 开启AOP注解支持 public class SpringConfig { // 配置类内容 } -
创建AOP切面类
java@Component @Aspect public class DataAdvice { // 定义切入点:com.example.service包下,所有Service结尾的类的所有恰有两个参数方法 // 方法返回类型为boolean @Pointcut("execution(boolean com.example.service.*Service.*(*,*))") private void servicePt() { // 方法体为空,仅用于定义切入点 } } -
编写环绕通知框架
java@Component @Aspect public class DataAdvice { @Pointcut("execution(boolean com.example.service.*Service.*(*,*))") private void servicePt() {} // 环绕通知:在切入点方法执行前后都可以执行逻辑 @Around("servicePt()") // @Around("DataAdvice.servicePt()")这两种写法都对 public Object trimStr(ProceedingJoinPoint pjp) throws Throwable { // 调用原始方法,暂不做任何处理 Object result = pjp.proceed(); return result; } } -
完成核心业务,实现参数去空格功能
java@Component @Aspect public class DataAdvice { @Pointcut("execution(boolean com.example.service.*Service.*(*,*))") private void servicePt() {} @Around("servicePt()") public Object trimStr(ProceedingJoinPoint pjp) throws Throwable { // 1. 获取原始方法的参数数组 Object[] args = pjp.getArgs(); // 2. 遍历所有参数 for (int i = 0; i < args.length; i++) { // 3. 判断参数是否为字符串类型 if(args[i].getClass().equals(String.class)){ args[i] = args[i].toString().trim(); } } // 5. 使用处理后的参数调用原始方法 Object result = pjp.proceed(args); // 6. 返回原始方法的执行结果 return result; } }
args[i].toString():将Object类型的args[i]转换为字符串类型
-
验证效果
运行App主程序,测试不同密码,不管密码
root前后是否加空格,最终控制台打印的都是true验证方式2:添加日志验证
修改DAO层实现,打印密码长度以验证AOP生效:
java@Repository public class ResourcesDaoImpl implements ResourcesDao { public boolean readResources(String url, String password) { // 打印接收到的密码和长度 System.out.println("接收密码: '" + password + "'"); System.out.println("密码长度: " + password.length()); // 模拟校验 return password.equals("root"); } }运行结果(不管密码
root前后是否加空格):arduino接收密码: 'root' 密码长度: 4 true
AOP事务管理
Spring事务简介
核心概念与作用
事务的核心作用 :确保一系列相关的数据库操作(例如多条SQL语句)作为一个不可分割的单元来执行。这些操作要么全部成功 (提交),要么在遇到问题时全部撤销(回滚),以此保障数据的一致性和完整性。
Spring事务的扩展作用 :将上述"同成功同失败"的保障能力,从传统的数据层 (如DAO层),扩展并提升到业务层(Service层)。这是Spring框架在事务管理上提供的关键价值。
事务需要提升到业务层的原因:
数据层的事务边界(通常是一个独立的方法调用)无法覆盖"跨多个数据库操作的完整业务单元"。一个典型的业务场景可以清晰地说明这个问题。
转账案例 - 需求分析
以银行转账业务为例:
- 业务逻辑:从A账户转100元到B账户。
- 对应的数据层操作 :
- 操作1 :从A账户的余额中
减100 - 操作2 :向B账户的余额中
加100
- 操作1 :从A账户的余额中
假设事务仅在数据层管理:
减100操作会对A账户开启并提交(或回滚)一个独立的事务Tx1。加100操作会对B账户开启并提交(或回滚)另一个独立的事务Tx2。- 风险 :如果
Tx1成功提交后,系统在执行Tx2前发生故障(如网络中断、服务器宕机),结果将是A账户的钱已扣除,但B账户的钱未到账,导致数据严重不一致。
结论 :必须将 减100 和 加100 这两个数据层操作,捆绑在同一个业务层事务中。Spring事务管理正是为了解决这一问题,它能确保一个业务方法内部所有的数据库操作,处于同一个事务管辖之下。
转账案例 - 环境搭建
1. 数据库准备
首先创建数据库和表结构:
sql
-- 创建数据库
create database spring_db character set utf8;
-- 使用数据库
use spring_db;
-- 创建账户表
create table tbl_account(
id int primary key auto_increment,
name varchar(35),
money double
);
-- 插入测试数据
insert into tbl_account values(1,'Tom',1000);
insert into tbl_account values(2,'Jerry',1000);
说明:创建了两个账户,初始余额各1000元,用于模拟转账操作。
2. 项目依赖配置(pom.xml)
项目使用Maven构建,需要添加以下依赖:
xml
org.springframework
spring-context
5.2.10.RELEASE
com.alibaba
druid
1.1.16
org.mybatis
mybatis
3.5.6
mysql
mysql-connector-java
5.1.47
org.springframework
spring-jdbc
5.2.10.RELEASE
org.mybatis
mybatis-spring
1.3.0
junit
junit
4.12
test
org.springframework
spring-test
5.2.10.RELEASE
3. 实体类(Account.java)
对应数据库表结构:
java
public class Account implements Serializable {
private Integer id;
private String name;
private Double money;
// 构造方法
public Account() {}
public Account(Integer id, String name, Double money) {
this.id = id;
this.name = name;
this.money = money;
}
// Getter和Setter方法
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getMoney() {
return money;
}
public void setMoney(Double money) {
this.money = money;
}
// toString方法
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", money=" + money +
'}';
}
}
4. 数据访问层(DAO)
定义账户操作接口:
java
public interface AccountDao {
// 使用MyBatis的注解方式
/**
* 转入操作(增加金额)
* @param name 账户名称
* @param money 转账金额
*/
@Update("UPDATE tbl_account SET money = money + #{money} WHERE name = #{name}")
void inMoney(@Param("name") String name, @Param("money") Double money);
/**
* 转出操作(减少金额)
* @param name 账户名称
* @param money 转账金额
*/
@Update("UPDATE tbl_account SET money = money - #{money} WHERE name = #{name}")
void outMoney(@Param("name") String name, @Param("money") Double money);
}
5. 业务逻辑层(Service)
定义转账业务接口和实现:
java
public interface AccountService {
/**
* 转账操作
* @param out 转出账户名
* @param in 转入账户名
* @param money 转账金额
*/
void transfer(String out, String in, Double money);
}
java
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public void transfer(String out, String in, Double money) {
// 第一步:从转出账户扣除金额
accountDao.outMoney(out, money);
// 第二步:向转入账户增加金额
accountDao.inMoney(in, money);
}
}
注意:当前的实现存在事务问题!如果在两个操作之间发生异常,会导致数据不一致。
6. 数据库连接配置(jdbc.properties)
properties
# 数据库驱动
jdbc.driver=com.mysql.jdbc.Driver
# 数据库连接URL
# useSSL=false表示禁用SSL连接
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
# 数据库用户名
jdbc.username=root
# 数据库密码
jdbc.password=root
7. JDBC配置类(JdbcConfig.java)
java
@PropertySource("classpath:jdbc.properties")
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
/**
* 创建数据源Bean
* @return 数据源对象
*/
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(userName);
dataSource.setPassword(password);
return dataSource;
}
}
8. MyBatis配置类(MybatisConfig.java)
java
public class MybatisConfig {
/**
* 配置SqlSessionFactory
* @param dataSource 数据源
* @return SqlSessionFactoryBean
*/
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
// 设置实体类所在的包
factoryBean.setTypeAliasesPackage("com.itheima.domain");
// 设置数据源
factoryBean.setDataSource(dataSource);
return factoryBean;
}
/**
* 配置Mapper接口扫描
* @return MapperScannerConfigurer
*/
@Bean
public MapperScannerConfigurer mapperScannerConfigurer() {
MapperScannerConfigurer configurer = new MapperScannerConfigurer();
// 设置DAO接口所在的包
configurer.setBasePackage("com.itheima.dao");
return configurer;
}
}
9. Spring主配置类(SpringConfig.java)
java
@Configuration // 标记为配置类
@ComponentScan("com.itheima") // 扫描组件
@PropertySource("classpath:jdbc.properties") // 加载属性文件
@Import({JdbcConfig.class, MybatisConfig.class}) // 导入其他配置类
public class SpringConfig {
// 主配置类,整合所有配置
}
10. 测试类(AccountServiceTest.java)
java
@RunWith(SpringJUnit4ClassRunner.class) // 使用Spring测试运行器
@ContextConfiguration(classes = SpringConfig.class) // 指定配置类
public class AccountServiceTest {
@Autowired
private AccountService accountService;
@Test
public void testTransfer() {
// 测试转账:从Tom账户转100元到Jerry账户
accountService.transfer("Tom", "Jerry", 100D);
// 预期结果:
// Tom账户:1000 - 100 = 900元
// Jerry账户:1000 + 100 = 1100元
}
}
最终创建好的项目结构如下:

存在的问题
当前的实现存在一个重要问题:缺乏事务管理 以确保两个操作(扣款和收款)要么全部成功,要么全部失败,否则在转账过程中出现异常,可能导致数据不一致。
目前,我们搭建了基本的Spring+MyBatis整合环境,后面将通过Spring事务管理来解决这个问题。
转账案例 - 事务管理
1. 在需要被事务管理的方法上添加事务注解
1.1 业务接口(无改动)
java
public interface AccountService {
/**
* 转账操作
* @param out 转出账户名
* @param in 转入账户名
* @param money 转账金额
*/
// 此处不需要@Transactional注解,建议在实现类上添加
void transfer(String out, String in, Double money);
}
1.2 业务实现类(添加事务管理)
java
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
// 方法上添加@Transactional注解,表示该方法需要事务管理
@Transactional
@Override // 注解实现的接口方法
public void transfer(String out, String in, Double money) {
// 第一步:从转出账户扣除金额
accountDao.outMoney(out, money);
// 模拟异常:这里会抛出算数异常ArithmeticException
int i = 1 / 0;
// 第二步:向转入账户增加金额(如果上面出现异常,这行代码不会执行)
accountDao.inMoney(in, money);
}
}
@Transactional注解详解
作用位置:
-
接口类上:该接口的所有实现类的所有方法都会有事务
java@Transactional public interface AccountService { void transfer(String out, String in, Double money); } -
接口方法上:该接口的所有实现类的该方法都会有事务
javapublic interface AccountService { @Transactional void transfer(String out, String in, Double money); } -
实现类上:该类中的所有方法都会有事务
java@Service @Transactional public class AccountServiceImpl implements AccountService { // 类中所有方法都会自动获得事务管理 } -
实现类方法上:只有该方法有事务
java@Service public class AccountServiceImpl implements AccountService { @Transactional public void transfer(String out, String in, Double money) { // 只有这个方法有事务 } }
最佳实践建议:
- 将
@Transactional注解添加在实现类或实现类的方法上 - 原因:Spring的事务管理是通过AOP实现的,注解在接口上可能在某些情况下不生效
2. 配置事务管理器
在JdbcConfig配置类中添加事务管理器配置:
java
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String userName;
@Value("${jdbc.password}")
private String password;
/**
* 配置数据源
* @return 数据源对象
*/
@Bean
public DataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(userName);
dataSource.setPassword(password);
return dataSource;
}
/**
* 配置事务管理器
* MyBatis使用的是JDBC事务,JDBC事务管理器需用DataSourceTransactionManager创建
* @param dataSource 数据源对象,Spring会自动注入
* @return 事务管理器
*/
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
// 创建JDBC事务管理器
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
// 设置数据源,事务管理器需要知道管理哪个数据库的事务
transactionManager.setDataSource(dataSource);
return transactionManager;
}
}
为什么需要事务管理器?
Spring本身不直接管理事务,而是通过事务管理器来协调各种持久层技术(JDBC、Hibernate等)的事务。不同的技术需要不同的事务管理器实现。
不同的持久层技术使用不同的事务管理器:
- JDBC/MyBatis:DataSourceTransactionManager
- JPA:JpaTransactionManager
- JTA(分布式事务):JtaTransactionManager
public PlatformTransactionManager transactionManager(DataSource dataSource)方法详解:
- 方法的作用:这个方法用于定义一个Spring Bean,这个Bean是PlatformTransactionManager类型。Spring容器会调用这个方法,并将返回的对象作为一个Bean注册到容器中。事务管理器是Spring事务管理的核心,它负责事务的开启、提交、回滚等操作。
- 参数说明:方法有一个参数:DataSource dataSource。Spring容器会自动根据类型注入一个DataSource Bean。这个DataSource就是我们在JdbcConfig中定义的dataSource()方法返回的数据源。事务管理器需要操作数据库,因此它需要知道数据源。
- 为什么需要这样做:
在Spring中,如果我们想要使用声明式事务(即通过@Transactional注解),我们必须配置一个事务管理器。因为Spring事务管理是基于AOP的,它会在运行时为带有@Transactional注解的方法创建代理,代理中会调用事务管理器来管理事务。
具体来说,当我们调用一个被@Transactional注解的方法时,Spring会通过事务管理器开启一个事务,然后执行方法,如果方法执行成功,则提交事务,如果方法抛出异常,则回滚事务。 - 注意:在Spring Boot中,如果我们引入了相应的依赖(如spring-boot-starter-jdbc),Spring Boot会自动配置一个事务管理器。但在我们现在的Spring项目中,我们需要手动配置。
3. 开启事务注解驱动
在Spring主配置类上添加@EnableTransactionManagement注解:
java
@Configuration // 标记为配置类
@ComponentScan("com.itheima") // 扫描组件,包括@Service
@PropertySource("classpath:jdbc.properties") // 加载数据库配置
@Import({JdbcConfig.class, MybatisConfig.class}) // 导入其他配置
@EnableTransactionManagement // 关键:开启注解式事务管理
public class SpringConfig {
// 主配置类,整合所有配置
}
@EnableTransactionManagement的作用:
- 启用Spring的注解驱动事务管理功能
- 告诉Spring容器扫描@Transactional注解,为标记@Transactional的方法创建AOP代理对象
- 代理对象会在方法执行时自动管理事务的开启、提交和回滚
4. 运行测试类
测试执行流程:
- 开始执行
transfer()方法 - Tom账户扣款100元成功
- 遇到
int i = 1/0异常,程序中断 - Spring检测到异常,自动回滚事务
- Tom账户扣款的SQL操作被撤销
- Jerry账户加款操作未执行
最终结果:
- 两个账户的金额都保持原样(各1000元)
- 数据一致性得到保障
事务管理工作原理图解
sql
[调用者]
|
| 调用transfer()方法
↓
+----------------------+
| Spring AOP 代理对象 | ← ① Spring为Service创建代理
+----------------------+
|
| ② 代理对象开始事务
↓
+----------------------+
| 开启数据库事务 |
| 设置自动提交为false |
+----------------------+
|
| ③ 执行业务方法
↓
+----------------------+
| accountDao.outMoney | ← 执行SQL1:Tom扣款
+----------------------+
|
| ④ 继续执行业务
↓
+----------------------+
| accountDao.inMoney | ← 执行SQL2:Jerry加款
+----------------------+
|
| ⑤ 业务执行完成
↓
+----------------------+
| 提交事务 | ← 所有SQL成功,提交事务
| (commit) |
+----------------------+
OR
+----------------------+
| 回滚事务 | ← 出现异常,回滚事务
| (rollback) | 撤销所有SQL操作
+----------------------+
Spring事务角色
核心概念解析
事务管理员(Transaction Administrator)
- 定义 :发起和控制事务的一方,通常是业务层(Service层)
- 职责 :
- 开启一个新的事务
- 定义事务的边界(从何处开始,到何处结束)
- 决定事务的提交或回滚
- 实现方式 :通过在业务方法上添加
@Transactional注解
事务协调员(Transaction Coordinator)
- 定义 :加入并参与现有事务的一方,通常是数据层(DAO层),也可以是业务层方法
- 职责 :
- 加入已经存在的事务
- 在事务中执行具体的数据库操作
- 不独立控制事务的提交或回滚
- 实现方式:通过Spring的事务管理机制自动参与事务
场景分析
无Spring事务管理的场景分析:两个数据库操作分别在两个独立事务中,无法保障数据一致性

有Spring事务管理的场景分析:
- transfer上添加了@Transactional注解,在该方法上就会有一个事务T
- AccountDao的outMoney方法的事务T1加入到transfer的事务T中
- AccountDao的inMoney方法的事务T2加入到transfer的事务T中
这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。

数据源一致性要求
java
@Configuration
public class JdbcConfig {
@Bean
public DataSource dataSource() {
// 创建数据源
return new DruidDataSource();
}
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
// 事务管理器使用同一个数据源
return new DataSourceTransactionManager(dataSource);
}
@Bean
public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
// MyBatis使用同一个数据源
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
return factory;
}
}
为什么事务管理器、MyBatis必须引用同一个DataSource Bean实例?
- 事务管理器在开启事务时,需要数据源实例来获取数据库连接,并将连接与当前线程绑定,以管理事务。
MyBatis执行SQL时也需要从数据源实例获取连接。 - 如果事务管理器和MyBatis使用的是同一个数据源实例DataSource Bean,那么Spring就能确保它们使用同一个数据库连接,从而使得事务能够正确管理。
- 反之,如果事务管理器和MyBatis使用了不同的数据源实例(即使这两个数据源的配置完全相同,指向同一个数据库),那么它们将使用两个独立的数据库连接,事务管理器管理的连接和MyBatis执行SQL使用的连接不是同一个,事务就无法正常工作。
上述为了简化演示将数据源、事务管理器和MyBatis配置全部放在JdbcConfig类中,实际开发建议为数据源、MyBatis、事务管理分别创建配置类
Spring事务属性
事务配置
在Spring中,@Transactional注解提供了丰富的事务配置选项,利用这些属性可以精确控制事务的行为。
核心事务属性详解

1. readOnly 属性
作用:
true:声明为只读事务,适用于查询操作false:声明为读写事务,适用于增删改操作
java
// 查询方法使用只读事务
@Transactional(readOnly = true)
public Account findById(Integer id) {
return accountDao.findById(id);
}
// 增删改方法使用读写事务
@Transactional(readOnly = false)
public void updateAccount(Account account) {
accountDao.update(account);
}
2. timeout 属性
作用:
- 设置事务的完成时间,单位:秒
- 事务在规定时间内未完成则自动回滚
- 默认值:-1(不设置超时,使用数据库的默认超时设置,不同数据库的默认超时设置不同)
java
// 设置10秒超时
@Transactional(timeout = 10)
public void batchProcess() {
// 复杂的批量处理逻辑
}
3. rollbackFor 和 noRollbackFor 属性
Spring事务默认只对以下异常进行回滚:
- Error异常:JVM严重错误
- RuntimeException异常及其子类:运行时异常
检查型异常(Checked Exception)默认不会触发事务回滚。
业务异常属于哪一类,完全取决于开发者如何设计,继承RuntimeException就属于RuntimeException异常(大多数),继承Exception就属于检查型异常。
rollbackFor 和 noRollbackFor的优先级:Spring会优先匹配noRollbackFor。如果一个异常同时匹配rollbackFor和noRollbackFor,则以noRollbackFor为准(不回滚)。
自定义回滚规则 :
指定特定异常触发回滚:
@Transactional(rollbackFor/noRollbackFor = 异常类名.class)@Transactional(rollbackFor/noRollbackFor = {异常类名1.class, 异常名类2.class})(当指定多个异常类时,使用数组形式)
java
// 指定特定异常触发回滚
@Transactional(rollbackFor = {IOException.class, SQLException.class})
public void transfer(String out, String in, Double money) throws IOException {
accountDao.outMoney(out, money);
throw new IOException("模拟异常");
// 现在IOException会触发事务回滚
}
// 指定特定异常不触发回滚
@Transactional(noRollbackFor = {BusinessException.class})
public void processBusiness() {
// BusinessException发生时,事务不会回滚
throw new BusinessException("业务异常,但不回滚事务");
}
字符串""配置方式:
rollbackForClassName等同于rollbackFor,但属性为异常的类全名字符串noRollbackForClassName等同于noRollbackFor,但属性为异常的类全名字符串@Transactional(rollbackForClassName/noRollbackForClassName = "异常的类全名字符串")(当指定多个异常类时,使用数组形式)
java
// 使用异常类全名(字符串形式)
@Transactional(
rollbackForClassName = {"java.io.IOException", "java.sql.SQLException"},
noRollbackForClassName = "com.example.BusinessException"
)
写法总结一下:
- 一个
@Transactional()注解可以同时包含rollbackFor、noRollbackFor、rollbackForClassName、noRollbackForClassName四个异常配置属性(以及其他非异常属性),这些属性之间用逗号分隔。 - 每个属性内部可以指定一个或多个异常类的Class对象或全限定名字符串
"" - 如果是多个异常类,则用逗号分隔并用大括号
{}包裹(数组形式)。
Java标准库的核心异常类
textThrowable (所有异常/错误的基类) ├── Error (错误) │ ├── VirtualMachineError (虚拟机错误) │ │ ├── OutOfMemoryError (内存溢出) │ │ ├── StackOverflowError (栈溢出) │ │ └── InternalError (内部错误) │ ├── LinkageError (链接错误) │ │ ├── NoClassDefFoundError (类定义未找到) │ │ └── ClassFormatError (类格式错误) │ └── AssertionError (断言错误) └── Exception (异常) ├── RuntimeException (运行时异常) │ ├── NullPointerException (空指针) │ ├── IllegalArgumentException (非法参数) │ ├── IndexOutOfBoundsException (索引越界) │ ├── ClassCastException (类型转换) │ └── ArithmeticException (算术异常) └── Checked Exception (检查型异常) ├── IOException (IO异常) ├── SQLException (SQL异常) ├── ParseException (解析异常) └── ClassNotFoundException (类未找到异常)
- 业务异常都是自定义的异常
4. isolation 属性
作用:设置事务的隔离级别。
写法:@Transactional(isolation = Isolation.隔离级别)
隔离级别从低到高:
- DEFAULT(默认)
- 使用底层数据库的默认隔离级别
- MySQL默认:REPEATABLE_READ(可重复读)
- Oracle默认:READ_COMMITTED(读已提交)
- READ_UNCOMMITTED(读未提交)
- 优点:性能最高
- 缺点:可能读取到其他事务未提交的数据(脏读)
- 使用场景:对数据一致性要求极低,追求性能的场景
- READ_COMMITTED(读已提交,Oracle默认级别)
- 优点:解决脏读问题
- 缺点:不可重复读(同一事务内两次读取结果可能不同)
- 使用场景:大多数业务场景
- REPEATABLE_READ(可重复读,MySQL默认级别)
- 优点:解决脏读、不可重复读问题
- 缺点:幻读(同一查询条件,多次查询返回的行数不同)
- 使用场景:对数据一致性要求较高的场景
- SERIALIZABLE(串行化)
- 优点:解决所有并发问题
- 缺点:性能最低(完全串行执行)
- 使用场景:对数据一致性要求极高,可接受性能损失的场景
java
// 使用READ_COMMITTED隔离级别
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateAccount(Account account) {
// 在高并发环境下,确保读取已提交的数据
accountDao.update(account);
}
// 使用SERIALIZABLE隔离级别处理敏感操作
@Transactional(isolation = Isolation.SERIALIZABLE)
public void processFinancialTransaction() {
// 财务交易,需要最高级别的一致性保证
}
转账业务追加日志案例
需求分析
在原有的银行转账功能基础上,新增日志记录功能,要求:
- 每次转账操作都要在数据库中留下记录
- 无论转账是否成功,都必须记录日志
- 日志内容应包含:转出账户、转入账户、转账金额、操作时间
环境准备
该环境是基于转账环境来完成的,所以环境的准备可以参考AOP事务管理 - Spring事务简介 - 转账案例-环境搭建,在其基础上,我们继续往下写
1. 数据库表结构
创建日志表,用于记录转账操作:
sql
create table tbl_log(
id int primary key auto_increment,
info varchar(255),
createDate datetime
);
2. 数据访问层(DAO)
添加日志数据访问接口LogDao:
java
public interface LogDao {
// 插入日志记录,使用当前时间
@Insert("insert into tbl_log (info,createDate) values(#{info},now())")
void log(String info);
}
3. 业务逻辑层(Service)
添加日志服务接口LogService及其实现类:
java
// 日志服务接口
public interface LogService {
void log(String out, String in, Double money);
}
// 日志服务实现类
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
// 日志记录方法,也有事务管理
@Transactional
public void log(String out, String in, Double money) {
// 拼接日志信息
String logInfo = "转账操作由" + out + "到" + in + ",金额:" + money;
logDao.log(logInfo);
}
}
在转账的业务中添加记录日志:
java
// 转账服务接口
public interface AccountService {
/**
* 转账操作
* @param out 转出账户
* @param in 转入账户
* @param money 转账金额
*/
void transfer(String out, String in, Double money) throws IOException;
}
// 转账服务实现类(问题版本)
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private LogService logService;
// 转账方法,有事务管理
@Transactional
public void transfer(String out, String in, Double money) {
try {
// 执行转账操作
accountDao.outMoney(out, money); // 转出
// int i = 1 / 0; // 模拟异常
accountDao.inMoney(in, money); // 转入
} finally {
// 期望无论转账是否成功,都记录日志
logService.log(out, in, money);
}
}
}
java
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
// 方法上添加@Transactional注解,表示该方法需要事务管理
@Transactional
@Override // 注解实现的接口方法
public void transfer(String out, String in, Double money) {
// 第一步:从转出账户扣除金额
accountDao.outMoney(out, money);
// 模拟异常:这里会抛出算数异常ArithmeticException
int i = 1 / 0;
// 第二步:向转入账户增加金额(如果上面出现异常,这行代码不会执行)
accountDao.inMoney(in, money);
}
}
运行程序
- 当程序正常运行,tbl_account表中转账成功,tbl_log表中日志记录成功
- 当转账业务之间出现异常(int i =1/0)时,转账失败,tbl_account表成功回滚,但是tbl_log表未添加数据,结果失败
失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败
目标效果:无论转账操作是否成功,日志必须保留
事务传播行为

对于上述案例的分析:
transfer()方法开启事务T,包含inMoney()方法和outMoney()方法log()方法因为加了@Transactional注解,开启了事务T2。log()方法默认传播行为是REQUIRED,会加入现有事务T- 当转账失败时,事务T回滚,导致
log()方法的操作也被回滚 - 最终结果:转账失败时日志没有被记录
要想解决这个问题,就需要用到事务传播行为
事务传播行为:指的是事务协调员(被调用的方法)对事务管理员(调用方 方法)所携带事务的处理态度。
在这个案例中的通俗理解:
- 当事务管理员(如
transfer()方法)调用事务协调员(如log()方法)时 - 事务协调员需要决定:是加入调用方的事务,还是自己开启新事务,或是不在事务中执行
- 这个决定就是事务传播行为
修改logService以改变事务的传播行为
将log()方法的事务传播行为改为REQUIRES_NEW:
java
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
// 设置传播行为为REQUIRES_NEW,始终开启新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out, String in, Double money) {
String logInfo = "转账操作由" + out + "到" + in + ",金额:" + money;
logDao.log(logInfo);
}
}
修改后的执行流程
转账成功的场景:
r
1. transfer()方法开启事务T
2. 执行accountDao.outMoney()(在事务T中)
3. 执行accountDao.inMoney()(在事务T中)
4. 调用logService.log()
5. 由于log()方法的传播行为是REQUIRES_NEW,挂起事务T,开启新事务T1
6. 在事务T1中执行日志记录
7. 提交事务T1(日志被记录)
8. 恢复事务T
9. 提交事务T(转账成功)
转账失败的场景:
r
1. transfer()方法开启事务T
2. 执行accountDao.outMoney()(在事务T中)
3. 执行int i = 1/0(抛出异常)
4. 调用logService.log()(在finally块中)
5. 由于log()方法的传播行为是REQUIRES_NEW,挂起事务T,开启新事务T1
6. 在事务T1中执行日志记录
7. 提交事务T1(日志被记录)
8. 恢复事务T
9. 事务T因异常而回滚(转账失败)
效果:修改后,无论转账是否成功,日志都会被记录。log()方法在自己的独立事务中执行,不受transfer()方法事务的影响。
事务传播行为的可选值
Spring提供了7种事务传播行为,每种行为都有特定的用途:

- REQUIRED(默认值)
- 含义:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务
- 使用场景:大多数业务方法适用,确保操作在事务中执行
java
@Transactional(propagation = Propagation.REQUIRED)
public void updateAccount(Account account) {
// 默认行为,无需显式指定
}
- REQUIRES_NEW
- 含义:始终创建一个新事务,如果当前存在事务,则挂起当前事务
- 使用场景:需要独立提交的操作,如日志记录、审计跟踪
java
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void auditLog(String operation) {
// 独立事务,不受调用方事务影响
}
- SUPPORTS
- 含义:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行
- 使用场景:查询方法,可以在事务中执行,也可以不在事务中执行
java
@Transactional(propagation = Propagation.SUPPORTS)
public List findAll() {
// 查询方法,可以支持事务但不是必须
}
- NOT_SUPPORTED
- 含义:以非事务方式执行操作,如果当前存在事务,则挂起当前事务
- 使用场景:不支持事务的操作,如发送邮件、调用外部API
java
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendEmail(String to, String content) {
// 非事务执行,即使调用方有事务也会被挂起
}
- MANDATORY
- 含义:必须在事务中执行,如果当前没有事务,则抛出异常
- 使用场景:强制要求调用方必须开启事务的方法
java
@Transactional(propagation = Propagation.MANDATORY)
public void financialTransaction(Transaction tx) {
// 必须在事务中调用,否则抛出异常
}
- NEVER
- 含义:必须在非事务状态下执行,如果当前存在事务,则抛出异常
- 使用场景:明确禁止在事务中执行的方法
java
@Transactional(propagation = Propagation.NEVER)
public void generateReport() {
// 不能在事务中调用
}
- NESTED
- 含义:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则创建新事务
- 使用场景:需要部分回滚的场景,嵌套事务可以独立回滚而不影响外部事务
- 注意:并非所有数据库都支持嵌套事务,MySQL的InnoDB引擎通过保存点(savepoint)实现
java
@Transactional(propagation = Propagation.NESTED)
public void processItem(Item item) {
// 嵌套事务,可以独立回滚
}
判断方法是事务管理员还是事务协调员,或者都不是
text
判断方法的事务角色
│
├─ 判断:方法是否有@Transactional注解?
│ │
│ ├─ 否 → 不是事务协调员,也不是事务管理员,只是普通方法
│ │
│ └─ 是 → 是事务相关方法
│ │
│ └─ 传播行为决定角色
│ │
│ ├─ 情况1:被调用时调用方无事务
│ │ │
│ │ ├─ REQUIRED/MANDATORY/NESTED: 开启新事务 → 事务管理员
│ │ │
│ │ ├─ REQUIRES_NEW: 开启新事务 → 事务管理员
│ │ │
│ │ └─ SUPPORTS/NOT_SUPPORTED/NEVER: 无事务执行 → 都不是(普通方法)
│ │
│ └─ 情况2:被调用时调用方有事务
│ │
│ ├─ REQUIRED/MANDATORY/SUPPORTS: 加入现有事务 → 事务协调员
│ │
│ ├─ REQUIRES_NEW: 开启新事务 → 事务管理员
│ │
│ ├─ NOT_SUPPORTED/NEVER: 无事务执行 → 都不是(普通方法)
│ │
│ └─ NESTED: 创建嵌套事务 → 特殊角色(既是协调员又是管理员)