Spring AOP

引言

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

AOP 是一种思想,表示对某一类事情的集中处理。Spring AOP 是一个框架,它是对 AOP 思想的实现。

为什么要使用 AOP 思想?

想象一个场景,我们在做用户验证的时候,除了登录和注册不需要做用户验证之外,几乎其他所有页面都需要先验证用户登录的状态,验证成功后,后面的代码才能继续走下去。以往我们的处理方式,是将每个 Controller 都要写一遍用户验证,然而当功能越来越多的时候,即 Controller 越多的时候,那么我们要写的登录验证也越来越多,而这些方法又是相同的,这么多的方法就会增加代码修改和维护的成本。此时,我们就可以考虑利用 AOP 来统一处理了。

而对于功能统一,且使用地方较多的功能,就可以优先考虑 AOP 思想。

一、AOP 组成

1-1

1. 切面 (Aspect)
切面包含了通知、切点和切面的类,相当于 AOP 实现某个功能的集合。

2. 连接点 (Join Point)
所有可能触发 AOP 的页面 / 数据,可以被称为连接点。

3. 切点 (Pointcut)
切点相当于保存了众多连接点的一个集合,如果把切点看作成一个表格,那么连接点就是表格中的一个个数据。另外,切点相当于增强的方法。

4. 通知 (Advice)
通知规定了 AOP 执行时机和执行方法。切面的工作就被称之为通知。

AOP 有五种通知:

① 前置通知
② 后置通知
③ 抛出异常之后的通知
④ 返回数据之后的通知
⑤ 环绕通知

二、Spring AOP 的使用

步骤1 添加依赖

由于在创建 Spring Boot 的时候,页面没有为我们提供 AOP 依赖,所以我们在 maven 仓库中,搜索如下依赖,并添加至 pom.xml 文件中。

1-2

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
	<version>2.7.3</version>
</dependency>

步骤2 添加切面和切点

1-3

切点指的是具体要处理的某一类问题,比如用户登录权限验证就是一个具体的问题;记录所有方法的执行日志就是一个具体的问题。

@Aspect // 当前类是一个切面
@Component
public class UserAspect {

    // 定义一个切点(设置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    public void point1() {

    }
}

注意:

① 在代码中,我们通过 " @Pointcut " 这个注解来定义一个切点,并在注解的括号中注明表达式,表示拦截规则。

② point1 方法为空方法,它并不需要有方法体,它本身的方法名只是作为一个 " 标识 " 作用,这样后面的 Advice 通知就可以使用当前的切点。( 根据不同的拦截规则,切点可以设置多个 )

③ " @Pointcut " 注解后面的表达式遵循了 AspectJ 语法。

1-4

AspectJ 语法

execution() 是最常用的切点函数,用来匹配方法,比方说,我们需要对非法用户进行拦截,那么就可以在后端的方法代码中,利用 execution() 函数进行拦截,匹配到哪些方法,哪些方法就需要用来拦截非法用户。

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

其中,返回类型、方法、参数都不能省略。

AspectJ 支持的三种通配符:

*  : 匹配任意字符,只匹配一个元素(包,或类,或方法,或方法参数)

.. : 匹配任意字符,可以匹配多个元素,在表示类时,必须和 * 联合使用

+  : 表示按照类型匹配指定类本身包括其所有子类,必须跟在类名后面

示例:

execution(* com.cad.demo.User.*(..)) :匹配 User 类里的所有方法

execution(* com.cad.demo.User+.*(..)) :匹配该类的子类包括该类的所有方法

execution(* com.cad.*.*(..)) :匹配 com.cad 包下的所有类的所有方法

execution(* com.cad..*.*(..)) :匹配 com.cad 包下、子孙包下所有类的所有方法

execution(* addUser(String, int)) :匹配 addUser 方法,且第一个参数类型是 String,第二个参数类型是 int.

步骤3 添加 Advice 通知

在 UserAspect 类中,实现切面:

@Aspect // 当前类是一个切面
@Component
public class UserAspect {

    // 定义一个切点(设置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    public void point1() {

    }

    // 1. 定义 point1 切点的前置通知
    @Before("point1()") // 选择 point1 切点,即遵循其拦截规则
    public void doBefore() {
        System.out.println("前置通知:被执行了");
    }

    // 2. 定义 point1 切点的后置通知
    @After("point1()")
    public void doAfter() {
        System.out.println("后置通知:被执行了");
    }

    // 3. 定义 point1 切点的返回之后通知
    @AfterReturning("point1()")
    public void doAfterReturning() {
        System.out.println("执行了 AfterReturning 方法");
    }

    // 4. 定义 point1 切点的异常通知
    @AfterThrowing("point1()")
    public void doAfterThrowing() {
        System.out.println("执行了 AfterThrowing 方法");
    }

    // 5.定义 point1 切点的环绕通知
    @Around("point1()")
    public Object doAround(ProceedingJoinPoint joinPoint) {
        Object result = null;
        System.out.println("环绕通知:开始");
        try {
            // 执行目标方法,以及目标方法所对应的相应的通知
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("环绕通知:结束");
        return result;
    }
}

在 UserController 类中,实现与前端交互:

@RestController
public class UserController {

    @RequestMapping("/hello1")
    public String hello1() {
        System.out.println("hello1 方法被执行了");
        return "你好,世界!";
    }

    @RequestMapping("/hello2")
    public String hello2() {
        System.out.println("hello2 方法被执行了");
        int a = 10 / 0;
        return "你好,世界!";
    }
}

前端访问 hello1 与 hello2 路径后,查看 IDEA 控制台:

1-5

在上面的输出结果中,我们可以清晰地看到五个 Advice 通知的执行顺序。鉴于此,我们就可以在后端的代码中,对前端传来的参数进行一些处理,亦可以对后端即将要返回的数据进行优化。

比方说登录验证,我们就可以在前置通知中,设置一个拦截器,也就是说在前端与后端交互的 Controller 层之前,就可以直接拦截非法用户。

比方说异常处理,如果用户输入了一个参数,导致服务器异常,而此异常后端并没有事先意料到,我们就可以在后置通知中,将原本 500 的错误状态码封装成一个 json 数据给前端,前端再拿此 json 数据优化成一个用户看得懂的保存信息。(这是一件非常有意义的事情,因为用户并不是程序员,他们根本看不懂后端的错误提示,甚至连前端拿到 500 这样的错误信息,也不知道后端出现了什么问题。)

三、使用 AOP 统计某个类中每个方法的执行时间

对上面的代码做一些修改:

在 UserAspect 类中,实现切面:

@Aspect // 当前类是一个切面
@Component
public class UserAspect {

    // 定义一个切点(设置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    public void point1() {

    }

    // 使用 AOP 统计 UserController 每个方法的执行时间
    @Around(("point1()"))
    public Object doAround2(ProceedingJoinPoint joinPoint) {
        // Spring 框架提供的 StopWatch,类似于 System.currentTimeMillis() 时间戳
        StopWatch stopWatch = new StopWatch();
        Object result = null;
        try {
            stopWatch.start();
            // 执行目标方法,以及目标方法所对应的相应的通知
            result = joinPoint.proceed();
            stopWatch.stop();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println(joinPoint.getSignature().getDeclaringType() + "." +
                            joinPoint.getSignature().getName() + " 方法花费的时间" +
                            stopWatch.getTotalTimeMillis()+ "ms");
        return result;
    }
}

在 UserController 类中,实现与前端交互:

@RestController
public class UserController {

    @RequestMapping("/hello2")
    public String hello2() {
        System.out.println("hello2 方法被执行了");
        return "你好,世界!";
    }

    @RequestMapping("/hello3")
    public String hello3() throws InterruptedException {
        Thread.sleep(3000);
        System.out.println("hello3 方法被执行了");
        return "hello, world!";
    }
}

1-6

四、Spring AOP 的实现原理

Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截。我们知道,Spring Boot 单元测试的最小单元是方法级别的,所以可以看见,Spring AOP 的拦截功能做到了非常精准。

如下图所示,以往我们前端直接就可以与后端交互了,但是这就会带来安全隐患,当有 AOP 作为后端代理后,它就会在前端访问时,进行一些校验和拦截,显然数据在传输过程中更加安全。

1-7

Spring AOP 动态代理实现的技术

1. JDK Proxy (JDK 动态代理)
2. GGLB Proxy

JDK Proxy 是官方提供的动态代理,但实际上它并不好用,所有 Spring AOP 一般来说会优先使用后者。当后者无法正常使用时,才会采用前者。

需要明确,GGLB Proxy 是通过继承代理对象来实现动态代理的,即子类拥有父类的所有功能的方式。但它不能代理最终类 (被final 修饰的类)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

十七ing

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

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

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

打赏作者

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

抵扣说明:

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

余额充值