上海古都建筑设计集团,上海办公室装修设计公司,上海装修公司高质量的内容分享社区,上海装修公司我们不是内容生产者,我们只是上海办公室装修设计公司内容的搬运工平台

【Spring AOP】@Aspect结合案例详解(一): @Pointcut使用@annotation + 五种通知Advice注解(已附源码)

guduadmin271月前

文章目录

  • 前言
    • AOP与Spring AOP
    • @Aspect简单案例快速入门
    • 一、@Pointcut
      • @annotation
      • 二、五种通知Advice
        • 1. @Before前置通知
        • 2. @After后置通知
        • 3. @AfterRunning返回通知
        • 4. @AfterThrowing异常通知
        • 5. @Around环绕通知
        • 总结

          前言

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

          AOP与Spring AOP

          在正式开始之前,我们还是先了解一下AOP与Spring AOP~

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

          【Spring AOP】@Aspect结合案例详解(一): @Pointcut使用@annotation + 五种通知Advice注解(已附源码),在这里插入图片描述,第1张

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

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

            @Aspect简单案例快速入门

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

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

              如果没有AOP基础,对于概念可能会比较懵,所以先上一个最简单案例,基于@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 {
              }
              

              pom.xml依赖:

              
                  org.springframework.boot
                  spring-boot-starter-aop
              
              

              本文所有源码预览(一共没有几行代码,很容易掌握):

              【Spring AOP】@Aspect结合案例详解(一): @Pointcut使用@annotation + 五种通知Advice注解(已附源码),在这里插入图片描述,第2张

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


              一、@Pointcut

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

              @annotation

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

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

              案例给出的@Pointcut说明:

              语法:@Pointcut(value = "@annotation(注解类名)")

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


              二、五种通知Advice

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

              注解说明
              @Before前置通知,在被切的方法执行前执行
              @After后置通知,在被切的方法执行后执行,比return更后
              @AfterRunning返回通知,在被切的方法return后执行
              @AfterThrowing异常通知,在被切的方法抛异常时执行
              @Around环绕通知,这是功能最强大的Advice,可以自定义执行顺序

              执行顺序如下:

              【Spring AOP】@Aspect结合案例详解(一): @Pointcut使用@annotation + 五种通知Advice注解(已附源码),在这里插入图片描述,第3张

              我这里在Service里定义了一个除法方法divide(),在这个方法也打上@MethodLog注解,让它可以被切面横切。

              @Service
              public class DemoService {
                  @MethodLog
                  public Integer divide(Integer a, Integer b) {
                      System.out.printf("方法内打印: a=%d  b=%d %n", a, b);
                      return a / b;
                  }
              }
              

              用于测试的controller代码,都很简单:

              @RestController
              @RequestMapping("/demo")
              public class DemoController {
                  @Autowired
                  private DemoService demoService;
                  @GetMapping("/divide")
                  public Integer divide(Integer a, Integer b) {
                      return demoService.divide(a, b);
                  }
              }
              

              1. @Before前置通知

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

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

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

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

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

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

              • getSignature()获取签名:

                MethodSignature signature = (MethodSignature) joinPoint.getSignature();

                通过signature可以获取名称 getName() 和 参数类型 getParameterTypes()

              • getTarget()获取目标类:

                Class clazz = joinPoint.getTarget().getClass();

                如果被切的类 是 被别的切面切过的类,可以使用AopUtils.getTargetClass获取一个数组,再从数组中找你期望的类。

                import org.springframework.aop.support.AopUtils;
                Class[] targets = AopUtils.getTargetClass(joinPoint.getTarget()).getInterfaces();
                
              • getArgs()获取入参值

                Object[] args = joinPoint.getArgs()

                基于这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 
                

                2. @After后置通知

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

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

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

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

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

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

                [MethodLogAspect]切面 [前置通知after] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
                方法内打印: a=10  b=2 
                [MethodLogAspect]切面 [后置通知after] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 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]
                

                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
                

                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
                

                总结

                本文主要说明了,如何通过@Aspect定义一个切面类,并结合打印Log案例主要介绍了两大核心的用法:

                • @Pointcut使用 @annotation 方式定义切入点
                • 五种通知(Advice)注解用法:@Before、@After、@AfterRunning、@AfterThrowing、@Around

                  源码0积分下载地址:https://download.csdn.net/download/scm_2008/87375584

                  如果感觉不错,欢迎关注我 天罡gg 分享更多干货: https://blog.csdn.net/scm_2008

                  大家的「关注 + 点赞 + 收藏」就是我创作的最大动力!谢谢大家的支持,我们下文见!

网友评论

搜索
最新文章
热门文章
热门标签
 
 梦见自己给别人理发  女人梦见狗咬但是没咬到  梦见吃面条预示着什么