Springcloud学习笔记40--通过自定义注解实现AOP(面向切面编程)的运行时间计算(过滤器、拦截器、AOP),@Around, @Before, @AfterReturning

1.AOP介绍

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

在微服务流行的当下,在使用SpringCloud/Springboot框架开发中,AOP使用的非常广泛,尤其是@Aspect注解方式当属最流行的,不止功能强大,性能也很优秀,还很舒心!所以结合案例详细介绍@Aspect方式的切面的各种用法,力求覆盖日常开发中的各种场景。本文带来的案例是:打印Log,主要介绍@Pointcut切点表达式的@annotation方式,以及 五种通知Advice注解:@Before、@After、@AfterRunning、@AfterThrowing、@Around。

在软件开发过程中,有一些逻辑横向遍布在各个业务模块中,像权限、监控、日志、事务、异常重试等等,所以造成代码分散且冗余度高,和业务代码混夹在一起, 写起来不够优雅,改起来更是一种折磨!为了解决这些问题,AOP(Aspect Oriented Programming:面向切面编程)也就应运而生了,它是一种编程思想,就像OOP(面向对象编程)也是一种编程思想,所以AOP不是某种语言或某个框架特有的,它实现的是将横向逻辑与业务逻辑解耦,实现对业务代码无侵入,从而让我们更专注于业务逻辑本身,本质是在不改变原有业务逻辑的情况下增强横切逻辑。

在Spring中,AOP共有3种实现方式:

  • Spring1.2 基于接口的配置:Spring最早的AOP实现是完全基于接口,虽然兼容,但已经不推荐了.
  • Spring2.0+ schema-based 配置 :Spring2.0之后,提供了 schema-based 配置,也就是xml的方式配置.
  • Spring2.0+ @Aspect配置:Spring2.0之后,也提供了 @Aspect 基于注解的实现方式,也就是本文的主角,也是目前最方便、最广泛使用的方式!(推荐)

1.1 AOP通俗解释

我们一般做活动的时候,一般对每一个接口都会做活动的有效性校验(是否开始、是否结束等等)、以及这个接口是不是需要用户登录。

按照正常的逻辑,我们可以这么做。

 这有个问题就是,有多少接口,就要多少次代码copy。对于一个“懒人”,这是不可容忍的。好,提出一个公共方法,每个接口都来调用这个接口。这里有点切面的味道了。

 同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法吧。于是就有了切面的概念,我将方法注入到接口调用的某个地方(切点)。

 1.2 AOP中的相关概念

@Aspect注解方式,它的概念像@Aspect、@Pointcut、@Before、@After、@Around等注解都是来自于 AspectJ,但是功能的实现是纯 Spring AOP 自己实现的,主要有两大核心:

定义[切入点]:使用 @Pointcut 切点表达式,你可以理解成类似于正则表达式的强大东东。(本文先只介绍@annotation方式)
定义[切入时机] 和 [增强处理逻辑]五种通知Advice注解 对[切入点]执行增强处理, 包括:@Before、@After、@AfterRunning、@AfterThrowing、@Around

  • Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。
  • Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
  • Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
  • Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
  • Target(目标对象):织入 Advice 的目标对象.。
  • Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程

1.3 过滤器、拦截器、AOP功能区别

1.3.1 过滤器和拦截器的区别

1、执行顺序:过滤器 -> 拦截器 -> 切面
2、过滤器和拦截器都是请求层面的拦截,切面是方法层面的拦截
3、拦截器只在请求进入controller前执行,过滤器会在进入controller与执行后执行,切面可通过配置在方法执行前、后、环绕、异常执行
4、获取参数方式:过滤器和拦截器都需要读取HttpRequest的流,切面可通过切点直接获取

从一个请求到返回数据的整个过程看,首先是请求进入Servlet,在Servlet中存在过滤器。对url进行拦截。后经过分发器后,会进入到spring容器,在请求尚未到达具体的controller时,会先经过拦截器的preHandle方法。方法通过后,进入业务层处理,返回实体,在进入postHandle方法。经过JSP后会进入afterCompletion方法,返回页面ModleAndView.

下图是Filter、Interceptor、Aspect 的执行顺序图;

 

结合代码示例:https://gitee.com/xiaorenwu_dashije/filter_interceptor

 1.3.2 AOP

面向切面拦截的是类的元数据(包、类、方法名、参数等),拦截是在controller层的类,接口,或者service层的类,方法的执行前后进行拦截。

相对于拦截器更加细致,而且非常灵活,拦截器只能针对URL做拦截,而AOP针对具体的代码,能够实现更加复杂的业务逻辑。

1.3.3 过滤器、拦截器、AOP使用场景

(1) 认证、授权校验:拦截器
只需在进入请求前判断是否认证及是否有权限
(2) 某些异常不影响上下文逻辑:切面(环绕增强)
redis宕机、解析失败等,导致redis不可用,但通过环绕切面可以捕获异常并继续执行逻辑
(3) 接口请求时间记录:切面(环绕增强)
在请求前后记录当前时间,并计算出请求时间并记录(还可以记录一些异常情况)
(4) 过滤器:目前没有使用到,但一些请求前请求后都需要处理的场景可以使用

总结:

对于大部分请求都需要处理的逻辑,例如登录、权限校验,就用拦截器
对于部分/某一类方法,需要做增强,例如redis宕机、重复请求校验,就用切面

1.3.4 AfterReturning

AfterReturning源码:

复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface AfterReturning {
    String value() default "";

    String pointcut() default "";

    String returning() default "";

    String argNames() default "";
}
复制代码

returning:自定义的变量,标识目标方法的返回值

pointcut/value:这两个属性的作用是一样的,它们都属于指定切入点对应的切入表达式。一样既可以是已有的切入点,也可直接定义切入点表达式。当指定了pointcut属性值后,value属性值将会被覆盖。

1.3.5 @Aspect简单案例快速入门

基于@Aspect注解方式如何实现切面:

复制代码
// @Aspect和@Component定义一个切面类
@Aspect
@Component
public class MethodLogAspect {
    // 核心一:定义切点(使用@annotation方式)
    @Pointcut(value = "@annotation(com.tiangang.aop.MethodLog)")
    public void pointCut() {

    }
    // 核心二:对切点增强处理(这是5种通知中的前置通知)
    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("前置通知:" + joinPoint);
    }
}
复制代码

一共没有几行代码,就非常简单实现了在方法执行前打印日志的功能,注解类如下(对于打上这个注解的方法 都会被切面类增强处理):

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodLog {

}

1.3.6 两大核心 @Pointcut 和 Advice

接下来我们分别具体来看这两大核心 @Pointcut 和 Advice .

1.3.6.1 @Pointcut

@Pointcut切点表达式非常丰富,可以将 方法(method)、类(class)、接口(interface)、包(package) 等作为切入点,非常灵活,常用的有@annotation、@within、execution等方式,本文先只介绍@annotation方式。

@annotation方式是指:切入点 是指定作用于方法上的注解,即被Spring扫描到方法上带有该注解 就会执行切面通知。

@Pointcut(value = "@annotation(com.tiangang.aop.MethodLog)")
public void pointCut() {

}

案例给出的@Pointcut说明:
语法:@Pointcut(value = "@annotation(注解类名)")

注:只有注解类名是动态的,其它是固定写法.

1.3.6.2 五种通知Advice

通过@Pointcut定义的切点,共有五种通知Advice方式:

执行顺序如下:

 

1.3.6.2.1 @Before前置通知

前置通知在被切的方法执行之前执行!

@Before("pointCut()")
public void before(JoinPoint joinPoint) throws NoSuchMethodException {
    printMethod(joinPoint, "[前置通知before]");
}

注解语法:@Before("切点方法名()")

注:只有《切点方法名》是动态的,其它是固定写法.

方法语法:public void 方法名(JoinPoint joinPoint)

这里有个非常重要参数JoinPoint:连接点 。因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法. 里面有三个常用的方法:

基于这3个方法,可以轻松打印:被切的类名、方法名、方法参数值、方法参数类型等,printMethod方法如下:

private void printMethod(JoinPoint joinPoint, String name) throws NoSuchMethodException {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Class<?> clazz = joinPoint.getTarget().getClass();
    Method method = clazz.getMethod(signature.getName(), signature.getParameterTypes());
    System.out.printf("[MethodLogAspect]切面 %s 打印 -> [className]:%s  ->  [methodName]:%s  ->  [methodArgs]:%s%n", name, clazz.getName(), method.getName(), Arrays.toString(joinPoint.getArgs()));
}

调用测试类,输出结果如下:

[MethodLogAspect]切面 [前置通知before] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
方法内打印: a=10  b=2 
1.3.6.2.2  @After后置通知

后置通知在被切的方法执行之后执行,无论被切方法是否异常都会执行!

@After("pointCut()")
public void after(JoinPoint joinPoint) throws NoSuchMethodException {
    printMethod(joinPoint, "[后置通知after]");
}

注解语法:@After("切点方法名()")

注:只有《切点方法名》是动态的,其它是固定写法.

方法语法:public void 方法名(JoinPoint joinPoint)

1.3.6.2.3  @AfterRunning返回通知

返回通知在被切的方法return后执行,带有返回值,如果被切方法异常则不会执行!

这里多了一个参数Object result,注解上也多了一个参数:returning

@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) throws NoSuchMethodException {
    printMethod(joinPoint, "[返回通知afterReturning]");
    System.out.printf("[MethodLogAspect]切面 [返回通知afterReturning] 打印结果 -> result:%s%n", result);
}

注解语法:@AfterReturning(value = "切点方法名(), returning = "返回值参数名")

 注:只有《切点方法名》和 《返回值参数名》是动态的,其它是固定写法.

方法语法:public void 方法名(JoinPoint joinPoint, Object result)

调用测试类,输出结果如下:

[MethodLogAspect]切面 [前置通知before] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
方法内打印: a=10  b=2 
[MethodLogAspect]切面 [返回通知afterReturning] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
[MethodLogAspect]切面 [返回通知afterReturning] 打印结果 -> result:5
[MethodLogAspect]切面 [后置通知after] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
1.3.6.2.4  @AfterThrowing异常通知

异常通知只在被切方法异常时执行,否则不执行。

这里多了一个参数Exception e,表示捕获所有异常,也可以设置为具体某一个异常,例如NullPointerException、RpcException等等。注解上也多了一个参数:throwing

@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) throws NoSuchMethodException {
    printMethod(joinPoint, "[异常通知afterThrowing]");
    System.out.printf("[MethodLogAspect]切面 [异常通知afterThrowing] 打印异常 -> Exception:%s%n", e);
}

注解语法:@AfterThrowing(value = "切点方法名(), throwing = "异常参数名")

注:只有《切点方法名》和 《异常参数名》是动态的,其它是固定写法.

方法语法:public void 方法名(JoinPoint joinPoint, Exception e)

调用测试类,输出结果如下:

[MethodLogAspect]切面 [前置通知before] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 0]
方法内打印: a=10  b=0 
[MethodLogAspect]切面 [异常通知afterThrowing] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 0]
[MethodLogAspect]切面 [异常通知afterThrowing] 打印异常 -> Exception:java.lang.ArithmeticException: / by zero
[MethodLogAspect]切面 [后置通知after] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 0]
2023-01-06 21:05:06.536 ERROR 15436 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause
1.3.6.2.5  @Around环绕通知

环绕通知方法可以包含上面四种通知方法,是最全面最灵活的通知方法。

这里的参数类型和其它通知方法不同,从JoinPoint变为ProceedingJoinPoint。

@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    printMethod(joinPoint, "[环绕通知around][proceed之前]");
    // 执行方法, 可以对joinPoint.proceed()加try catch处理异常
    Object result = joinPoint.proceed();
    System.out.printf("[MethodLogAspect]切面 [环绕通知around][proceed之后]打印 -> [result]:%s%n", result);
    return result;
}

注解语法:@Around("切点方法名()")

注:只有《切点方法名》是动态的,其它是固定写法.

方法语法:public Object 方法名(ProceedingJoinPoint joinPoint) throws Throwable

调用测试类,输出结果如下:

[MethodLogAspect]切面 [环绕通知around][proceed之前] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
[MethodLogAspect]切面 [前置通知before] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
方法内打印: a=10  b=2 
[MethodLogAspect]切面 [返回通知afterReturning] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
[MethodLogAspect]切面 [返回通知afterReturning] 打印结果 -> result:5
[MethodLogAspect]切面 [后置通知after] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
[MethodLogAspect]切面 [环绕通知around][proceed之后]打印 -> [result]:5

2.使用@Before, @AfterReturning自定义注解的AOP实际使用场景

2.1 引入Pom依赖

<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2 自定义注解

复制代码
/**
 * @Author lucky
 * @Date 2022/1/26 15:33
 * 统计耗时
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ExecuteTime {
}
复制代码

2.3 AOP绑定自定义注解。并在aop中实现处理逻辑

复制代码
package com.ttbank.flep.core.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;

/**
 * @Author lucky
 * @Date 2022/1/26 15:35
 */
@Aspect
@Component
@Slf4j
public class ExecuteTimeAspect {
    private static final ThreadLocal<Long> beginTimeThreadLocal = new NamedThreadLocal<>("ThreadLocal beginTime");

    //切点:决定用注解方式的方法切还是针对某个路径下的所有类和方法进行切,方法必须是返回void类型
    @Pointcut("@annotation(com.ttbank.flep.core.aspect.ExecuteTime)")
    private void logTimeCalculateCut(){

    }

    /**
     * 前置通知 (在方法执行之前返回)用于拦截Controller层记录用户的操作的开始时间
     * @param joinPoint 切点
     */
    @Before("logTimeCalculateCut()")
    public void doBefore(JoinPoint joinPoint) throws InterruptedException{
        beginTimeThreadLocal.set(System.currentTimeMillis());
    }


    /**
     * 后置通知(在方法执行之后并返回数据) 用于拦截Controller层无异常的操作
     */
    @AfterReturning(pointcut = "logTimeCalculateCut()",returning = "ret")
    public void after(Object ret){
        Long executeTime=System.currentTimeMillis()-beginTimeThreadLocal.get();
        log.info("执行时间为:"+executeTime);
        log.info("sleepTime:"+ret);
    }

}
复制代码

2.4 AOP的使用

用在接口层校验登陆的角色是否有权限使用该接口

复制代码
/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author lucky
 * @since 2021-11-25
 */
@RestController
@RequestMapping("/student")
@Slf4j
public class StudentController {
    @Autowired
    private IStudentService studentService;

    @PostMapping("/getStudentName")
    @ExecuteTime
    public int getStudentName(){
        log.info("studentName:lucky");
        Random random=new Random();
        int sleepTime = random.nextInt(10000);
        try {
            Thread.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return sleepTime;
    }
}
复制代码

postman测试:

控制台输出:

3.@Around简单使用示例——SpringAOP增强处理

3.1 @Around的作用

  • 既可以在目标方法之前织入增强动作,也可以在执行目标方法之后织入增强动作;
  • 可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标目标方法的执行;
  • 可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值; 当需要改变目标方法的返回值时,只能使用Around方法;

虽然Around功能强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturing增强方法就可以解决的事情,就没有必要使用Around增强处理了。

3.2 注解方式

如果需要对某一方法进行增强,只需要在相应的方法上添加上自定义注解即可

(1)定义注解类

复制代码
package com.ttbank.flep.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @Author lucky
 * @Date 2022/6/27 9:47
 */
@Retention(RetentionPolicy.RUNTIME)//运行时有效
@Target(ElementType.METHOD)//作用于方法
public @interface MyAnnotation {
    String methodName () default "";
}
复制代码

(2) 定义增强处理类(切面类)

复制代码
package com.ttbank.flep.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * @Author lucky
 * @Date 2022/6/27 9:44
 */
@Aspect //标注增强处理类(切面类)
@Component //交由Spring容器管理
public class AnnotationAspect {
    /*
    可自定义切点位置,针对不同切点,方法上的@Around()可以这样写ex:@Around(value = "methodPointcut() && args(..)")
    @Pointcut(value = "@annotation(com.rq.aop.common.annotation.MyAnnotation)")
    public void methodPointcut(){}

    @Pointcut(value = "@annotation(com.rq.aop.common.annotation.MyAnnotation2)")
    public void methodPointcut2(){}
    */

    //定义增强,pointcut连接点使用@annotation(xxx)进行定义
    @Around(value = "@annotation(around)") //around 与 下面参数名around对应
    public Object processAuthority(ProceedingJoinPoint point, MyAnnotation around) throws Throwable{
        System.out.println("ANNOTATION welcome");
        System.out.println("ANNOTATION 调用方法:"+ around.methodName());
        System.out.println("ANNOTATION 调用类:" + point.getSignature().getDeclaringTypeName());
        System.out.println("ANNOTATION 调用类名" + point.getSignature().getDeclaringType().getSimpleName());
        Object result = point.proceed();//调用目标方法
        System.out.println("ANNOTATION login success");
        return result;
    }


}
复制代码

(3)Controller

复制代码
package com.ttbank.flep.controller;

import com.ttbank.flep.aspect.MyAnnotation;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

/**
 * @Author lucky
 * @Date 2022/6/27 9:49
 */
@RestController
@RequestMapping("/hello")
public class HelloController {

    @PostMapping("/login")
    @MyAnnotation(methodName = "login")
    public void login(String name){
        System.out.println("hello!"+name);

    }

}
复制代码

@RestController可以使得postman能够接收返回值,回显在postman页面中;而@Controller并不能;

postman测试:

控制台输出:

3.3 匹配方法执行连接点方式

复制代码
@Aspect
@Component
@Order(0)  //设置优先级,值越低优先级越高
public class ExecutionAspect {
    @Around(value = "execution(* com.ttbank.flep.controller..*.*(..))")
    public Object processAuthority (ProceedingJoinPoint point)throws Throwable{
        System.out.println("EXECUTION welcome");
        System.out.println("EXECUTION 调用方法:" + point.getSignature().getName());
        System.out.println("EXECUTION 目标对象:" + point.getTarget());
        //System.out.println("EXECUTION 首个参数:" + point.getArgs()[0]);
        Object proceed = point.proceed();
        System.out.println("EXECUTION success");
        return proceed;
    }

}
复制代码

注意:切面需要返回执行结果,否则会导致返回结果丢失

eg.

任意公共方法的执行:execution(public * *(..))
任何一个以“set”开始的方法的执行:execution(* set*(..))
AccountService 接口的任意方法的执行:execution(* com.xyz.service.AccountService.*(..))
定义在service包里的任意方法的执行: execution(* com.xyz.service.*.*(..))
定义在service包和所有子包里的任意类的任意方法的执行:execution(* com.xyz.service..*.*(..))

控制台输出:

 切面执行顺序:

 

 

参考文献:

https://blog.csdn.net/scm_2008/article/details/128593857 (推荐)

https://blog.csdn.net/weixin_38221481/article/details/121301519 (推荐)

https://blog.csdn.net/zhanglf02/article/details/89787937

https://blog.csdn.net/qq_41981107/article/details/85260765 (经典)

https://www.cnblogs.com/niceyoo/p/10907203.html

https://blog.csdn.net/weixin_44421461/article/details/128325700

posted @   雨后观山色  阅读(1843)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
历史上的今天:
2019-01-26 LSTM(长短期记忆网络)及其tensorflow代码应用
点击右上角即可分享
微信分享提示