当前位置: 首页 > news >正文

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 核心概念

我们来通过一个场景理解几个关键术语:

假设BookDaoImplsave()update()delete()select()四个方法。我们想给updatedelete增加“计算万次执行时间”的功能,但不改原代码。

概念解释举例
连接点(JoinPoint)程序执行过程中能插入增强的任意位置BookDaoImpl中的所有方法
切入点(Pointcut)真正需要增强的方法(匹配连接点的式子)update()delete()
通知(Advice)抽取出来的共性功能(要增强的内容)“计算万次执行时间”的方法
通知类定义通知的类MyAdvice
切面(Aspect)描述通知与切入点的对应关系“在update()delete()上应用计时的通知”

🧠一句话记忆:连接点是“地点”,切入点是“我选中的地点”,通知是“要干的事”,切面是“在哪里干什么”。


二、AOP 入门案例

需求:在方法执行前打印当前系统时间,不改原代码。

2.1 环境准备

创建 Maven 项目,添加spring-context依赖,创建BookDaoBookDaoImplSpringConfig配置类和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// 标识这是一个切面类publicclassMyAdvice{@Pointcut("execution(void com.itheima.dao.BookDao.update())")privatevoidpt(){}// 切入点定义:无参数、无返回值、方法体为空@Before("pt()")// 绑定通知到切入点,在方法执行前运行publicvoidmethod(){System.out.println(System.currentTimeMillis());}}

步骤 3:在配置类上开启 AOP 注解功能

@Configuration@ComponentScan("com.itheima")@EnableAspectJAutoProxy// 开启注解格式 AOP 功能publicclassSpringConfig{}

📝关键注解速查

  • @EnableAspectJAutoProxy:开启 AOP 注解支持
  • @Aspect:声明切面类
  • @Pointcut:定义切入点
  • @Before:前置通知

三、AOP 工作流程

3.1 工作流程四步走

  1. Spring 容器启动:加载需要增强的类和通知类(此时 bean 还未创建)
  2. 读取切入点配置:只读取被实际使用的切入点(没被通知绑定的切入点忽略)
  3. 初始化 bean,匹配切入点
    • 匹配失败 → 创建原始对象
    • 匹配成功 → 创建代理对象(Proxy)
  4. 获取 bean 执行方法
    • 拿到的原始对象 → 直接调用
    • 拿到的代理对象 → 运行代理逻辑,在原始方法前后插入增强

3.2 验证:容器中的对象是代理还是原始?

BookDaobookDao=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(publicUsercom.itheima.service.UserService.findById(int))// ↑访问修饰符 ↑返回值 ↑包名 ↑类名 ↑方法名 ↑参数
  • 访问修饰符、异常名可以省略
  • 写在接口上和写在实现类上都能匹配到(调用接口最终走的还是实现类)
4.1.2 通配符
符号含义示例
*单个独立的任意符号execution(* com.*.service.*.find*(*))
..多个连续的任意符号execution(* com..service.*.*(..))
+匹配子类类型*Service+(用得少,了解即可)
4.1.3 书写技巧
  • 描述接口,不描述实现类(避免紧耦合)
  • 访问修饰符通常省略(接口方法都是public
  • 查询方法返回值用*,增删改用精准类型
  • 包名尽量不用..,用*做单级匹配
  • 接口名用*Service表示业务层模块
  • 方法名保留动词(getfind),名词用*

🎯实战套路:业务层全部方法 →execution(* com.itheima.service.*Service.*(..))

4.2 AOP 通知类型

共 5 种通知类型,下面通过一张图理解它们的位置:

方法执行流程: ┌─ 后置通知(After):成功/异常都执行 ─┐ 前置通知(Before)→ → 原始方法执行 → 返回后通知(AfterReturning):正常返回才执行 └─ 异常后通知(AfterThrowing):抛异常才执行 ─┘ 环绕通知(Around):= 前置 + 后置,能完全控制方法执行
通知类型注解执行时机
前置通知@Before方法执行前
后置通知@After方法执行后(无论是否异常)
返回后通知@AfterReturning正常返回后
异常后通知@AfterThrowing抛出异常后
环绕通知@Around前后都可以,最强大

4.3 环绕通知详解(重点)

环绕通知能实现其他所有通知类型的功能,是实际开发中最常用的。

@Around("pt()")publicObjectaround(ProceedingJoinPointpjp)throwsThrowable{System.out.println("around before advice ...");// 前置部分Objectret=pjp.proceed();// 调用原始方法System.out.println("around after advice ...");// 后置部分returnret;}

环绕通知注意事项(重要!)

  1. 必须依赖形参ProceedingJoinPoint,否则无法调用原始方法
  2. 如果不调用pjp.proceed(),原始方法根本不会执行
  3. 如果原始方法有返回值,通知方法最好返回Object,并把pjp.proceed()的返回值return出去
  4. 原始方法无返回值时,通知方法可设为voidObject
  5. 必须处理Throwable异常

⚠️坑点pjp.proceed()有两个重载版本。无参版本会自动传入原始参数;有参版本pjp.proceed(args)可以修改参数后传入。后面案例会用到这个特性。

4.4 AOP 通知获取数据

4.4.1 获取参数
  • 非环绕通知:在方法签名中加JoinPoint参数

    @Before("pt()")publicvoidbefore(JoinPointjp){Object[]args=jp.getArgs();System.out.println(Arrays.toString(args));}
  • 环绕通知:用ProceedingJoinPoint(它是JoinPoint的子类)

    @Around("pt()")publicObjectaround(ProceedingJoinPointpjp)throwsThrowable{Object[]args=pjp.getArgs();// 获取参数args[0]=666;// 可以修改参数returnpjp.proceed(args);// 传入修改后的参数}
4.4.2 获取返回值
  • 环绕通知:直接接收pjp.proceed()的返回值
  • 返回后通知:用returning属性
    @AfterReturning(value="pt()",returning="ret")publicvoidafterReturning(Objectret){System.out.println("afterReturning advice ..."+ret);}
4.4.3 获取异常
  • 环绕通知:用try-catch捕获
  • 抛出异常后通知:用throwing属性
    @AfterThrowing(value="pt()",throwing="t")publicvoidafterThrowing(Throwablet){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

@ServicepublicclassAccountServiceImplimplementsAccountService{@AutowiredprivateAccountDaoaccountDao;@Transactionalpublicvoidtransfer(Stringout,Stringin,Doublemoney){accountDao.outMoney(out,money);// int i = 1 / 0; // 出现异常,事务会自动回滚accountDao.inMoney(in,money);}}

步骤 2:配置事务管理器(JdbcConfig 类中)

@BeanpublicPlatformTransactionManagertransactionManager(DataSourcedataSource){DataSourceTransactionManagermanager=newDataSourceTransactionManager();manager.setDataSource(dataSource);returnmanager;}

因为 MyBatis 底层用的是 JDBC,所以这里用DataSourceTransactionManager

步骤 3:在配置类上开启事务注解

@Configuration@ComponentScan("com.itheima")@PropertySource("classpath:jdbc.properties")@Import({JdbcConfig.class,MybatisConfig.class})@EnableTransactionManagement// 开启注解式事务publicclassSpringConfig{}

📝关键注解速查

  • @Transactional:声明方法需要事务管理
  • @EnableTransactionManagement:开启事务注解支持

6.3 事务角色

角色说明对应
事务管理员发起事务的一方业务层开启事务的方法(如transfer
事务协调员加入事务的一方数据层方法(如outMoneyinMoney

开启 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 默认只对RuntimeExceptionError回滚。受检异常(如IOException)需要手动用rollbackFor指定才会回滚。

6.5 事务传播行为(Propagation)

场景:转账操作中,不管转账是否成功,都需要记录日志。但日志操作不能因为转账失败而回滚。

解决:让日志方法用REQUIRES_NEW传播行为,独立开启一个新事务。

@ServicepublicclassLogServiceImplimplementsLogService{@AutowiredprivateLogDaologDao;@Transactional(propagation=Propagation.REQUIRES_NEW)publicvoidlog(Stringout,Stringin,Doublemoney){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 开发能力将迈上一个大台阶。如果觉得有帮助,欢迎点赞、收藏、关注,我们下节课见!

http://www.jsqmd.com/news/793305/

相关文章:

  • AI赋能风景园林设计:技术原理、实践案例与未来挑战
  • crawdad-openclaw:开源通用爬虫框架的设计、实战与工程化部署
  • Arm GNU工具链技术解析与实战应用指南
  • 大厂IT面试通关:简历优化+高频面试题拆解(2026最新版)
  • 机器学习在非洲传染病预测与监测中的实战应用
  • 三、进程概念(操作系统与进程(1))
  • Install ncdu Disk Usage Analyzer on Linux
  • ARM710a处理器架构与性能优化实战解析
  • 【C#】 HTTP 请求通讯实现指南
  • MCP TypeScript SDK 服务说明文档
  • STM32——OLED显示字符串
  • 量子自旋冰的Dirac弦约束与蒙特卡洛模拟研究
  • 告别配置烦恼:用CMake管理你的Qt + Eigen项目(附完整CMakeLists.txt)
  • 机器学习在非洲公共卫生疾病预测中的实战应用与技术解析
  • Java+YOLO+TensorRT 8.6:GPU 加速推理实战,延迟压至 12ms 以内
  • 基于Langchain-Chatchat构建私有化RAG知识库问答系统实战指南
  • AI代码助手性能基准测试:从原理到实践的科学评估方法
  • 封装工具类,JwtUtils令牌工具类
  • 【没事学点啥】TurboBlog轻量级个人博客项目——Turbo Blog 项目学习与上线指南
  • HQChart使用教程105-K线图,分时图如何对接AI进行数据分析
  • 基于ESP32-S3与CAN总线的开源机械臂控制器设计
  • 抖音下载器终极指南:三步轻松保存无水印视频和音乐
  • 3分钟破解百度网盘限速:直链生成工具终极指南
  • 基于Kubernetes部署Dify AI开发平台:从Docker Compose到生产级K8s方案全解析
  • 开源仿生夹爪crawdad-openclaw:从3D打印到智能抓取的完整实践指南
  • 如何快速提取Unity游戏资源?AssetStudio终极使用指南
  • 物流分拣系统:C# + YOLOv12实现快递面单信息提取与包裹体积测量
  • 【VUE专题】2. 零基础-ElementUI前端组件安装使用保姆级教程
  • 微信聊天记录永久保存与深度分析:你的数字记忆守护者
  • 第五篇:Spring事务管理——@Transactional的底层实现与失效场景