Spring AOP实践
概念详解
- Pointcut:切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。切点分为 execution 方式和 annotation 方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。
- Advice:处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。
- Aspect:切面,即 Pointcut 和 Advice。
- Joint point:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
- Weaving:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。
实例
1package com.zxy.demo.aspect;
2
3import com.alibaba.fastjson.JSON;
4import org.aspectj.lang.JoinPoint;
5import org.aspectj.lang.annotation.*;
6import org.springframework.stereotype.Component;
7
8/**
9 * @description: study
10 * 模块名称:
11 * 说明: 测试切面
12 * 作者(@author): zxy
13 * 创建日期: 2022年1月17日14:53:33
14 */
15
16@Aspect
17@Component
18public class TestAspect {
19 @Pointcut("@annotation(com.zxy.demo.annotations.AsyncBizLog)")
20 private void servicePointcut() {
21 }
22
23 @Before("servicePointcut()")
24 public void doBefore(JoinPoint joinPoint) {
25 System.out.println("-------------------方法进入时-------------------");
26 System.out.println("参数:" + joinPoint.getArgs()[0]);
27 }
28 @AfterReturning(value = "servicePointcut()", returning = "retValue")
29 public void doAfter(JoinPoint joinPoint, Object retValue) {
30 System.out.println("-------------------方法返回后-------------------");
31 System.out.println("方法返回值:" + JSON.toJSONString(retValue));
32 }
33
34 @AfterThrowing(pointcut = "servicePointcut()", throwing = "ex")
35 public void doAfterThrow(Throwable ex) {
36 System.out.println("-------------------方法抛出异常-------------------");
37 }
38
39 @After(value = "servicePointcut()")
40 public void doAfter() {
41 System.out.println("-------------------方法结束-------------------");
42 }
43
44}
45
1package com.zxy.demo.annotations;
2
3import java.lang.annotation.*;
4
5@Retention(RetentionPolicy.RUNTIME)
6@Target({ElementType.METHOD})
7@Documented
8public @interface AsyncBizLog {
9}
10
注解
@Pointcut
- @Pointcut 注解,用来定义一个切点,即上文中所关注的某件事情的入口,切入点定义了事件触发时机。
- @Pointcut 注解指定一个切点,定义需要拦截的东西,这里介绍两个常用的表达式:一个是使用 execution(),另一个是使用 annotation()。
execution 表达式:
以 execution(* com.mutest.controller...(..))) 表达式为例:
- 第一个 * 号的位置:表示返回值类型,* 表示所有类型。
- 包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,在本例中指 com.mutest.controller 包、子包下所有类的方法。
- 第二个 * 号的位置:表示类名,* 表示所有类。
- *(..):这个星号表示方法名, 表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。
annotation() 表达式:
annotation()
方式是针对某个注解来定义切点,比如我们对具有 @PostMapping 注解的方法做切面,可以如下定义切面:
1@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")
2public void annotationPointcut() {}
3
然后使用该切面的话,就会切入注解是 @PostMapping
的所有方法。这种方式很适合处理 @GetMapping、@PostMapping、@DeleteMapping
不同注解有各种特定处理逻辑的场景。
@Around
@Around
注解用于修饰 Around 增强处理,Around 增强处理非常强大,表现在:
- @Around 可以自由选择增强动作与目标方法的执行顺序,也就是说可以在增强动作前后,甚至过程中执行目标方法。这个特性的实现在于,调用 ProceedingJoinPoint 参数的 procedd()方法才会执行目标方法。
- @Around 可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。
Around
增强处理有以下特点:
- 当定义一个 Around 增强处理方法时,该方法的第一个形参必须是 ProceedingJoinPoint 类型(至少一个形参)。在增强处理方法体内,调用 ProceedingJoinPoint 的 proceed 方法才会执行目标方法:这就是 @Around 增强处理可以完全控制目标方法执行时机、如何执行的关键;如果程序没有调用 ProceedingJoinPoint 的 proceed 方法,则目标方法不会执行。
- 调用 ProceedingJoinPoint 的 proceed 方法时,还可以传入一个 Object[ ]对象,该数组中的值将被传入目标方法作为实参——这就是 Around 增强处理方法可以改变目标方法参数值的关键。这就是如果传入的 Object[ ]数组长度与目标方法所需要的参数个数不相等,或者 Object[ ]数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。
@Around
功能虽然强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的 Before、AfterReturning 就能解决的问题,就没有必要使用 Around 了。如果需要目标方法执行之前和之后共享某种状态数据,则应该考虑使用 Around。尤其是需要使用增强处理阻止目标的执行,或需要改变目标方法的返回值时,则只能使用 Around 增强处理了。
@Before
@Before
注解指定的方法在切面切入目标方法之前执行 ,可以做一些 Log
处理,也可以做一些信息的统计,比如获取用户的请求 URL
以及用户的 IP
地址等等
JointPoint
对象很有用,可以用它来获取一个签名,利用签名可以获取请求的包名、方法名,包括参数(通过 joinPoint.getArgs()
获取)等。
@After
@After
注解和 @Before
注解相对应,指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 Log 处理。
@AfterReturning
@AfterReturning
注解和 @After
有些类似,区别在于 @AfterReturning
注解可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理。
需要注意的是,在 @AfterReturning
注解 中,属性 returning
的值必须要和参数保持一致,否则会检测不到。该方法中的第二个入参就是被切方法的返回值,在 doAfterReturning
方法中可以对返回值进行增强,可以根据业务需要做相应的封装。
@AfterThrowing
当被切方法执行过程中抛出异常时,会进入 @AfterThrowing
注解的方法中执行,在该方法中可以做一些异常的处理逻辑。要注意的是 throwing
属性的值必须要和参数一致,否则会报错。该方法中的第二个入参即为抛出的异常。