⭐️前面的话⭐️
本文已经收录到《Spring框架全家桶系列》专栏,本文将介绍AspectJ表达式的书写与SpringAOP下的五种通知类型。
📒博客主页:未见花闻的博客主页
🎉欢迎关注🔎点赞👍收藏⭐️留言📝
📌本文由未见花闻原创,CSDN首发!
📆首发时间:🌴2023年5月15日🌴
✉️坚持和努力一定能换来诗与远方!
💭推荐书籍:📚《无》
💬参考在线编程网站:🌐牛客网🌐力扣🌐acwing
博主的码云gitee,平常博主写的程序代码都在里面。
博主的github,平常博主写的程序代码都在里面。
🍭作者水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
📌导航小助手📌
- 1.AspectJ表达式
- 1.1语法格式与含义
- 1.2通配符
- 1.3注意事项
- 1.4扩展知识
- 2.Spring中五种通知模式
- 2.1运行前通知@Before
- 2.2运行后通知@After
- 2.3返回后通知@AfterReturning
- 2.4抛异常通知@AfterThrowing
- 2.5环绕通知@Around
- 3.通知参数的获取
- 3.1通知方法获取参数列表
- 3.2通知方法返回返回值
- 3.3通知方法获取异常
- 4.通知类型总结
- 知识点1:@Before
- 知识点2:@After
- 知识点3:@AfterReturning
- 知识点4:@AfterThrowing
- 知识点5:@Around
1.AspectJ表达式
1.1语法格式与含义
AspectJ表达式是一种切点匹配表达式,用来匹配切点对应的方法,它的语法格式如下:
execution(访问权限修饰符(可省) 返回值 包路径.类名.方法名(形参类型表) 异常(可省略))
具体说明:
- 访问权限修饰符,表示所匹配方法的访问权限,可省略,省略表示访问权限不限,在实际开发中基本上都是对
public
修饰的方法做拦截或者增强,此项一般可以不写。 - 返回值,必填项,表示所匹配方法的返回值类型。
- 包路径,必填项,表示在哪个或者哪些包下进行查找方法。
- 类名称,必填项,表示在哪个类或者哪些类下进行匹配方法。
- 方法名,必填项,表示匹配的方法名是什么。
- 异常,可选项,表示匹配可能会抛出对应类型的方法,一般省略,异常有其他方式做统一拦截处理。
下面来做一些小练习:
execution(void com.aop.demo.controller.AddController.playing())
表示匹配任意权限返回值为void
,处于com.aop.demo.controller.AddController
类下的无参名为playing
的方法。
execution(void com.aop.demo.controller.AddController.playing(int, String))
表示匹配任意权限返回值为void
,处于com.aop.demo.controller.AddController
类下的有两个参数并且参数列表分别为int
String
并且名为playing
的方法。
1.2通配符
AspectJ表达式支持三种通配符:
*
,匹配任意字符,但只能匹配一个元素。..
,匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用。+
,表示按照类型匹配指定类的所有类,包括匹配类本身,还包括继承该类的所有子类,必须跟在类名后面,如 com.cad.Car+ ,表示继承该类的所有子类(包括本身)。
来做几个小练习:
execution(void com.aop.demo.controller.*Controller.*ing(*,*))
表示所有权限返回值为void
,在com.aop.demo.controller
包下所有以Controller
结尾的类或接口下含有两个参数并方法名以ing
结尾的所有方法。
execution(void com..controller.*Controller.*ing(..))
表示所有权限返回值为void
,在com
包已经子包下所有以Controller
结尾的类或接口下含有任意参数列表类型并方法名以ing
结尾的所有方法。
execution(void com.*.*.*.*.update())
表示所有权限返回值为void
,在com
包下任意三级包下的任意类(或接口)中无参数名为update
所有方法。
execution(void *..update())
表示所有权限返回值为void
,在com
包下任意包中的任意类(或接口)中无参数名为update
所有方法。
execution(* *..*Service+.*(..))
表示任意权限返回任意类型返回值,在任何包以及子包下以Service
结尾的类(或接口)及其继承(实现)该类(接口)的所有子类(子接口)下的任意参数列表类型任意方法名的所有方法。
这样写其实就是包含了与Service
类及其相关类的所有方法了,不推荐这么写,因为搜索也是需要成本的,还是你需要匹配什么你就具体写出来。
1.3注意事项
对于切入点表达式的编写其实是很灵活的,要注意以下几点:
- 所有代码按照标准规范开发,否则后续所有注意全部失效。
- 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了。
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)。
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用
*
通配快速描述。 - 包名书写尽量不使用…匹配,效率过低,常用
*
做单个包描述匹配,或精准匹配。 - 接口名/类名书写名称与模块相关的采用*匹配,例如
UserService
书写成*Service
,绑定业务层接口名。 - 方法名书写以动词进行准匹配,名词采用
*
匹配,例如getById
书写成getBy*
,selectAll
书写成selectAll
。 - 参数规则较为复杂,根据业务方法灵活调整。
- 通常不使用异常作为匹配规则。
1.4扩展知识
切入点指示符用来指示切入点表达式目的,在Spring AOP中目前只有执行方法这一个连接点,Spring AOP支持的AspectJ切入点指示符如下:
execution:用于匹配方法执行的连接点;
within:用于匹配指定类型内的方法执行;
this:用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;
target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;
args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;
@within:用于匹配所以持有指定注解类型内的方法;
@target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;
@args:用于匹配当前执行的方法传入的参数持有指定注解的执行;
@annotation:用于匹配当前执行方法持有指定注解的方法;
bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;
reference pointcut:表示引用其他命名切入点,只有@ApectJ风格支持,Schema风格不支持。
AspectJ切入点支持的切入点指示符还有: call
、get
、set
、preinitialization
、staticinitialization
、initialization
、handler
、adviceexecution
、withincode
、cflow
、cflowbelow
、if
、@this
、@withincode
;但Spring AOP目前不支持这些指示符,使用这些指示符将抛出IllegalArgumentException
异常,这些指示符Spring AOP可能会在以后进行扩展。
2.Spring中五种通知模式
首先来说明一下演示项目的结构:
Spring配置的实现和依赖和上一节AOP实现步骤案例是一样的。
TestServiceImpl与TestService:
public interface TestService {
public void testBefore();
public void testAfter();
public void testAround();
public void testAfterThrowing();
public void testAfterReturning();
public String testHaveReturnAround();
public void test(int a, String s);
public String test();
}
@Service
public class TestServiceImpl implements TestService {
@Override
public void testBefore() {
System.out.println("业务执行中...");
}
@Override
public void testAfter() {
System.out.println("业务执行中...");
}
@Override
public void testAround() {
System.out.println("业务执行中...");
}
@Override
public String testHaveReturnAround() {
System.out.println("业务执行中...");
return "我有返回值!";
}
@Override
public void testAfterThrowing() {
System.out.println("业务执行中...");
try {
int a = 1 / 0;
} catch (ArithmeticException e) {
System.out.println("存在除0异常!");
e.printStackTrace();
}
}
@Override
public void testAfterReturning() {
System.out.println("业务执行中...");
}
@Override
public void test(int a, String s) {
System.out.println("含参目标方法!");
}
@Override
public String test() {
System.out.println("含返回值目标方法!");
return "test返回值!";
}
}
2.1运行前通知@Before
运行前通知就是在执行目标方法前,执行通知方法,用法很简单,创建好SpringAOP环境后,我们只需在通知类中做如下步骤:
第一步,设置切点,就是使用@Pointcut注解,参数为AspectJ表达式,匹配方法。
第二步,设置通知,执行前通知使用@Before注解,参数为切点的方法名以及参数列表。
第三步,实现通知。
测试代码:
运行结果:
2.2运行后通知@After
意思就是在目标方法执行完毕后执行运行后通知方法。
运行后通知设置步骤后运行前设置步骤一模一样,就将@Before注解改为@After即可,其他的切点配置和切面配置对应着改一改即可。
切点:
切面和通知:
验证代码:
运行结果:
2.3返回后通知@AfterReturning
意思是在目标方法返回后,执行返回后通知方法,如果发生异常则该类型的通知方法不执行,做法是一样的,就不细说了。
切点:
切面和通知:
验证程序:
运行结果:
2.4抛异常通知@AfterThrowing
抛异常通知指的就是如果目标方法执行出现异常,则执行抛异常通知否则不执行,为了验证它真的在抛异常后执行了,我们在service实现类中故意让目标方法抛出异常。
用法同上。
切点:
切面与通知:
验证程序:
运行结果:
2.5环绕通知@Around
环绕通知就是在目标方法执行前可以执行环绕通知一部分逻辑,目标方法执行后执行环绕通知一部分逻辑,这个实现就有一点不一样了,因为你写一个环绕通知方法,里面的代码编译器或者jvm或者spring也不知道你那些代码在目标方法之前执行,哪些方法在目标方法之后执行。
要解决这个问题,得将目标方法抽提出来,然后将目标方法执行时机放入环绕通知合适的位置才可以解决,SpringAOP就帮助我们做了这件事情,在环绕通知方法参数中,可以加上一个ProceedingJoinPoint
类型的参数,带类型的对象出了可以调用切点方法,还可以获取具体执行方法的信息。
调用 proceed
方法就可以调用目标方法,调用getSignature
方法就能获得目标方法的详细信息,包括所在包,所在类,以及具体方法名等,当这个对象不仅仅可以获取这些内容,还有很多也能获取其他的就自己去探索吧。
首先配置切点:
配置切面和通知:
验证程序:
运行结果:
这样写当然是没有问题的,因为它运行都符合预期了,但是事实上实现通知的方法更推荐带一个返回值毕竟只是刚好我们执行的这个方法没有返回值罢了,如果一个方法实际有返回值,但是你在实现通知类的实例没有将返回值返回出去那么在验证的时候,它的返回值会是null
。
假设通知类实现不变,我们换一个有返回值的目标方法,输出它返回的结果,验证我们的猜想是否正确。
验证程序:
运行结果:
最终返回的结果是null
,那如何将返回值带出去呢?考虑到通知方法可能会用于很多目标方法,因此我们给通知方法进行Object对象的返回,我们在实现时,直接将proceed
方法执行的返回值保存再返回,就是原目标方法返回的内容了。
验证程序不变,运行结果如下:
当然,AOP通知是能够篡改原目标方法的返回值的,这样在原目标方法执行出现我们意料之外或错误的情况时,我们可以通过AOP进行纠正。
如何篡改呢?就是将通知方法返回值改变即可,假设变为执行方法的信息:
验证程序不变,运行结果如下:
3.通知参数的获取
3.1通知方法获取参数列表
对于通知,其实是可以获取到目标方法的一些信息的,不仅仅只有环绕通知能够获取,非环绕通知也是能够获取的。
对于非环绕通知可以在通知方法参数列表加上JoinPoint
类型的参数,使用Object数组就能获取目标方法的参数了。
目标方法如下:
AOP实现:
测试程序:
运行结果:
对于环绕通知也是一样的,不过使用的是我们熟悉的ProceedingJoinPoint
类型的参数,其实就是JoinPoint
的子类了,为什么不使用JoinPoint
呢,因为JoinPoint
没有proceed
方法,那为什么非环绕通知不使用ProceedingJoinPoint
类型的参数呢,因为Spring不允许在非环绕通知类使用ProceedingJoinPoint
类型的参数,两者的使用方法基本上一模一样。
运行结果:
3.2通知方法返回返回值
对于返回值,只有返回后AfterReturing
和环绕Around
这两个通知类型可以获取。
环绕通知前面已经说明过了,直接使用Object对象获取原目标方法执行结果即可,并且可以篡改。
AfterReturing
通知获取方式如下,在通知方法加上一个Object
类型参数,名字假设为ret
,再设置@AfterReturing
注解中的returning
参数为ret
,即与参数同名,即可获取参数。
目标方法如下:
AOP实现如下:
验证程序:
运行结果:
3.3通知方法获取异常
对于环绕方法就简单了,因为它需要抛目标方法可能会产生的异常,我们直接能捕获异常即可。对于其他非环绕通知,只有抛异常后通知可以获取异常,获取方式与返回后通知获取返回值比较像。
即给通知加上Throwable
类型参数t
,并在@AfterThrowing的throwing
参数写同名参数t
,然后这个t
变量就是获取到的异常。
因为与返回后通知获取返回值比较像,我就不写具体案例了,小伙伴可以基于我的案例进行补充。
我直接在前面演示抛异常的例子上进行补充,目标方法:
AOP实现:
运行结果:
至此,AOP通知如何获取数据就已经讲解完了,数据中包含参数
、返回值
、异常(了解)
。
4.通知类型总结
知识点1:@Before
名称 | @Before |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行 |
知识点2:@After
名称 | @After |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行 |
知识点3:@AfterReturning
名称 | @AfterReturning |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法正常执行完毕后执行 |
知识点4:@AfterThrowing
名称 | @AfterThrowing |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行 |
知识点5:@Around
名称 | @Around |
---|---|
类型 | 方法注解 |
位置 | 通知方法定义上方 |
作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行 |