Spring AOP

什么是 Spring AOP

AOP 是(Aspect Oriented Programming),也就是面向切面编程。是⼀种思想,是对某⼀类事情的集中处理。Spring AOP 提供了一种对 AOP 思想的实现。是对 OOP(面向对象思想) 思想的扩充

比如用户登录权限的校验,不使用 AOP 的话,所有需要判断用户登录的页面,都要各自实现或调用用户验证的方法。然而有了 AOP 之后,只需要在某一处配置一下,所有需要验证用户登陆的页面就可以全部实现用户登录验证了,不在需要每个方法都写相同的用户登陆验证了。

出来上面说的这种的用户登录判断外,还可以实现:

  1. 统一日志记录
  2. 统一方法执行时间统计
  3. 统一的返回格式设置
  4. 统一的异常处理
  5. 事务的开启和提交等

AOP 的组成

切面

AOP 是面向 切面编程,所以这是 AOP 最重要的功能。定义 AOP 是针对哪个统一的功能,这个功能就叫做一个切面。比如用户登录功能或方法的统计日志,他们就各自是一个切面。切面是由 切点 和 通知组成的。

连接点

就是所有可能触发 AOP(拦截方法的点),就称为连接点。

切点

切点会提供一个规则,用来匹配连接点,并且来实现通知。也就是定义 AOP 拦截的规则的。

通知

切面要完成的工作就是通知。就是规定 AOP 执行的时机和执行的方法。通知注解如下:

  1. 前置通知: 使用 @Before,通知方法会在目标方法调用之前执行。
  2. 后置通知: 使用 @After,通知方法会在目标方法返回或者抛出异常后调用。
  3. 返回之后通知: 使用 @AfterReturning,通知方法会在目标方法返回后调用。
  4. 抛异常后通知: 使用 @AfterThrowing,通知方法会在目标方法抛出异常后调用。
  5. 环绕通知: 使用 @Around,通知包裹了被通知的方法,在被通知的方法通知之前和调用之后,执行自定义的行为。

AOP 的整个组成部分概念就像这张图片,以多个页面抖音访问用户登录权限为例:
在这里插入图片描述

Spring AOP 实现

  1. 在项目中添加 Spring AOP 框架。
  2. 定义切面。
  3. 定义切点。
  4. 实现通知。

添加 Spring AOP 框架

在创建项目的时候,一般是没有 AOP 框架的,所以就需要在中央仓库里面找到 AOP 依赖,然后导入就好了:

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

然后刷新 pom 就可以了。

定义切面

先创建一个类,然后加上 @Aspect 注解 就可以了。然后在设置切点。

@Aspect
@Component
public class UserAspect {
}

设置切点

通过 @Pointcut 注解就可以定义切点了。固定写法如下 @Pointcut("execution(* com.example.springaop.controller.UserController.*(..))")

代码意思如下:
在这里插入图片描述
这里切点的语法是 AspectJ 表达式语法。

AspectJ 语法

我们设置 切点 的语法就是 AspectJ 语法,也就是 Spring AOP 切点的匹配语法:
在这里插入图片描述
修饰符和异常通常都是省略的。public 就是公共方法,* 就是任意方法。

返回值不能省略,void 表示没有返回值,String 表示返回字符串,* 表示任意返回值。

Aspect 语法中的通配符

  1. * :表示匹配任意的内容,用在返回值,包名,类名,方法名都可以使用。
  2. .. :匹配任意字符,可以使用在方法参数上,如果用在类上需要配合 * 一起使用。
  3. + :表示按照类型匹配指定类的所有类,必须挂在类名后面,如 com.cad.Car+ 表示继承该类的所有子类,也包括本身。

包,类,方法如下
在这里插入图片描述
表达式实例
在这里插入图片描述

使用通知

前置通知

通过 @Before 来实现前置通知:

@Before("pointcut()")
public void doBefore() {
    System.out.println("执行前置通知");
}

前置通知里面针对的切点就是我们之前设置的 pointcut 。设置 controller 类当中的方法:

@RestController
@RequestMapping("/user")
public class UserController {

    @RequestMapping("/sayhi")
    public String sayHi() {
        System.out.println("sayHi");
        return "hello word";
    }
    @RequestMapping("/sayhello")
    public String sayHello() {
        System.out.println("sayHello");
        return "word hello";
    }
}

访问结果如下:

在这里插入图片描述
IDEA 当中的日志如下,就是先执行 前置通知,然后才是方法内容:
在这里插入图片描述
然后执行 sayhello:
在这里插入图片描述
IDEA 的日志如下:
在这里插入图片描述
也是先执行前置通知。

后置通知

就是执行力目标方法之后在执行通知。通过 @After 注解就可以实现。代码如下:

@After("pointcut()")
public void doAfter() {
    System.out.println("执行后置通知");
}

然后启动项目访问,日志如下:
在这里插入图片描述

返回之后通知

通过 @AfterReturning 注解来实现,就是在 return 之前执行,执行完之后,才会执行 后置通知:

@AfterReturning("pointcut()")
public void doAfterReturning() {
    System.out.println("AfterReturning  返回之后通知");
}

访问结果如下:
在这里插入图片描述

抛出异常之后通知

通过 @AfterThrowing 注解来实现,就是抛出异常之后才会执行此通知:

@AfterThrowing("pointcut()")
public void doAfterThrowing() {
    System.out.println("doAfterThrowing 抛出异常后通知");
}

然后在访问路径的方法当中制造一个异常:

@RequestMapping("/sayhello")
public String sayHello() {
    System.out.println("sayHello");
    int num = 10/0;
    return "word hello";
}

访问结果如下:
在这里插入图片描述
因为抛出异常了,所以就没有返回,也就没有返回通知了。

环绕方法通知

通过 @Around 注解来实现,环绕通知可以实现统计方法运行的时间,但是使用的时候 必须要有 ProceedingJoinPoint 参数:

@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
    Object result = null;
    System.out.println("Around 前置通知");
    try {
        result = joinPoint.proceed();
    } catch (Throwable e) {
        throw new RuntimeException(e);
    }
    System.out.println("Around 后置通知");
    return result;
}

访问结果如下:
在这里插入图片描述

基于环绕通知统计方法执行时间

通过 Spring 提供的 StopWatch 来统计时间:

@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
    Object result = null;
    StopWatch stopWatch = new StopWatch();
    System.out.println("Around 前置通知");
    try {
        stopWatch.start();
        result = joinPoint.proceed();
        stopWatch.stop();
    } catch (Throwable e) {
        throw new RuntimeException(e);
    }
    System.out.println("Around 后置通知");
    System.out.println(joinPoint.getSignature().getName() + " 方法时间:" + stopWatch.getTotalTimeMillis() + "ms");
    return result;
}

访问结果如下:
在这里插入图片描述

Spring AOP 实现原理

Spring AOP 是构建在 动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截。Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理。

不使用 AOP 代理

不使用 AOP 代理的时候,前后端交互如下:
在这里插入图片描述
是前端直接调用后端来实现交互的,比如说要有登陆验证,每次都需要和后端进行验证,都需要调用验证的方法。

使用 AOP 代理

使用 AOP 代理的话,就相当于是在前后端之间的中间商插手了:
在这里插入图片描述
AOP 会把所有符合拦截规则的方法都给拦截了。如果想要交互的话,就得先通过 AOP 的验证,然后才能与后端交互。到了这里,Spring 当中存储的就是动态代理的对象了。

动态代理的实现

Spring 当中提供了两种 动态代理 的实现方式:

  1. JDK Proxy(JDK 动态代理)
  2. CGLIB Proxy。默认情况下,Spring AOP 都会采用 CGLIB 来实现动态代理。是通过 继承代理对象 来实现动态代理的(子类拥有父类的所有功能)。但是不能代理最终类(也就是被 final 修饰的类),此时就需要 JDK Proxy 来实现动态代理了。

代理过程如下:
在这里插入图片描述

织入(Weaving):代理的生成时机

织入 ,与 AOP 的4个定义(切面,切点,连接点,通知) 是 并列的关系。织入,就是 AOP 第5个定义:就是把切⾯应⽤到⽬标对象并创建新的代理对象的过程,切⾯在指定的连接点被织⼊到⽬标对象中。说白了:织入,就是描述 动态代理 是在什么时候生成的。

目标对象在生命周期里有多个点可以进行织入,一共三个点

  1. 编译期:切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就是以这种⽅式织⼊切⾯的。
  2. 类加载器:切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载器(ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。
  3. 运行期:切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊切⾯的。

Spring AOP 是在 运行期 生成的动态代理

JDK 动态代理实现(依靠反射)

JDK 实现时,先通过实现 InvocationHandler 接⼝创建⽅法调⽤处理器,再通过 Proxy 来创建代理类:

import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

//动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被代理类必须实现接⼝
public class PayServiceJDKInvocationHandler implements InvocationHandler
{
    //⽬标对象即就是被代理对象
    private Object target;
    public PayServiceJDKInvocationHandler( Object target) {
        this.target = target;
    }
    
    //proxy代理对象,method 执行的目标方法,args 执行方法所需的参数
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //1.安全检查
        System.out.println("安全检查");
        
        //2.记录⽇志
        System.out.println("记录⽇志");
        
        //3.时间统计开始
        System.out.println("记录开始时间");
        
        
        //通过反射调⽤被代理类的⽅法 - 重点
        // invoke 就是实例反射的意思,把 目标对象 target 和 响应的参数args,传进去
        Object retVal = method.invoke(target, args);
        
        
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }
    public static void main(String[] args) {
    // PayService 它是一个接口,但对接的类 需要根据实际情况来决定
    // 下面就是 对应着 阿里的支付服务的实体类
        PayService target= new AliPayService();
        
       //⽅法调⽤处理器
        InvocationHandler handler =
                new PayServiceJDKInvocationHandler(target);
                
        //创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
        PayService proxy = (PayService) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                new Class[]{PayService.class},
                handler
        );
        // 调用 代理类
        proxy.pay();
    }
}

CGLIB 动态代理实现

实现的方式和 JDK 的一模一样,只有三处不同:

  1. 实现接口换成了 MethodInterceptor
  2. 重写方法的传参发生了改变
  3. 调用的时候,比较简单一些

代码如下:

import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.Method;
public class PayServiceCGLIBInterceptor implements MethodInterceptor {
    //被代理对象
    private Object target;
    public PayServiceCGLIBInterceptor(Object target){
        this.target = target;
    }
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
       //1.安全检查
        System.out.println("安全检查");
       //2.记录⽇志
        System.out.println("记录⽇志");
        //3.时间统计开始
        System.out.println("记录开始时间");
        
        //通过cglib的代理⽅法调⽤
        Object retVal = methodProxy.invoke(target, args);
        
        //4.时间统计结束
        System.out.println("记录结束时间");
        return retVal;
    }
    public static void main(String[] args) {
        PayService target= new AliPayService();
        PayService proxy= (PayService) Enhancer.create(target.getClass(),
                new PayServiceCGLIBInterceptor(target));
        proxy.pay();
    }
}

JDK 和 CGLIB 的区别

  1. JDK 是官方提供的,CGLIB 是第三方提供的。
  2. CGLIB 比 JDK 更高效。
  3. CGLIB 是通过 实现 继承 代理对象 来实现 动态代理的。(如果代理的对象是 最终类(被 final 修饰的类),Spring AOP 才会去调用 JDK 的方式生成 动态代理。)
  • 14
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 13
    评论
评论 13
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lockey-s

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值