【Spring】使用Spring的AOP

1,018 阅读7分钟

Spring Aop

AOP是什么

AOP(Aspect Oriented Programming):面向切面编程。

AOP是面向切面编程, 它的思想对某个方法进行切入,为某一类事务添加一些业务之外的操作,比如说给每个操作加一个日志读写操作。

AOP作用

比如说我有一个系统,需要为每个操作添加写入日志操作,如果是使用面向过程编程的话,每个方法都需要写日志的相关代码,而使用AOP,只需要编写一个通用的日志代码即可。

AOP就是OOP(面向过程编程)的一个扩充,对现有功能的补充。

Spring Aop 实现

spring是使用cglib技术或者JDK的动态代理来实现Aop的。

默认是使用JDK动态代理,不能使用JDK动态代理的,再使用Cglib方式。

CGLIB

CGLIB底层是如何动态的生成一个目标类的子类,它是使用动态字节码技术,我们知道我们编写的Java对象都是先编译为.class文件,然后由类加载器加载到内存中变为一个Java对象的,动态字节码技术就是通过转换字节码生成新的类来实现改变一个类的内部逻辑的。(运行时起作用)

spring的Aop原理就是创建一个代理类,去继承被代理的类,对方法进行重写,在这个重写方法里添加一些额外代码。

spring帮我们创建一个子类,子类中去编写增强代码。

所以底层就是通过调整字节码从而来改变原始方法的行为。

spring 动态代理类 = 目标类 + 编写的非业务代码

JDK 动态代理

是存在于reflect包里的api,可以调用Proxy.newInstance()来创建代理对象。通过反射来获取动态代理对象。

实现原理是通过实现被代理对象实现的接口,从而生成代理对象来重写方法,并在里面织入非业务代码。

两种方式的效率

在1.6和1.7的时候,JDK动态代理的速度要比CGLib动态代理的速度要慢,但是并没有教科书上的10倍差距,在JDK1.8的时候,JDK动态代理的速度已经比CGLib动态代理的速度快很多了。

参照文章:Spring AOP中的JDK和CGLib动态代理哪个效率更高?_jdk动态代理和cglib动态代理的区别-CSDN博客

spring中来使用AOP

一般是使用AspectJ框架, 而没有使用原生的spring aop实现,使用该框架配合注解可完成 面向切面编程:

  1. 导入包, 使用这个框架,肯定是需要先在maven里导入的: image.png

  2. 编写一个MailService: 表示为业务代码:

    @Component
    public class MailService {
    
        public void sendLoginMail(String s) {
            System.out.println("发送登录邮件:"  + s);
        }
        public void sendRegistrationMail() {
            System.out.println("发送了注册邮件");
        }
    }
    
  3. 编写切面方法:

    @Aspect
    @Component
    public class LogAspect {
        @Before("execution(public * service.MailService.*(..))")
        public void doCheck() {
            System.out.println("before执行的方法,操作之前进行检查");
        }
        @Around("execution(public * service.MailService.*(..))")
        public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
            System.out.println("环绕方法执行开始:" + pjp.getSignature());
            // 获得方法返回值,这里才调用方法
            Object retVal = pjp.proceed();
            System.out.println(retVal);
            System.out.println("环绕方法执行完毕:" + pjp.getSignature());
            return retVal;
        }
    }
    

    这个切面可能大家看不懂,待会儿我仔细介绍里面的内容。

  4. 测试:

    @Configuration
    @ComponentScan("service")
    @ComponentScan("aspect")
    // 开启AspectJ动态代理
    @EnableAspectJAutoProxy
    public class AppConfig {
       public static void main(String[] args) {
           ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
           MailService service = context.getBean(MailService.class);
           service.sendRegistrationMail();
       }
    }
    
  5. 输出: 环绕方法执行开始:void service.MailService.sendRegistrationMail()
    before执行的方法,操作之前进行检查
    发送了注册邮件
    null
    环绕方法执行完毕:void service.MailService.sendRegistrationMail()


上面是一个完整的编写流程, 下面我仔细讲讲第三步的内容:

Aspect框架介绍

  1. 使用@AspectJ来表示该类是一个切面,spring使用@EnableAspectJAutoProxy来开启动态代理。

  2. 在切面类里面,我们需要为方法添加一些执行时机:

    • @Before: 方法执行之前调用
    • @AfterReturning: 方法正常返回后执行,如果方法中抛出异常,通知无法执行 必须在方法执行后才执行,所以可以获得方法的返回值。
    • @After: 最终通知,不管有没有异常,都会执行
    • @AfterThrowing: 抛出异常是执行,否则不执行
    • @Around: 环绕通知,方法执行前后分别执行,可以阻止方法的执行,必须手动执行目标方法
  3. 我们可以看到代码里面@Before(), 里面是有参数的,它表示匹配到哪些方法:

    Spring AOP AspectJ切点表达式详解_spring aspect表达式-CSDN博客

    直接去上面文章看详细解释,写的很全很全。

@before, @After等执行时机获取代理方法的参数

参照下面的文章: 可以了解里面参数的使用及如果获取方法的参数:

Spring AOP中args()和argNames的含义_@around argnames-CSDN博客

@After("execution(* com.bxp.controller.TestController.*(..)) && args(userId, userAge)")
public void after(JoinPoint point, String userId, String userAge){
    System.out.println("userId===========" + userId);
    System.out.println("userAge===========" + userAge);
}

关键点是args(), 里面需要写要获得的参数的名称, 下面的方法去接收该参数,然后就可以获取传来的参数值。

JoinPoint类的使用

它是写在@Before,@After注解的方法里的参数之中,用于获取被织入方法的信息,表示连接点对象。

@Around 使用它的子接口 ProceedingJoinPoint, 里面具有扩展方法proceed()。

JoinPoint必须放在方法参数的第一个。

AspectJ中的org.aspectj.lang.JoinPoint接口的主要方法及使用-CSDN博客

@AfterReturing与@AfterThrowing的使用

这个是@AfterReturing的使用:

@AfterReturning(value = "execution(public * service.MailService.*(..))", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, String result){
    System.out.println(result);
}

它的注解里面有一个参数returing, 可以指定一个名称用来接收方法的返回值

@AfterThrowing的使用同理,它的参数名称为throwing, 使用如下:

@AfterThrowing(value = "execution(public * service.MailService.*(..))", throwing = "result")
public void logAfterThrowing(JoinPoint joinPoint, Throwable result){
    System.out.println(result);
}

自定义切入点

当我们不想每次都去写匹配表达式:

我们可以使用@Pointcut来定义表达式函数,spring会去读取entryPoint()方法上注解的参数.

```

/**
 * 切入点
 * 配置好注解所在路劲
 */
@Pointcut("@annotation(com.example.demo.annotation.CommonBeforeAnnotation) ")
public void entryPoint() {
}
```

使用方法: @Before("entryPoint()")

类似于调用函数的写法

Aspect框架执行顺序

image.png

首先是执行

  1. @Around的前置方法
  2. 执行@Before的方法
  3. 此时执行被代理类的真实方法
  4. 关键点是: 被代理类的真实方法是否抛出异常:
    • 抛出异常:
      1. 执行@After注解的方法
      2. 执行@AfterThrowing注解的方法
    • 未抛出异常或者异常被捕获:
      1. 执行@Around的后置方法
      2. 执行@After注解的方法
      3. 执行@AfterReturning注解的方法

上面所说的是同一个切面里的执行顺序,那如果是多个切面呢?

多个切面的执行顺序

参照: spring 多个切面的执行顺序及原理_多个切面执行顺序-CSDN博客

使用Aspectj的@order函数必须从1开始排序开始(没有1会按默认顺序执行)

使用:

@Aspect 
@Component
@Order(1)//第一个执行 
public class CommonAroundAspect {}
@Aspect 
@Component
@Order(2)//第二个执行 
public class CommonAroundAspect {}

image.png

当一个注解被织入了多个切面,按照order的顺序来执行:

比如有两个切面(aop1, aop2), 定义了五个执行时间段的函数:

执行顺序为(不发生异常):

  1. aop1的Around前置方法
  2. aop1的Before
  3. aop2的Around前置方法
  4. aop2的Before
  5. aop2的逻辑代码
  6. aop2的Around后置方法
  7. aop2的After
  8. aop2的AfterReturning
  9. aop1的Around后置
  10. aop1的After
  11. aop1的AfterReturning

aop执行顺序为: 先进后出, 观察上面的图示就知道,开始早的一定结束晚。

注意: 虽然有多个aop,但真实调用被代理对象的方法的次数永远是一次