国庆节10月1日
--


五种通知类型的执行时机(含本机实测的正常顺序与异常对照)、@Pointcut 抽取公共切点、多切面的通知顺序与 @Order 的两条规则、execution 与 @annotation 两种切入点表达式,以及用连接点取出目标类名、方法名和参数

85 篇已经把 AOP 跑通了:一个切面类 + 一个 @Around 就能接管一批方法的耗时统计。但它只用了一种通知、表达式也只写了一种形式。这一篇(PPT 第 13~27 页)把 AOP 的四个细节一次讲透:通知类型(一共有五种,分别什么时候执行)、通知顺序(多个切面都匹配上时谁先谁后)、切入点表达式(execution 该怎么写、以及更省事的 @annotation)、连接点(怎么在通知里拿到”我正在通知谁”)。
第 13 页先回顾这一章的三大块(AOP基础 / AOP进阶 / AOP案例),第 14 页把”02 AOP进阶”拆成四小节:
通知类型 → 通知顺序 → 切入点表达式 → 连接点
四小节的顺序就是本篇的顺序。其中”通知类型”和”通知顺序”在 PPT 里各夹了一页回顾(第 17、23、25 页是通过问答形式做的小结),笔记里也照样接在对应小节后面。
PPT 第 15 页开门见山:
根据通知方法执行时机的不同,将通知类型分为以下常见的五类:
| 通知类型 | 注解 | 执行时机(PPT 原文) |
|---|---|---|
| 环绕通知 | @Around | 此注解标注的通知方法在目标方法前、后都被执行 |
| 前置通知 | @Before | 此注解标注的通知方法在目标方法前被执行 |
| 后置通知 | @After | 此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 |
| 返回后通知 | @AfterReturning | 此注解标注的通知方法在目标方法后被执行,有异常不会执行 |
| 异常后通知 | @AfterThrowing | 此注解标注的通知方法发生异常后执行 |
注意后三个长得很像,差别全在”什么条件下才会执行”:
@After → 无论成功还是异常,目标方法执行完它都跑(相当于 finally);@AfterReturning → 只有正常返回才跑(有异常就不跑);@AfterThrowing → 只有抛了异常才跑。也就是说:一次正常调用能看到 @After 和 @AfterReturning(没有 @AfterThrowing);一次异常调用能看到 @After 和 @AfterThrowing(没有 @AfterReturning)。这一点下面有本机实测对照。
两条”注意”是考试和面试的重点,PPT 专门框了出来:
注意 1:
@Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行。注意 2:
@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。
为什么只有环绕通知要 proceed()?因为只有它”前后都执行”——它得自己决定原始方法什么时候跑(85 篇的代理伪代码就是这个形状);其它四种通知只是”插入一句话”,原始方法由框架照常执行,不用你操心。
课程代码里把这五种通知写在同一个切面类里(aop/MyAspect1.java,@Aspect 默认注释着,做实验时打开):
1@Slf4j2//@Aspect3@Component4public class MyAspect1 {5
6 // 把公共的切入点表达式抽出来,五个通知都引用它7 @Pointcut("execution(* com.itheima.service.impl.*.*(..))")8 private void pt(){}9
10 //前置通知 - 目标方法运行之前运行11 @Before("pt()")12 public void before(){13 log.info("before ....");14 }15
16 //环绕通知 - 目标方法运行之前、后运行17 @Around("pt()")18 public Object around(ProceedingJoinPoint pjp) throws Throwable {19 log.info("around ... before ....");20
21 Object result = pjp.proceed(); // 自己调用,原始方法才会执行22
23 log.info("around ... after ....");24 return result; // 返回值必须能装下原始方法的返回值25 }26
27 //后置通知 - 目标方法运行之后运行, 无论是否出现异常都会执行28 @After("pt()")29 public void after(){30 log.info("after ....");31 }32
33 //返回后通知 - 目标方法运行之后运行, 如果出现异常不会运行34 @AfterReturning("pt()")35 public void afterReturning(){36 log.info("afterReturning ....");37 }38
39 //异常后通知 - 目标方法运行之后运行, 只有出现异常才会运行40 @AfterThrowing("pt()")41 public void afterThrowing(){42 log.info("afterThrowing ....");43 }44}
@Around 方法里 Object result = pjp.proceed(); 这一行就是”注意 1”,最后的 return result; 就是”注意 2”一个方法上同时有多个通知时,它们是按通知类型的顺序”包”起来的——最外层是 @Around,往里依次是 @Before → 目标方法 → @AfterReturning / @After / @AfterThrowing,最后再回到 @Around 的后半段。把这句结构记住,下面两组实测日志就不用死记了。
| PPT 的问题 | 答案 |
|---|---|
| 常见的通知类型有哪些?分别在什么时候执行 | @Before 前置通知(目标方法前)、@After 后置通知(目标方法后,无论是否有异常都会执行)、@Around 环绕通知(目标方法前后都执行,重点)、@AfterReturning 返回后通知(目标方法后执行,有异常不会执行)、@AfterThrowing 异常后通知(目标方法发生异常后执行) |
把 MyAspect1 的 @Aspect 打开,调用 GET /depts(正常返回 200),控制台里按时间顺序打出来的是(本机实测):
1around ... before .... ← @Around 在 pjp.proceed() 之前的代码2before .... ← @Before3[目标方法真正执行]4afterReturning .... ← @AfterReturning(正常返回才执行)5after .... ← @After(无论是否异常都执行)6around ... after .... ← @Around 在 pjp.proceed() 之后的代码按上面那条”结构”逐层对一遍:
@Around 的”前一半”最先——它是最外层,进来先执行;@Before 第二——它在环绕通知的内部执行,但仍在目标方法之前;DeptServiceImpl.list(),日志里看不到它自己打的日志,只是”中间那一刻”);@AfterReturning 先(“正常返回”这个条件是刚满足的);@After 后(它是”无论如何”的那一个,排在返回后通知之后);@Around 的”后一半”——最外层最后收尾;afterThrowing(没有异常,它不会执行)。在 DeptServiceImpl.list() 里把那行注释掉的 int i = 1/0; 打开(人为制造算数异常),再调一次 GET /depts——接口返回 HTTP 500,异常是 java.lang.ArithmeticException: / by zero,日志顺序变成(本机实测):
1around ... before ....2before ....3afterThrowing .... ← 出现!@AfterThrowing 只在异常时执行4after .... ← 仍然执行(无论是否异常)这组对照把五种通知”什么时候执行”钉死了:
| 通知 | 正常调用 | 抛异常 | 说明 |
|---|---|---|---|
@Around 前一半 | 执行 | 执行 | 只要进到通知就开始跑 |
@Before | 执行 | 执行 | 目标方法之前的都照跑 |
@AfterReturning | 执行 | 不执行 | 有异常就没有”正常返回” |
@AfterThrowing | 不执行 | 执行 | 只有异常才触发 |
@After | 执行 | 执行 | 无论成败都执行 |
@Around 后一半 | 执行 | 不执行 | 见下 |
三个值得单独记住的现象:
afterReturning 消失了——PPT 那句”有异常不会执行”就是这么来的;after 还在——“无论是否有异常都会执行”;@Around 的”后一半”没执行——异常是在 pjp.proceed() 那一刻从目标方法里抛出来的,它一路向外抛,around 方法里 proceed() 之后的代码(log.info("around ... after ...."))被跳过;异常继续往上抛,最终变成了 HTTP 500。第 3 条很容易踩坑:如果你在环绕通知里写了”收尾代码”(比如关资源、清理数据),它不一定能被执行到。真要保证”无论如何都执行”,得用 try-finally 包住 pjp.proceed()。
@Pointcut:把公共切入点抽出来(PPT 第 16 页)#PPT 第 16 页只有一句话加一张图:
@Pointcut该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。
(PPT 的标题把这个注解写成 @PointCut,代码里真正的写法是 @Pointcut——大小写别敲错。)
在 MyAspect1 里就是最上面那两行——先定义一个”空方法”挂上 @Pointcut,五个通知都改写成 "pt()":
1// 抽取公共的切入点表达式:方法体是空的,方法名就是"引用名"2@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")3public void pt(){}1@Around("pt()") // 引用上面那个切入点2public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
@Pointcut 定义 pt(),下面的环绕通知直接写 @Around("pt()") 引用它;以后表达式要改,只改一处可见性(方法上的修饰符)决定了它能被谁引用,PPT 列了两条:
| 修饰符 | 能被谁引用 |
|---|---|
private | 仅能在当前切面类中引用该表达式 |
public | 在其他外部的切面类中也可以引用该表达式(引用时要写全名,如 类名.方法名()) |
为什么抽出来值得?上一节的 MyAspect1 里五个通知本来要写五遍同一个表达式——一旦要改(比如只统计 list),五处都得改;抽成 pt() 以后只改一处。这就是 PPT 说的”提高复用性”。
| PPT 的问题 | 答案 |
|---|---|
@Pointcut 注解的作用是什么? | 抽取公共的切点表达式,提高复用性 |
第 18 页又是一个分隔页(通知类型 / 通知顺序 / 切入点表达式 / 连接点),第 19 页给出问题与规则:
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。
那谁先谁后?PPT 给了两套规则。
不同切面类中,默认按照切面类的类名字母排序:
- 目标方法前的通知方法:字母排名靠前的先执行
- 目标方法后的通知方法:字母排名靠前的后执行
(“后执行”那句读起来别扭,但它和下面 @Order 的规则是同一个道理:进的时候正着走、出的时候倒着走——像穿过一个洋葱,先进去的后出来。)
@Order(数字) 明确指定#用
@Order(数字)加在切面类上来控制顺序:
- 目标方法前的通知方法:数字小的先执行
- 目标方法后的通知方法:数字小的后执行
课程代码里准备了三个切面(MyAspect2 / MyAspect3 / MyAspect4),除了 @Order 的数字不同,内容几乎一样——都是 @Before + @After 各打一行日志:
1@Slf4j2@Order(8) // 切面类上加 @Order 控制顺序3@Component4//@Aspect5public class MyAspect2 {6 //前置通知7 @Before("execution(* com.itheima.service.impl.*.*(..))")8 public void before(){9 log.info("MyAspect2 -> before ...");10 }11
12 //后置通知13 @After("execution(* com.itheima.service.impl.*.*(..))")14 public void after(){15 log.info("MyAspect2 -> after ...");16 }17}三个类分别标的是 @Order(8)(MyAspect2)、@Order(5)(MyAspect3)、@Order(3)(MyAspect4)。

@Before 打一行、一个 @After 打一行;三个切面类只差类名和日志里的编号
@Order(5) 写在切面类上(和 @Aspect、@Component 并列)——数字越小,目标方法前越早执行@Order(3) / (5) / (8) 的真实顺序#把 MyAspect2(@Order(8))、MyAspect3(@Order(5))、MyAspect4(@Order(3)) 三个切面同时打开,调用 GET /depts,控制台顺序是(本机实测):
1MyAspect4 -> before ... ← @Order(3)2MyAspect3 -> before ... ← @Order(5)3MyAspect2 -> before ... ← @Order(8)4[目标方法]5MyAspect2 -> after ... ← @Order(8)6MyAspect3 -> after ... ← @Order(5)7MyAspect4 -> after ... ← @Order(3)结论和 PPT 的规则一字不差:
3 → 5 → 8(数字小的先执行);8 → 5 → 3(数字小的后执行,也就是”倒着回来”)。不写 @Order 时则按切面类名的字母顺序排(MyAspect2 < MyAspect3 < MyAspect4,正好也是 2 → 3 → 4 的顺序,所以想让实验”看出差别”就得给它们标上不相邻的数字),前同样正着走、后同样倒着走。
第 20 页又是分隔页(通知类型 / 通知顺序 / 切入点表达式 / 连接点),第 21 页给出定义:
介绍:描述切入点方法的一种表达式。 作用:用来决定项目中的哪些方法需要加入通知。 常见形式:
execution(……):根据方法的签名来匹配@annotation(……):根据注解匹配
一句话概括两者的分工:execution 靠”方法长什么样”(返回值、包、类、方法名、参数)来挑;@annotation 靠”方法上贴了什么注解”来挑。

@annotation 的样子——@Before("@annotation(com.itheima.anno.Log)"),括号里写的是注解的全类名(图里用的是案例工程 tlias 的 Log 注解;实验工程 springboot-aop-quickstart 里叫 LogOperation)execution 的完整语法(PPT 第 22 页)#1execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)其中带 ? 的表示可以省略的部分,PPT 逐条说明了怎么省:
| 可省略项 | 说明 |
|---|---|
| 访问修饰符 | 可省略(比如 public、protected) |
| 包名.类名 | 可省略 |
throws 异常 | 可省略(注意是方法上声明抛出的异常,不是实际抛出的异常) |
“包名.类名可省略”虽然语法允许,但课程代码的注释里写得很直接://@Before("execution(void delete(java.lang.Integer))") //包名.类名 强烈不建议省略——省掉之后,全工程所有叫 delete 的方法(不分包、不分类)都会被你切中,属于自找麻烦。
| 通配符 | 含义(PPT 原文) |
|---|---|
* | 单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分 |
.. | 多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数 |
PPT 给的两个例子正好各用一个:
1execution(* com.*.service.*.update*(*))2execution(* com.itheima..DeptService.*(..))* 依次通配了”返回值""com 下的一段包""service 下的类名”,方法名写成 update* 表示”update 开头的方法”,参数 (*) 表示”任意类型的一个参数”——最终匹配”某层包里的 service 下某个类的 updateXxx(一个参数) 方法”;.. 通配了”com.itheima 下面任意层级的包”,DeptService 这个类名写死(注意它是接口名),.*(..) 表示”这个接口里的任意方法、任意参数”——DeptService 的实现类在 service.impl 包下,也能被这条表达式切中。课程代码 MyAspect5 把同一批 delete 方法用各种写法逐个演示了一遍(全是注释状态,打开注释就能试),可以当成一份”表达式写法对照表”:
写法(@Before(...) 括号里的内容) | 匹配范围 / 考点 |
|---|---|
execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer)) | 全都写:访问修饰符 + 返回值 + 全类名 + 方法名 + 参数类型(参数类型要写全限定名) |
execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer)) | 省略访问修饰符 |
execution(void delete(java.lang.Integer)) | 省略包名.类名——代码注释里明确写着”强烈不建议” |
execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer)) | 返回值用 *(返回 void 也能匹配) |
execution(* com.*.service.impl.DeptServiceImpl.delete(java.lang.Integer)) | 包名的一部分用 *(这里是 com.itheima 的 itheima) |
execution(* com.itheima.service.impl.*.delete(java.lang.Integer)) | 类名用 *(impl 包下任意类的 delete) |
execution(* com.itheima.service.impl.*.*(java.lang.Integer)) | 类名、方法名都用 * |
execution(* com.itheima.service.impl.*.*(*)) | 参数用 *——表示一个任意类型的参数 |
execution(* com.itheima.service.impl.*.del*(*)) | 方法名的一部分用 *(del 开头) |
execution(* com.itheima.service.impl.*.*e(*)) | 方法名的一部分用 *(以 e 结尾) |
execution(* com..service.impl.DeptServiceImpl.*(..)) | 包用 ..——任意层级的包 |
execution(* com.itheima.service.*.*(..)) | 这里的 * 通配 service 下一层子包(也就是 impl) |
execution(* com.itheima.service.impl.DeptServiceImpl.list(..)) || execution(* com.itheima.service.impl.DeptServiceImpl.delete(..)) | 用 || 组合两个表达式:list 与 delete 都匹配 |
&&、||、!(PPT 第 22 页”注意 1”)#根据业务需要,可以使用 且(
&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
三种组合的含义直白:&& 两边都满足才匹配(范围变小)、|| 满足一个即可(范围变大)、! 取反(排除)。
| PPT 的问题 | 答案 |
|---|---|
execution 切入点表达式的完整语法? | execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?),其中访问修饰符、包名.类名、throws 异常可以省略 |
| 通配符有哪些? | *:单个独立的任意符号(通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分);..:多个连续的任意符号(通配任意层级的包,或任意类型、任意个数的参数) |
同页还有三条书写建议(PPT 第 23 页):
findXxx、updateXxx;DeptService 而不是 DeptServiceImpl),增强拓展性;..,使用 * 匹配单个包。这三条其实是一套配套思路:名字规范 → 表达式能写得又短又准;面向接口 → 换实现类(比如以后 DeptServiceImpl 拆成两个类)表达式不用改;缩小范围 → 少误伤无关方法,性能也更好。
@annotation(PPT 第 24~25 页)#第 24 页给出定义:
@annotation切入点表达式,用于匹配标识有特定注解的方法。
用法是”两步走”:先有自定义注解,再在表达式里写上注解的全类名。
1package com.itheima.anno;2
3import java.lang.annotation.ElementType;4import java.lang.annotation.Retention;5import java.lang.annotation.RetentionPolicy;6import java.lang.annotation.Target;7
8@Target(ElementType.METHOD) // 只能加在方法上9@Retention(RetentionPolicy.RUNTIME) // 运行时仍保留(AOP 运行时才读得到)10public @interface LogOperation {11}(@Target 和 @Retention 都是”元注解”:前者限定这个注解能贴在哪,后者决定它保留到什么时候。RUNTIME 是必须的——AOP 是在运行时读注解来决定切谁。)
1@Around("@annotation(com.itheima.anno.LogOperation)") // 括号里是注解的全类名2public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {3 long startTime = System.currentTimeMillis();4 Object result = joinPoint.proceed();5 // ... 省略6}
@Around("@annotation(com.itheima.anno.LogOperation)"),注解的全类名写在括号里;这样切面就只认”贴了这个注解的方法”1@LogOperation // 贴了注解的方法才会被切中2@DeleteMapping3public Result delete(Integer id){4 System.out.println("根据ID删除部门数据: " + id);5 deptService.delete(id);6 return Result.success();7}8
9@LogOperation10@PostMapping11public Result save(@RequestBody Dept dept){12 System.out.println("新增部门数据: " + dept);13 deptService.save(dept);14 return Result.success();15}
@LogOperation 贴到了控制器方法上(delete、save)——从此这些方法会被那个 @annotation 切面切中,而没贴注解的方法不受影响这种写法最大的好处是指哪打哪:不用去数”哪些方法名符合规律”,而是在方法上贴一个注解,语义一目了然;以后新增接口想记录日志,贴一个注解就行——87 篇的案例选的就是这条路。
| PPT 的问题 | 答案 |
|---|---|
@annotation 切入点表达式的写法? | @annotation(注解全类名) |
execution 切入点表达式与 @annotation 切入点表达式的应用场景? | 如果 execution 表达式方便描述指定的方法,就使用 execution 表达式;否则,就使用 @annotation 表达式 |
怎么判断”方不方便”?看方法名有没有规律:像85 篇那种”整个 service 包都要统计耗时”,execution(* com.itheima.service.impl.*.*(..)) 一行就够;反过来,要切的方法零散分布在不同包的控制器里、名字还没规律,execution 就得靠 || 拼一长串——这时 @annotation 更合适。
第 26 页是分隔页(通知类型 / 通知顺序 / 切入点表达式 / 连接点),第 27 页给出定义:
在 Spring 中用
JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
两条使用规则:
对于
@Around通知,获取连接点信息只能使用ProceedingJoinPoint; 对于其它四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型。
(父类型的含义:ProceedingJoinPoint 能当 JoinPoint 用,反过来不行——因为只有子类型多了 proceed() 这个方法。所以 @Around 里能拿信息、也能执行原始方法;其它通知里只能拿信息。)
PPT 给了两份代码对照:
1// @Around 环绕通知:用 ProceedingJoinPoint2@Around("execution(* com.itheima.service.DeptService.*(..))")3public Object around(ProceedingJoinPoint joinPoint) throws Throwable {4 String className = joinPoint.getTarget().getClass().getName(); // 获取目标类名5 Signature signature = joinPoint.getSignature(); // 获取目标方法签名6 String methodName = joinPoint.getSignature().getName(); // 获取目标方法名7 Object[] args = joinPoint.getArgs(); // 获取目标方法运行参数8
9 Object res = joinPoint.proceed(); // 执行原始方法,获取返回值(环绕通知)10 return res;11}12
13// 其它通知:用 JoinPoint14@Before("execution(* com.itheima.service.DeptService.*(..))")15public void before(JoinPoint joinPoint) {16 String className = joinPoint.getTarget().getClass().getName(); // 获取目标类名17 Signature signature = joinPoint.getSignature(); // 获取目标方法签名18 String methodName = joinPoint.getSignature().getName(); // 获取目标方法名19 Object[] args = joinPoint.getArgs(); // 获取目标方法运行参数20}四个常用方法一起记:
| 方法 | 拿到什么 |
|---|---|
joinPoint.getTarget() | 目标对象本身(打印出来是 类名@哈希值) |
joinPoint.getTarget().getClass().getName() | 目标类全名(形如 com.itheima.service.impl.DeptServiceImpl) |
joinPoint.getSignature() | 方法签名对象(getSignature().getName() 才是方法名) |
joinPoint.getArgs() | 方法运行参数(一个 Object[] 数组) |
joinPoint.proceed() | 仅 ProceedingJoinPoint 有——执行原始方法并拿到返回值 |
课程代码里的 MyAspect6 默认就是打开的(@Aspect 没被注释),调用 GET /depts 后控制台输出(本机实测):
1获取目标对象: com.itheima.service.impl.DeptServiceImpl@34a8c1a42获取目标类: com.itheima.service.impl.DeptServiceImpl3获取目标方法: list4获取目标方法参数: []逐行对上:joinPoint.getTarget() → 目标对象(类名@哈希值);joinPoint.getTarget().getClass().getName() → 目标类全名;joinPoint.getSignature().getName() → 方法名 list;joinPoint.getArgs() → 参数数组(list() 没有参数,打印出来是 [])。如果换成 delete(Integer id),最后一行就会打印出参数值——PPT 第 27 页列的四个方法在这里全部验证过。
连接点信息是87 篇的”地基”:操作日志要记的”执行方法的全类名、方法名、运行参数、返回值”,前三个从连接点里取,返回值则是 proceed() 的返回值。
| 问题 | 答案 |
|---|---|
| 常见通知类型有哪五种? | @Around 环绕(前后都执行)、@Before 前置(前)、@After 后置(后,无论是否有异常都执行)、@AfterReturning 返回后(后,有异常不执行)、@AfterThrowing 异常后(发生异常后执行) |
@Around 有哪两条注意? | ① 必须自己调 ProceedingJoinPoint.proceed() 让原始方法执行;② 返回值必须指定为 Object,用来接收原始方法的返回值 |
| 本机实测的正常顺序? | around ... before → before → 目标方法 → afterReturning → after → around ... after |
| 本机实测的异常顺序? | 打开 int i = 1/0; 后:around ... before → before → afterThrowing → after;afterReturning 和 @Around 的后半段都不执行(接口 500) |
@Pointcut 干什么用? | 把公共的切点表达式抽出来复用;private 只能本切面类引用,public 外部切面类也能引用 |
| 多个切面的通知顺序? | 默认按切面类名字母排序(前:字母靠前的先执行;后:字母靠前的后执行);用 @Order(数字) 指定(前:数字小的先执行;后:数字小的后执行) |
本机实测的 @Order 结果? | 前 MyAspect4(3) → MyAspect3(5) → MyAspect2(8);后 8 → 5 → 3 |
| 切入点表达式两种形式? | execution(……) 按方法签名匹配;@annotation(……) 按注解匹配 |
execution 的完整语法? | execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)——访问修饰符、包名.类名、throws 异常可省略;通配符 *(单个任意)与 ..(连续任意);可用 &&、||、! 组合 |
| 切入点表达式的三条书写建议? | 方法命名规范(findXxx/updateXxx)、基于接口描述(不用实现类)、尽量缩小匹配范围(少用 ..,用 * 匹配单个包) |
@annotation 的写法与应用场景? | 写法 @annotation(注解全类名);execution 方便描述就用 execution,否则用 @annotation |
| 连接点能取到什么? | 目标对象 getTarget()、目标类全名 getTarget().getClass().getName()、方法名 getSignature().getName()、参数 getArgs();@Around 用 ProceedingJoinPoint(多一个 proceed()),其它通知用 JoinPoint(前者的父类型) |
@Around 环绕(目标方法前后都执行)、@Before 前置(目标方法前)、@After 后置(目标方法后,无论是否异常都会执行)、@AfterReturning 返回后(目标方法后,有异常不会执行)、@AfterThrowing 异常后(发生异常后执行)@Around 的两条注意:① 环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行;② 环绕通知方法的返回值必须指定为 Object,用来接收原始方法的返回值@Pointcut 的作用:把公共的切点表达式抽取出来,用时引用即可(提高复用性);private 仅能在当前切面类中引用,public 在其他外部切面类中也能引用@Around 前一半 → @Before → 目标方法 → @AfterReturning → @After → @Around 后一半;实测日志为 around ... before .... / before .... /(目标方法)/ afterReturning .... / after .... / around ... after ....DeptServiceImpl.list() 打开 int i = 1/0; 后接口返回 HTTP 500(java.lang.ArithmeticException: / by zero),日志变成 around ... before → before → afterThrowing → after;afterReturning 不执行,@Around 的后半段也不执行@Order(数字) 时(前:数字小的先执行;后:数字小的后执行)。实测 MyAspect4(@Order(3)) → MyAspect3(@Order(5)) → MyAspect2(@Order(8)) 是目标方法前的顺序,目标方法后倒过来 8 → 5 → 3execution 语法与通配符:execution(……) 根据方法的签名匹配、@annotation(……) 根据注解匹配,组合可用 &&(且)/ ||(或)/ !(非);execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?)——访问修饰符、包名.类名、throws 异常可省略;* = 单个独立的任意符号(任意返回值/包名/类名/方法名、任意类型的一个参数,也可通配名字的一部分),.. = 多个连续的任意符号(任意层级的包、任意类型任意个数的参数)findXxx、updateXxx);通常基于接口描述(不用实现类,增强拓展性);尽量缩小匹配范围(包名尽量不用 ..,用 * 匹配单个包)@annotation 的写法与应用场景:写法是 @annotation(注解全类名);execution 表达式方便描述指定方法就用 execution,否则用 @annotationgetTarget()(目标对象)、getTarget().getClass().getName()(目标类全名)、getSignature().getName()(目标方法名)、getArgs()(运行参数);@Around 只能用 ProceedingJoinPoint(多一个 proceed()),其它四种通知只能用 JoinPoint(ProceedingJoinPoint 的父类型)。本机实测输出:获取目标对象: com.itheima.service.impl.DeptServiceImpl@34a8c1a4、获取目标类: com.itheima.service.impl.DeptServiceImpl、获取目标方法: list、获取目标方法参数: [] 2-1 一个切面里把五种通知都写出来
需求:有一个切面类,要切住 com.itheima.service.impl 包下的所有方法,并在五个时机各打印一行日志:① 目标方法执行前;② 目标方法执行前后都打印(前后两行);③ 目标方法执行后,不管成功失败都打印;④ 目标方法正常返回后打印;⑤ 目标方法抛异常后打印。要求:这五段逻辑写在同一个切面类里,切入点表达式只写一处、五个通知都引用它(别把表达式复制五遍)。写完再回答:五种通知里哪一种必须自己”让原始方法执行”?它的返回值有什么硬性要求?
(练习文件 test_86_通知类型与顺序.java 的题目 2-1 里给了写作区。)
一级 · 思路:先把切入点用”抽出一个空方法 + 一个注解”的方式定义成公用的一份,再让五个通知方法各自引用它;注意”前后都打印”的那个要自己调原始方法、还要把结果交回去
二级 · 方法:@Pointcut("execution(* com.itheima.service.impl.*.*(..))") 定义 pt();五个注解 @Before("pt()") / @Around("pt()") / @After("pt()") / @AfterReturning("pt()") / @AfterThrowing("pt()");环绕通知的参数是 ProceedingJoinPoint、方法里 pjp.proceed()、返回 Object
三级 · 骨架:
1@Slf4j2@____3@____4public class MyAspect1 {5
6 @____("execution(* com.itheima.service.impl.*.*(..))")7 private void pt(){}8
9 @____("pt()")10 public void before(){ log.info("before ...."); }11
12 @____("pt()")13 public Object around(____ pjp) throws Throwable {14 log.info("around ... before ....");15 Object result = ____;16 log.info("around ... after ....");17 return ____;18 }19
20 @____("pt()")21 public void after(){ log.info("after ...."); }22
23 @____("pt()")24 public void afterReturning(){ log.info("afterReturning ...."); }25
26 @____("pt()")27 public void afterThrowing(){ log.info("afterThrowing ...."); }28}1@Slf4j2@Aspect3@Component4public class MyAspect1 {5
6 @Pointcut("execution(* com.itheima.service.impl.*.*(..))")7 private void pt(){}8
9 //前置通知 - 目标方法运行之前运行10 @Before("pt()")11 public void before(){12 log.info("before ....");13 }14
15 //环绕通知 - 目标方法运行之前、后运行16 @Around("pt()")17 public Object around(ProceedingJoinPoint pjp) throws Throwable {18 log.info("around ... before ....");19 Object result = pjp.proceed();20 log.info("around ... after ....");21 return result;22 }23
24 //后置通知 - 目标方法运行之后运行, 无论是否出现异常都会执行25 @After("pt()")26 public void after(){27 log.info("after ....");28 }29
30 //返回后通知 - 目标方法运行之后运行, 如果出现异常不会运行31 @AfterReturning("pt()")32 public void afterReturning(){33 log.info("afterReturning ....");34 }35
36 //异常后通知 - 目标方法运行之后运行, 只有出现异常才会运行37 @AfterThrowing("pt()")38 public void afterThrowing(){39 log.info("afterThrowing ....");40 }41}回答:必须自己”让原始方法执行”的是 @Around 环绕通知——它要在 pjp.proceed() 那一行手动触发目标方法;返回值必须声明为 Object(用来接收原始方法的返回值,再由它交回给调用方)。其它四种通知只管”插一句话”,目标方法由框架照常执行。
2-2 用注解明确两个切面的先后顺序
需求:工程里有两个切面类 AspectA 和 AspectB,切入点都把同一批业务方法切中了,两个类里各有一个前置通知和一个后置通知。现在的要求是:目标方法执行之前,AspectA 的通知先执行、AspectB 的后执行;目标方法执行之后,AspectB 的通知先执行、AspectA 的后执行——而且这个顺序不能靠类名字母”碰运气”,必须显式指定。写出这两个切面类(关键注解 + 方法),并说明如果不显式指定,框架会按什么规则排。
(练习文件 test_86_通知类型与顺序.java 的题目 2-2 里给了写作区。)
一级 · 思路:给”谁先谁后”这件事加一个带数字的注解,数字小的一方先冲进目标方法、也最后离开;两个类都要加
二级 · 方法:切面类上加 @Order(数字)(和 @Aspect、@Component 并列);前:数字小的先执行;后:数字小的后执行——所以让 AspectA 的数字比 AspectB 小
三级 · 骨架:
1@Slf4j2@____(____) // AspectA:数字更小3@Aspect4@Component5public class AspectA {6 @Before("execution(* com.itheima.service.impl.*.*(..))")7 public void before(){ log.info("A -> before ..."); }8
9 @After("execution(* com.itheima.service.impl.*.*(..))")10 public void after(){ log.info("A -> after ..."); }11}(AspectB 照抄,注解里的数字改成更大的那个)
1@Slf4j2@Order(1) // 数字更小 → 目标方法前先执行、目标方法后后执行3@Aspect4@Component5public class AspectA {6 @Before("execution(* com.itheima.service.impl.*.*(..))")7 public void before(){ log.info("AspectA -> before ..."); }8
9 @After("execution(* com.itheima.service.impl.*.*(..))")10 public void after(){ log.info("AspectA -> after ..."); }11}1@Slf4j2@Order(2) // 数字更大 → 前:晚于 AspectA;后:早于 AspectA3@Aspect4@Component5public class AspectB {6 @Before("execution(* com.itheima.service.impl.*.*(..))")7 public void before(){ log.info("AspectB -> before ..."); }8
9 @After("execution(* com.itheima.service.impl.*.*(..))")10 public void after(){ log.info("AspectB -> after ..."); }11}实际打印顺序(对应本机实测那组 @Order 日志的形状):
1AspectA -> before ...2AspectB -> before ...3[目标方法]4AspectB -> after ...5AspectA -> after ...不显式指定 @Order 时,框架按切面类的类名字母排序:目标方法前字母排名靠前的先执行,目标方法后字母排名靠前的后执行。(所以如果类名恰好是 AspectA、AspectB,字母序和 @Order(1)/@Order(2) 的效果一样——想把实验做出区别,得给数字换个不相邻的排法。)
2-3 用注解”点单”,而不是靠方法名去猜
需求:现在的 execution 表达式要靠”方法名有规律”才能写出来。要求换一套思路:自定义一个只能加在方法上的注解(运行时保留),然后在切面里按”方法上有没有贴这个注解”来匹配,并把注解贴到两个控制器方法上(新增部门、删除部门)。写出三部分:自定义注解、切面里的表达式、控制器方法上的用法。
(练习文件 test_86_切入点表达式.txt 的题目 2-3 里给了写作区。)
一级 · 思路:思路是”注解当标记、表达式认标记”。注解本身要能活到运行时;表达式括号里写的是注解的全类名(不是简单名字)
二级 · 方法:自定义注解用 @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME);表达式写 @annotation(com.itheima.anno.LogOperation)(放进 @Before / @Around 的括号里);用的时候直接在控制器方法上加 @LogOperation
三级 · 骨架:
1@____(ElementType.METHOD)2@____(RetentionPolicy.RUNTIME)3public @interface LogOperation {4}1@____("@____(com.itheima.anno.LogOperation)")2public void before(){ log.info("before ..."); }1@____2@PostMapping3public Result save(@RequestBody Dept dept){ ... }1// 1. 自定义注解(课程代码 anno/LogOperation.java)2package com.itheima.anno;3
4import java.lang.annotation.ElementType;5import java.lang.annotation.Retention;6import java.lang.annotation.RetentionPolicy;7import java.lang.annotation.Target;8
9@Target(ElementType.METHOD) // 只能加在方法上10@Retention(RetentionPolicy.RUNTIME) // 运行时保留,AOP 才读得到11public @interface LogOperation {12}1// 2. 切面里按注解匹配2@Before("@annotation(com.itheima.anno.LogOperation)")3public void before(){4 log.info("MyAspect5 -> before ...");5}1// 3. 控制器方法上贴注解2@LogOperation3@PostMapping4public Result save(@RequestBody Dept dept){5 deptService.save(dept);6 return Result.success();7}8
9@LogOperation10@DeleteMapping11public Result delete(Integer id){12 deptService.delete(id);13 return Result.success();14}自查:① 表达式括号里必须是全类名(com.itheima.anno.LogOperation)——只写 LogOperation 找不到;② 注解的 @Retention 必须是 RUNTIME,否则运行时读不到、切面永远不生效;③ 贴上注解的 save/delete 被切中,而没贴注解的 list 不受影响(这就是 @annotation 比 execution “指哪打哪”的地方)。
2-4 在通知里拿到”我正在通知谁”
需求:写一个前置通知,切住 com.itheima.service 包下所有方法,打印出四样东西:目标对象、目标类的全名、目标方法名、方法的参数。再写一个环绕通知,把原始方法的返回值也拿到手里(打印一行或直接返回)。
写完回答:这两个通知的参数类型能不能互换?为什么?
(练习文件 test_86_通知类型与顺序.java 的题目 2-4 里给了写作区。)
一级 · 思路:连接点对象就是”当前被通知的那个方法”的载体,四种”拿信息”的方法它都有;只有环绕通知多一个”执行原始方法”的能力
二级 · 方法:前置通知参数用 JoinPoint,取 getTarget()、getTarget().getClass().getName()、getSignature().getName()、getArgs()(参数是数组,可 Arrays.toString);环绕通知参数用 ProceedingJoinPoint,用 proceed() 拿返回值
三级 · 骨架:
1@Before("execution(* com.itheima.service.*.*(..))")2public void before(____ joinPoint){3 log.info("获取目标对象: {}", ____);4 log.info("获取目标类: {}", joinPoint.getTarget().getClass().____());5 log.info("获取目标方法: {}", joinPoint.____.getName());6 log.info("获取目标方法参数: {}", Arrays.toString(____.getArgs()));7}8
9@Around("execution(* com.itheima.service.*.*(..))")10public Object around(____ pjp) throws Throwable {11 Object result = ____;12 log.info("返回值: {}", result);13 return ____;14}1@Before("execution(* com.itheima.service.*.*(..))")2public void before(JoinPoint joinPoint){3 log.info("before ....");4 //1. 获取目标对象5 Object target = joinPoint.getTarget();6 log.info("获取目标对象: {}", target);7 //2. 获取目标类8 String className = joinPoint.getTarget().getClass().getName();9 log.info("获取目标类: {}", className);10 //3. 获取目标方法11 String methodName = joinPoint.getSignature().getName();12 log.info("获取目标方法: {}", methodName);13 //4. 获取目标方法参数14 Object[] args = joinPoint.getArgs();15 log.info("获取目标方法参数: {}", Arrays.toString(args));16}17
18@Around("execution(* com.itheima.service.*.*(..))")19public Object around(ProceedingJoinPoint pjp) throws Throwable {20 Object result = pjp.proceed(); // 原始方法的返回值就在手里21 log.info("原始方法的返回值: {}", result);22 return result;23}本机实测(MyAspect6 对 GET /depts 的输出):
1获取目标对象: com.itheima.service.impl.DeptServiceImpl@34a8c1a42获取目标类: com.itheima.service.impl.DeptServiceImpl3获取目标方法: list4获取目标方法参数: []回答:不能互换。JoinPoint 是父类型,只负责”拿信息”;ProceedingJoinPoint 是它的子类型,多了一个 proceed()。所以环绕通知必须用 ProceedingJoinPoint(要执行原始方法),其它通知用 JoinPoint 就够(用 ProceedingJoinPoint 也能通过编译,但没必要;反过来在环绕通知里写 JoinPoint 就调不到 proceed()、原始方法不会执行)。
3-1 照着课程把 AOP 进阶的四块内容各跑一遍
这一题用实验工程 springboot-aop-quickstart,把”打开一个切面类 → 调 GET /depts → 看控制台”这个循环重复几遍,每遍只验证一件事。
MyAspect1 的 @Aspect,调一次 GET /depts,把六行日志按顺序抄下来(含”目标方法执行”那一句的位置说明);DeptServiceImpl.list() 里那行 int i = 1/0; 的注释去掉,重新构建启动,再调一次 GET /depts,记录 HTTP 状态码、异常类型和这次的日志顺序——指出哪一行消失了、哪一行是新出现的,@Around 的后半段有没有执行;int i = 1/0; 注释回去,关掉 MyAspect1,同时打开 MyAspect2(@Order(8))、MyAspect3(@Order(5))、MyAspect4(@Order(3)),调一次 GET /depts,把六行日志抄下来,并指出”目标方法之前”和”目标方法之后”各自的执行顺序;MyAspect5(它用的是”按注解匹配”的写法),给控制器方法贴上/去掉注解各调一次,确认”只有贴了注解的方法”才打印;MyAspect6(默认就开着),调一次 GET /depts,抄下四行连接点信息;(练习文件 test_86_通知类型与顺序.java 的”综合题”一段里按这 6 步给了写作区。)
回答:① 第 2 步里为什么 @After 还能执行、而 @AfterReturning 不能?② 第 4 步的注解匹配和 execution 相比,各自的”好用场景”是什么?
涉及知识点
| 知识点 | 在这里的应用 |
|---|---|
| 五种通知与执行时机(PPT 15) | 第 1、2 步——同一批通知在正常/异常两种情况下谁跑谁不跑 |
@Around 的两条注意(PPT 15) | 第 1、2 步——proceed() 与返回值;异常时后半段被跳过 |
| 通知顺序(PPT 19) | 第 3 步——@Order(3/5/8) 的实测顺序 |
切入点表达式与 @annotation(PPT 21~25) | 第 4 步——按注解匹配的写法 |
| 连接点(PPT 27) | 第 5 步——目标对象/目标类/方法名/参数 |
| 实验工程的切面开关(本机实测) | 全部步骤——一次只开一个切面,日志才干净 |
一级 · 思路:每一步只打开一个切面类、只改一处注释,然后”重启 → 调接口 → 看日志”,最后把日志和 PPT 的规则逐条对上
二级 · 方法:切面开关 = 注释/取消注释类上的 @Aspect;异常开关 = DeptServiceImpl 里的 int i = 1/0;;接口 GET http://localhost:8080/depts;确认”是否执行”就看日志里那几行关键字(before / after / afterReturning / afterThrowing / around ... after)
三级 · 骨架:① 日志六行的关键字按顺序是 around ... ____ → ____ →(目标方法)→ ____ .... → ____ .... → around ... ____ ....;② 状态码 、异常 java.lang.____;③ 前: → ____ → ;后: → ____ → ____;⑤ 四行分别是”获取目标对象/目标类/目标方法/目标方法参数”
GET /depts,MyAspect1):
1around ... before .... ← @Around 在 pjp.proceed() 之前的代码2before .... ← @Before3[目标方法真正执行]4afterReturning .... ← @AfterReturning(正常返回才执行)5after .... ← @After(无论是否异常都执行)6around ... after .... ← @Around 在 pjp.proceed() 之后的代码int i = 1/0;):GET /depts 返回 HTTP 500,异常是 java.lang.ArithmeticException: / by zero,日志变成
1around ... before ....2before ....3afterThrowing .... ← 新出现的(只有异常才执行)4after .... ← 还在(无论是否异常都执行)afterReturning ....(PPT:有异常不会执行);新出现的是 afterThrowing ....;@Around 的后半段(around ... after ....)没有执行——异常从 pjp.proceed() 里抛出后,around 方法里 proceed() 之后的代码被跳过,异常一路向外抛成了 500。MyAspect2/MyAspect3/MyAspect4):
1MyAspect4 -> before ... ← @Order(3)2MyAspect3 -> before ... ← @Order(5)3MyAspect2 -> before ... ← @Order(8)4[目标方法]5MyAspect2 -> after ... ← @Order(8)6MyAspect3 -> after ... ← @Order(5)7MyAspect4 -> after ... ← @Order(3)3 → 5 → 8);目标方法之后:数字小的后执行(8 → 5 → 3)。MyAspect5 打开后用的表达式是 @annotation(com.itheima.anno.LogOperation),所以只有方法上贴了 @LogOperation 的调用才会打印 MyAspect5 -> before ...(课程代码里 DeptServiceImpl.list() 和 delete() 上正好贴着这个注解);把方法上的注解去掉再调,日志就不出现了——这说明 @annotation 是”按注解匹配”,跟方法属于哪个包、哪个类、叫什么名字都无关。MyAspect6):
1获取目标对象: com.itheima.service.impl.DeptServiceImpl@34a8c1a42获取目标类: com.itheima.service.impl.DeptServiceImpl3获取目标方法: list4获取目标方法参数: []getTarget()、getTarget().getClass().getName()、getSignature().getName()、getArgs()。@After 是”目标方法执行后,无论是否有异常都会执行”(相当于 finally),所以异常时照样跑;@AfterReturning 是”有异常不会执行”——这次目标方法根本没正常返回,它的条件不成立。同理 @AfterThrowing 只在抛异常时才出现。
② execution 好在批量:方法名有规律、或者”整个包都要切”时一行表达式搞定(如 execution(* com.itheima.service.impl.*.*(..)));@annotation 好在精确:要切的方法零散分布、名字没规律时,用 execution 得靠 || 拼一长串还容易漏,改成”在方法上贴一个注解”就一目了然(87 篇的操作日志案例正是这么选的)。如果你喜欢,那么欢迎来到我的世界!
了解更多暂未播放



"所有的相遇都是久别重逢。"—— 未知


