Spring 第四天:AOP 面向切面编程与声明式事务管理
前言
Spring 有两大核心:一个是前几天我们重点攻克的 IoC/DI,另一个就是今天要深入学习的 AOP(面向切面编程)。
还记得那句话吗?“AOP 是在不改变原有代码的前提下对其进行功能增强”。听起来很神奇对吧?今天我们就来揭开它神秘的面纱,还会学到 AOP 最重要的实际应用——声明式事务管理。掌握了事务,你的程序才能真正做到数据安全可靠。
💬 课前唠一唠:你有没有遇到过这种场景——想在每个方法执行前后都加个日志或计时,结果发现要改几十个地方?今天学完 AOP,你可以用几行代码就搞定这件事。你最想用 AOP 解决什么重复代码?评论区许个愿,说不定今天的案例就能帮你实现。
一、AOP 简介
1.1 什么是 AOP?
- AOP(Aspect Oriented Programming)面向切面编程,是一种编程范式,指导开发者如何组织程序结构。
- 我们熟悉的 OOP(面向对象编程)是另一种编程范式,两者互为补充。
1.2 AOP 的作用
在不惊动原始设计的基础上,为方法进行功能增强。
说白了就是:原来有一段代码已经写好了,现在想给它加点功能(比如打印日志、统计时间),但又不想改原有代码,AOP 就是干这个的。
💡 本质:Spring AOP 底层采用的是代理模式,后面我们会验证。
1.3 AOP 核心概念
我们来通过一个场景理解几个关键术语:
假设
BookDaoImpl有save()、update()、delete()、select()四个方法。我们想给update和delete增加“计算万次执行时间”的功能,但不改原代码。
| 概念 | 解释 | 举例 |
|---|---|---|
| 连接点(JoinPoint) | 程序执行过程中能插入增强的任意位置 | BookDaoImpl 中的所有方法 |
| 切入点(Pointcut) | 真正需要增强的方法(匹配连接点的式子) | update() 和 delete() |
| 通知(Advice) | 抽取出来的共性功能(要增强的内容) | “计算万次执行时间”的方法 |
| 通知类 | 定义通知的类 | MyAdvice 类 |
| 切面(Aspect) | 描述通知与切入点的对应关系 | “在 update() 和 delete() 上应用计时的通知” |
🧠 一句话记忆:连接点是“地点”,切入点是“我选中的地点”,通知是“要干的事”,切面是“在哪里干什么”。
二、AOP 入门案例
需求:在方法执行前打印当前系统时间,不改原代码。
2.1 环境准备
创建 Maven 项目,添加 spring-context 依赖,创建 BookDao、BookDaoImpl、SpringConfig 配置类和 App 运行类。项目结构如下:
src/main/java/com/itheima
├── config/SpringConfig
├── dao/BookDao (接口)
├── dao/impl/BookDaoImpl (实现类)
└── App (运行类)
2.2 AOP 实现步骤
步骤 1:导入 AOP 依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
说明:
spring-context已包含spring-aop,这里只需导入 AspectJ 的织入包。Spring 整合 AspectJ 是目前最常用的 AOP 开发方式。
步骤 2:定义通知类与通知
@Component
@Aspect // 标识这是一个切面类
public class MyAdvice {
@Pointcut("execution(void com.itheima.dao.BookDao.update())")
private void pt(){} // 切入点定义:无参数、无返回值、方法体为空
@Before("pt()") // 绑定通知到切入点,在方法执行前运行
public void method() {
System.out.println(System.currentTimeMillis());
}
}
步骤 3:在配置类上开启 AOP 注解功能
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy // 开启注解格式 AOP 功能
public class SpringConfig { }
📝 关键注解速查:
@EnableAspectJAutoProxy:开启 AOP 注解支持@Aspect:声明切面类@Pointcut:定义切入点@Before:前置通知
三、AOP 工作流程
3.1 工作流程四步走
- Spring 容器启动:加载需要增强的类和通知类(此时 bean 还未创建)
- 读取切入点配置:只读取被实际使用的切入点(没被通知绑定的切入点忽略)
- 初始化 bean,匹配切入点:
- 匹配失败 → 创建原始对象
- 匹配成功 → 创建代理对象(Proxy)
- 获取 bean 执行方法:
- 拿到的原始对象 → 直接调用
- 拿到的代理对象 → 运行代理逻辑,在原始方法前后插入增强
3.2 验证:容器中的对象是代理还是原始?
BookDao bookDao = ctx.getBean(BookDao.class);
System.out.println(bookDao.getClass()); // 增强后打印的是代理类,未增强是原始类
⚠️ 注意:不要直接用
System.out.println(bookDao),因为toString()被重写过,看不出区别。用getClass()才能看到真实类型。
3.3 新增核心概念
| 概念 | 解释 |
|---|---|
| 目标对象(Target) | 被代理的原始对象 |
| 代理(Proxy) | 对目标对象增强后生成的对象,包含原始方法 + 通知逻辑 |
🔄 总结:Spring AOP 的本质 = 代理模式。匹配切入点的方法会生成代理对象,不匹配的就是原始对象。
四、AOP 配置管理
4.1 切入点表达式
4.1.1 语法格式
标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
execution(public User com.itheima.service.UserService.findById(int))
// ↑访问修饰符 ↑返回值 ↑包名 ↑类名 ↑方法名 ↑参数
- 访问修饰符、异常名可以省略
- 写在接口上和写在实现类上都能匹配到(调用接口最终走的还是实现类)
4.1.2 通配符
| 符号 | 含义 | 示例 |
|---|---|---|
* |
单个独立的任意符号 | execution(* com.*.service.*.find*(*)) |
.. |
多个连续的任意符号 | execution(* com..service.*.*(..)) |
+ |
匹配子类类型 | *Service+(用得少,了解即可) |
4.1.3 书写技巧
- 描述接口,不描述实现类(避免紧耦合)
- 访问修饰符通常省略(接口方法都是
public) - 查询方法返回值用
*,增删改用精准类型 - 包名尽量不用
..,用*做单级匹配 - 接口名用
*Service表示业务层模块 - 方法名保留动词(
get、find),名词用*
🎯 实战套路:业务层全部方法 →
execution(* com.itheima.service.*Service.*(..))
4.2 AOP 通知类型
共 5 种通知类型,下面通过一张图理解它们的位置:
方法执行流程:
┌─ 后置通知(After):成功/异常都执行 ─┐
前置通知(Before)→ → 原始方法执行 → 返回后通知(AfterReturning):正常返回才执行
└─ 异常后通知(AfterThrowing):抛异常才执行 ─┘
环绕通知(Around):= 前置 + 后置,能完全控制方法执行
| 通知类型 | 注解 | 执行时机 |
|---|---|---|
| 前置通知 | @Before |
方法执行前 |
| 后置通知 | @After |
方法执行后(无论是否异常) |
| 返回后通知 | @AfterReturning |
正常返回后 |
| 异常后通知 | @AfterThrowing |
抛出异常后 |
| 环绕通知 | @Around |
前后都可以,最强大 |
4.3 环绕通知详解(重点)
环绕通知能实现其他所有通知类型的功能,是实际开发中最常用的。
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ..."); // 前置部分
Object ret = pjp.proceed(); // 调用原始方法
System.out.println("around after advice ..."); // 后置部分
return ret;
}
环绕通知注意事项(重要!):
- 必须依赖形参
ProceedingJoinPoint,否则无法调用原始方法 - 如果不调用
pjp.proceed(),原始方法根本不会执行 - 如果原始方法有返回值,通知方法最好返回
Object,并把pjp.proceed()的返回值return出去 - 原始方法无返回值时,通知方法可设为
void或Object - 必须处理
Throwable异常
⚠️ 坑点:
pjp.proceed()有两个重载版本。无参版本会自动传入原始参数;有参版本pjp.proceed(args)可以修改参数后传入。后面案例会用到这个特性。
4.4 AOP 通知获取数据
4.4.1 获取参数
-
非环绕通知:在方法签名中加
JoinPoint参数@Before("pt()") public void before(JoinPoint jp) { Object[] args = jp.getArgs(); System.out.println(Arrays.toString(args)); } -
环绕通知:用
ProceedingJoinPoint(它是JoinPoint的子类)@Around("pt()") public Object around(ProceedingJoinPoint pjp) throws Throwable { Object[] args = pjp.getArgs(); // 获取参数 args[0] = 666; // 可以修改参数 return pjp.proceed(args); // 传入修改后的参数 }
4.4.2 获取返回值
- 环绕通知:直接接收
pjp.proceed()的返回值 - 返回后通知:用
returning属性@AfterReturning(value = "pt()", returning = "ret") public void afterReturning(Object ret) { System.out.println("afterReturning advice ..." + ret); }
4.4.3 获取异常
- 环绕通知:用
try-catch捕获 - 抛出异常后通知:用
throwing属性@AfterThrowing(value = "pt()", throwing = "t") public void afterThrowing(Throwable t) { System.out.println("afterThrowing advice ..." + t); }
五、AOP 总结
| 概念 | 说明 |
|---|---|
| AOP 作用 | 不惊动原代码,为方法增强功能(无侵入式) |
| 本质 | 代理模式(生成代理对象) |
| 切入点表达式中最实用写法 | execution(* com.xxx.service.*Service.*(..)) |
| 最常用通知 | 环绕通知 @Around |
| 环绕通知核心 | ProceedingJoinPoint.proceed() 调用原始方法 |
| 可获取数据 | 参数(getArgs())、返回值(ret)、异常(try-catch) |
六、AOP 事务管理(重点)
事务是 AOP 最重要的实际应用场景。Spring 通过 AOP 实现了声明式事务管理,让我们只需要一个注解就能搞定复杂的数据库事务。
6.1 为什么需要事务?转账案例分析
场景:Tom 给 Jerry 转账 100 元。
- Tom 账户减 100
- Jerry 账户加 100
两个操作必须同时成功或同时失败。如果减钱成功、加钱失败,100 块就凭空消失了。
问题:数据层每个操作都有自己独立的事务,业务层的 transfer 方法没有事务,出现异常时无法统一回滚。
解决方案:用 Spring 事务管理,让 transfer 方法开启一个大事务,把减钱和加钱两个操作纳入同一个事务中。
6.2 Spring 事务管理三步走
步骤 1:在需要事务的方法上加 @Transactional
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Transactional
public void transfer(String out, String in, Double money) {
accountDao.outMoney(out, money);
// int i = 1 / 0; // 出现异常,事务会自动回滚
accountDao.inMoney(in, money);
}
}
步骤 2:配置事务管理器(JdbcConfig 类中)
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
DataSourceTransactionManager manager = new DataSourceTransactionManager();
manager.setDataSource(dataSource);
return manager;
}
因为 MyBatis 底层用的是 JDBC,所以这里用
DataSourceTransactionManager。
步骤 3:在配置类上开启事务注解
@Configuration
@ComponentScan("com.itheima")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class, MybatisConfig.class})
@EnableTransactionManagement // 开启注解式事务
public class SpringConfig { }
📝 关键注解速查:
@Transactional:声明方法需要事务管理@EnableTransactionManagement:开启事务注解支持
6.3 事务角色
| 角色 | 说明 | 对应 |
|---|---|---|
| 事务管理员 | 发起事务的一方 | 业务层开启事务的方法(如 transfer) |
| 事务协调员 | 加入事务的一方 | 数据层方法(如 outMoney、inMoney) |
开启 Spring 事务后,协调员的事务会加入到管理员的事务中,形成一个整体。任何一个环节出异常,整个事务都会回滚。
6.4 事务属性
@Transactional 有丰富的属性可配置:
| 属性 | 作用 | 示例 |
|---|---|---|
readOnly |
只读事务(查询用 true,增删改用 false) |
@Transactional(readOnly = true) |
timeout |
超时时间(秒),-1 为永不超时 | @Transactional(timeout = -1) |
rollbackFor |
指定哪些异常回滚(默认只回滚 RuntimeException 和 Error) | @Transactional(rollbackFor = {IOException.class}) |
noRollbackFor |
指定哪些异常不回滚 | @Transactional(noRollbackFor = {NullPointerException.class}) |
isolation |
事务隔离级别 | @Transactional(isolation = Isolation.DEFAULT) |
propagation |
事务传播行为 | @Transactional(propagation = Propagation.REQUIRES_NEW) |
⚠️ 重要:Spring 默认只对
RuntimeException和Error回滚。受检异常(如IOException)需要手动用rollbackFor指定才会回滚。
6.5 事务传播行为(Propagation)
场景:转账操作中,不管转账是否成功,都需要记录日志。但日志操作不能因为转账失败而回滚。
解决:让日志方法用 REQUIRES_NEW 传播行为,独立开启一个新事务。
@Service
public class LogServiceImpl implements LogService {
@Autowired
private LogDao logDao;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out, String in, Double money) {
logDao.log("转账操作由" + out + "到" + in + ",金额:" + money);
}
}
| 传播行为 | 管理员有事务 | 管理员无事务 |
|---|---|---|
REQUIRED(默认) |
加入管理员事务 | 新建事务 |
REQUIRES_NEW |
新建独立事务 | 新建事务 |
SUPPORTS |
加入管理员事务 | 无事务运行 |
NOT_SUPPORTED |
挂起管理员事务,无事务运行 | 无事务运行 |
MANDATORY |
加入管理员事务 | 报错 |
NEVER |
报错 | 无事务运行 |
NESTED |
设置回滚点(savePoint) | 新建事务 |
日常开发中
REQUIRED(默认)能满足大部分需求,REQUIRES_NEW用于像日志这种需要独立事务的场景。
七、总结
今天我们系统学习了 Spring 的第二个核心大模块——AOP 和声明式事务。
| 模块 | 核心要点 |
|---|---|
| AOP 核心概念 | 连接点、切入点、通知、切面、目标对象、代理 |
| 切入点表达式 | execution(* com.xxx.service.*Service.*(..)) |
| 五种通知 | 前置 @Before、后置 @After、返回后 @AfterReturning、异常后 @AfterThrowing、环绕 @Around |
| 数据获取 | 参数(getArgs())、返回值(proceed() 返回 / returning)、异常(try-catch / throwing) |
| 事务管理 | @Transactional + 事务管理器 + @EnableTransactionManagement |
| 事务传播 | REQUIRED(默认,加入现有事务)、REQUIRES_NEW(始终新建事务) |
🎤 结课小调查:今天的内容量不小,你最想在项目里试一试的是哪个?
A. 用环绕通知给所有 Service 方法加日志
B. 用 AOP 统一处理参数空格
C. 用@Transactional管理数据库事务
D. 写个独立的日志事务(REQUIRES_NEW)评论区告诉我你的选择,也欢迎分享你在事务管理上踩过的坑,我们一起避雷!
本文为 Spring Framework 第四天授课内容整理。AOP 和事务管理是 Spring 的精髓所在,掌握了它们,你的 Java 开发能力将迈上一个大台阶。如果觉得有帮助,欢迎点赞、收藏、关注,我们下节课见!
更多推荐



所有评论(0)