相关推荐recommended
Spring AOP实现
作者:mmseoamin日期:2024-02-03

Spring AOP实现

  • AOP概述
    • 什么是AOP
    • 什么是Spring AOP
    • Spring AOP快速入门
      • 引入依赖
      • 实现计时器
      • Spring AOP详解
        • Spring AOP核心概念
          • 切点(Pointcut)
          • 连接点(Join Point)
          • 通知(Advice)
          • 切面(Aspect)
          • 通知类型
            • 注意事项
            • @PointCut
            • 多个切面
            • 切面优先级 @Order
            • 切点表达式
              • execution表达式
              • @annotation

                AOP概述

                Spring AOP实现,在这里插入图片描述,第1张

                什么是AOP

                Aspect Oriented Programming(面向切面编程)

                什么是面向切面编程,切面指的是某一类特定的问题,所以AOP也可以理解为面向特定方法的编程

                简单来说:AOP是一种思想,是对某一类问题的集中处理

                什么是Spring AOP

                AOP是一种思想,它的实现方法有很多,其中包括Spring AOP,也有AspectJ、CGLIB等

                Spring AOP快速入门

                引入依赖

                在pom.xml文件中添加配置

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

                实现计时器

                这里我通过一个AOP的实现来记录程序中各个函数执行的时间

                package com.example.demo.aspect;
                import lombok.extern.slf4j.Slf4j;
                import org.aspectj.lang.ProceedingJoinPoint;
                import org.aspectj.lang.annotation.Around;
                import org.aspectj.lang.annotation.Aspect;
                import org.springframework.stereotype.Component;
                @Slf4j
                @Aspect
                @Component
                public class TimeAspect {
                    @Around("execution(* com.example.demo.controller.*.*(..))")
                    public Object timeCost(ProceedingJoinPoint joinPoint) throws Throwable {
                        long start = System.currentTimeMillis();
                        Object result = joinPoint.proceed();
                        long end = System.currentTimeMillis();
                        log.info(joinPoint+"消耗时间:{}",end-start+"ms");
                        return result;
                    }
                }
                

                执行上述代码

                Spring AOP实现,在这里插入图片描述,第2张

                我们来分析一些这段代码

                Spring AOP实现,在这里插入图片描述,第3张

                这个注解,表明当前类是一个切面类

                Spring AOP实现,在这里插入图片描述,第4张

                这个注解,将该段程序交给spring来管理

                Spring AOP实现,在这里插入图片描述,第5张

                环绕通知,在目标方法的前后都会被执行.后面的表达式表示对哪些方法进行增强

                Spring AOP实现,在这里插入图片描述,第6张

                表示当前需要执行的方法

                Spring AOP实现,在这里插入图片描述,第7张

                表示开始执行当前的方法

                Spring AOP实现,在这里插入图片描述,第8张

                上面代码就解析完成了,接下来,我们开始正式学习AOP的知识

                Spring AOP详解

                Spring AOP核心概念

                切点(Pointcut)

                Pointcut的作用就是提供⼀组规则(使用AspectJ pointcut expression language 来描述), 告诉程序对哪些方法来进行功能增强.

                Spring AOP实现,在这里插入图片描述,第9张

                @Around注解里面的就是切入点的表达式

                连接点(Join Point)

                满足切点表达式规则的方法, 就是连接点. 也就是可以被AOP控制的方法,以入门程序举例, 所有 com.example.demo.controller 路径下的方法, 都是连接点.

                切点和连接点的关系

                连接点是满足切点表达式的元素. 切点可以看做是保存了众多连接点的⼀个集合

                通知(Advice)

                通知就是具体要做的工作, 指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)比如上述程序中记录业务方法的耗时时间, 就是通知

                Spring AOP实现,在这里插入图片描述,第10张

                切面(Aspect)

                切⾯(Aspect) = 切点(Pointcut) + 通知(Advice),既是整个程序

                通知类型

                上面我们讲了什么是通知, 接下来学习通知的类型. @Around 就是其中⼀种通知类型, 表示环绕通知.

                Spring中AOP的通知类型有以下几种

                • @Around: 环绕通知, 此注解标注的通知方法在目标方法前, 后都被执行

                • @Before: 前置通知, 此注解标注的通知方法在目标方法前被执行

                • @After: 后置通知, 此注解标注的通知方法在目标方法后被执行, 无论是否有异常都会执行

                • @AfterReturning: 返回后通知, 此注解标注的通知方法在目标方法后被执行, 有异常不会执行

                • @AfterThrowing: 异常后通知, 此注解标注的通知方法发生异常后执行

                接下来.我们通过程序来进行学习

                package com.example.demo.aspect;
                import lombok.extern.slf4j.Slf4j;
                import org.aspectj.lang.ProceedingJoinPoint;
                import org.aspectj.lang.annotation.*;
                import org.springframework.stereotype.Component;
                @Slf4j
                @Aspect
                @Component
                public class AspectDemo {
                    @Before("execution(* com.example.demo.controller.*.*(..))")
                    public void doBefore(){
                        log.info("执行AspectDemo doBefore");
                    }
                    @After("execution(* com.example.demo.controller.*.*(..))")
                    public void doAfter(){
                        log.info("执行AspectDemo doAfter");
                    }
                    @AfterReturning("execution(* com.example.demo.controller.*.*(..))")
                    public void doAfterReturning(){
                        log.info("doAfterReturning");
                    }
                    @AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
                    public void doAfterThrowing(){
                        log.info("doAfterThrowing");
                    }
                    @Around("execution(* com.example.demo.controller.*.*(..)) ")
                    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
                        log.info("doAround 前");
                        Object result = joinPoint.proceed();
                        log.info("doAround 后");
                        return result;
                    }
                }
                
                package com.example.demo.controller;
                import com.example.demo.aspect.MyAspect;
                import lombok.extern.slf4j.Slf4j;
                import org.springframework.stereotype.Controller;
                import org.springframework.web.bind.annotation.RequestMapping;
                import org.springframework.web.bind.annotation.RestController;
                @Slf4j
                @RestController
                public class HelloController {
                    @RequestMapping("/t1")
                    public String helloTest(){
                        return "t1";
                    }
                    @RequestMapping("/t2")
                    public String testError(){
                        Integer i = 10/0;
                        return "t2";
                    }
                }
                

                我们运行程序,我们通过访问t1的url来执行t1的程序

                Spring AOP实现,在这里插入图片描述,第11张

                可以看到,各个注解执行的时间顺序是不一样的

                而在程序正常的情况下,@AfterThrowing的通知并没有执行

                那么接下来,我们来访问t2的url

                结果是出现报错了,我们跳过程序打印的错误日志,可以看到

                Spring AOP实现,在这里插入图片描述,第12张

                执行结果变成了四个

                注意事项

                • @Around 环绕通知需要调用ProceedingJoinPoint.proceed() 来让原始方法执行, 其他通知不需要考虑目标方法执行.

                • @Around 环绕通知方法的返回值, 必须指定为Object, 来接收原始方法的返回值, 否则原始方法执行完毕, 是获取不到返回值的,而且@Around必须要有返回值

                @PointCut

                上面代码存在⼀个问题, 就是存在大量重复的切点表达式 execution(*

                com.example.demo.controller..(…)) , Spring提供了 @PointCut 注解, 把公共的切点表达式提取出来, 需要用到时引用该切入点表达式即可.

                下面我们来讲讲怎么使用

                @Pointcut("execution(* com.example.demo.controller.*.*(..))")
                    public void pointcut(){}
                

                这里的方法名可以随意取

                定义完成,怎么使用呢

                我们只需要将pointcut()把其他通知里的切入点表达式给替换掉就行了

                package com.example.demo.aspect;
                import lombok.extern.slf4j.Slf4j;
                import org.aspectj.lang.ProceedingJoinPoint;
                import org.aspectj.lang.annotation.*;
                import org.springframework.stereotype.Component;
                @Slf4j
                @Aspect
                @Component
                public class AspectDemo {
                    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
                    public void pointcut(){}
                    @Before("pointcut()")
                    public void doBefore(){
                        log.info("执行AspectDemo doBefore");
                    }
                    @After("pointcut()")
                    public void doAfter(){
                        log.info("执行AspectDemo doAfter");
                    }
                    @AfterReturning("pointcut()")
                    public void doAfterReturning(){
                        log.info("doAfterReturning");
                    }
                    @AfterThrowing("pointcut()")
                    public void doAfterThrowing(){
                        log.info("doAfterThrowing");
                    }
                    @Around("pointcut() ")
                    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
                        log.info("doAround 前");
                        Object result = joinPoint.proceed();
                        log.info("doAround 后");
                        return result;
                    }
                }
                

                这样就好啦,如果需要在其他切面中使用该切入点的定义时,需要将方法的修饰改为public,同时需要在切入点表达式中,在pointcut()前加上全限定类名,引用方式为: 全限定类名.方法名()

                多个切面

                我们创建多个切面类,观察一些结果

                package com.example.demo.aspect;
                import lombok.extern.slf4j.Slf4j;
                import org.aspectj.lang.annotation.After;
                import org.aspectj.lang.annotation.Aspect;
                import org.aspectj.lang.annotation.Before;
                import org.springframework.stereotype.Component;
                @Slf4j
                @Aspect
                @Component
                public class AspectDemo1 {
                    @Before("com.example.demo.aspect.AspectDemo.pt()")
                    public void doBefore(){
                        log.info("执行AspectDemo1 doBefore");
                    }
                    @After("com.example.demo.aspect.AspectDemo.pt()")
                    public void doAfter(){
                        log.info("执行AspectDemo1 doAfter");
                    }
                }
                package com.example.demo.aspect;
                import lombok.extern.slf4j.Slf4j;
                import org.aspectj.lang.annotation.After;
                import org.aspectj.lang.annotation.Aspect;
                import org.aspectj.lang.annotation.Before;
                import org.springframework.stereotype.Component;
                @Slf4j
                @Aspect
                @Component
                public class AspectDemo2 {
                    @Before("com.example.demo.aspect.AspectDemo.pt()")
                    public void doBefore(){
                        log.info("执行AspectDemo2 doBefore");
                    }
                    @After("com.example.demo.aspect.AspectDemo.pt()")
                    public void doAfter(){
                        log.info("执行AspectDemo2 doAfter");
                    }
                }
                package com.example.demo.aspect;
                import lombok.extern.slf4j.Slf4j;
                import org.aspectj.lang.annotation.After;
                import org.aspectj.lang.annotation.Aspect;
                import org.aspectj.lang.annotation.Before;
                import org.springframework.stereotype.Component;
                @Slf4j
                @Aspect
                @Component
                public class AspectDemo3 {
                    @Before("com.example.demo.aspect.AspectDemo.pt()")
                    public void doBefore(){
                        log.info("执行AspectDemo3 doBefore");
                    }
                    @After("com.example.demo.aspect.AspectDemo.pt()")
                    public void doAfter(){
                        log.info("执行AspectDemo3 doAfter");
                    }
                }
                

                执行程序,访问t1

                Spring AOP实现,在这里插入图片描述,第13张

                从执行结果可以看出,如果程序中有多个切面,执行顺序是依据切面类的名字来执行的

                存在多个切面类时,默认按照切面类的类名字母排序:

                • @Before 通知:字母排名靠前的先执行

                • @After 通知:字母排名靠前的后执行

                切面优先级 @Order

                对于多个切面的情况,我们可以利用 @Order来控制他们执行的顺序

                Spring AOP实现,在这里插入图片描述,第14张

                Spring AOP实现,在这里插入图片描述,第15张

                Spring AOP实现,在这里插入图片描述,第16张

                执行程序,观察结果

                Spring AOP实现,在这里插入图片描述,第17张

                很明显,执行结果如愿得到了优化

                @Order 注解标识的切面类, 执行顺序如下:

                • @Before 通知:数字越小先执行

                • @After 通知:数字越大先执行

                切点表达式

                上面的代码中, 我们⼀直在使用切点表达式来描述切点. 下面我们来介绍⼀下切点表达式的语法.

                切点表达式常见有两种表达方式

                1. execution(……):根据方法的签名来匹配
                2. @annotation(……) :根据注解匹配

                execution表达式

                execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)

                其中:访问修饰符和异常可以省略

                切点表达式⽀持通配符表达:

                1. '* :匹配任意字符,只匹配⼀个元素(返回类型, 包, 类名, 方法或者方法参数)

                  a. 包名使用* 表示任意包(⼀层包使用⼀个*)

                  b. 类名使用* 表示任意类

                  c. 返回值使用* 表示任意返回值类型

                  d. ⽅法名使用* 表示任意方法

                  e. 参数使用* 表示⼀个任意类型的参数

                2. … :匹配多个连续的任意符号, 可以通配任意层级的包, 或任意类型, 任意个数的参数

                  a. 使用… 配置包名,标识此包以及此包下的所有子包

                  b. 可以使用… 配置参数,任意个任意类型的参数

                @annotation

                首先,我们先自定义一个注解(其他已有的注解也可以)

                package com.example.demo.aspect;
                import java.lang.annotation.ElementType;
                import java.lang.annotation.Retention;
                import java.lang.annotation.RetentionPolicy;
                import java.lang.annotation.Target;
                @Retention(RetentionPolicy.RUNTIME)
                @Target({ElementType.METHOD})
                public @interface MyAspect {
                }
                

                在我们需要实现功能增强的连接点(方法)上添加我们刚刚创建的注解

                这里我们在t1上添加

                Spring AOP实现,在这里插入图片描述,第18张

                然后我们可以再创建一个切面类

                package com.example.demo.aspect;
                import lombok.extern.slf4j.Slf4j;
                import org.aspectj.lang.annotation.After;
                import org.aspectj.lang.annotation.Aspect;
                import org.aspectj.lang.annotation.Before;
                import org.springframework.stereotype.Component;
                @Slf4j
                @Aspect
                @Component
                public class MyAspectDemo {
                    @Before("@annotation(com.example.demo.aspect.MyAspect)")
                    public void doBefore(){
                        log.info("MyAspectDemo doBefore");
                    }
                    @After("@annotation(com.example.demo.aspect.MyAspect)")
                    public void doAfter(){
                        log.info("MyAspectDemo doAfter");
                    }
                }
                

                可以看到,代码中,我们的切入点表达式格式是

                @annotation(注解的全限定类名+注解名)

                接下来,我们执行程序,注意,这里需要屏蔽其他切面的影响

                访问t1

                Spring AOP实现,在这里插入图片描述,第19张

                程序执行成功了

                然后我们执行t2

                可以看到,日志并没有t2的相关结果

                那么到这里,AOP的使用,我就分享完了,感谢大家的支持