Spring AOP

前言

❤️❤️❤️SSM专栏更新中,各位大佬觉得写得不错,支持一下,感谢了!❤️❤️❤️

Spring + Spring MVC + MyBatis_冷兮雪的博客-CSDN博客

终于到了本专栏最后一个模块------AOP。

一、什么是 Spring AOP?

在专栏第一篇我们就简单说了Spring框架提供了对AOP的支持,那Spring AOP和AOP有什么不同吗?

AOP(Aspect Oriented Programming):面向切面编程,它是⼀种思想,它是对某⼀类事情的集中处理。比如用户登录权限的效验,没学 AOP 之前,我们所有需要判断用户登录的页面(中的方法),都要各自实现或调用用户验证的方法,然而有了 AOP 之后,我们只需要在某⼀处配置⼀下,所有需要判断用户登录页面(中的方法)就全部可以实现用户登录验证了,不再需要每个方法中都写相同的用户登录验证了。

而 AOP 是⼀种思想,Spring AOP 是⼀个框架,提供了⼀种对 AOP 思想的实现,它们的关系和 IoC 与 DI 类似(思想与实现)。

二、为什么学AOP?

有这样⼀个场景,当我们在做后台系统时,除了登录和注册等几个功能不需要做用户登录验证之外,其他几乎所有页面调用的前端控制器( Controller)都需要先验证用户登录的状态,那这个时候我们要怎么处理呢?

我们之前的处理方式是每个 Controller 都要写⼀遍用户登录验证,然而当你的功能越来越多,那么你要写的登录验证也越来越多,而这些方法又是是相同的,这么多的方法就会代码修改和维护的成本。那有没有简单的处理方案呢?答案是有的,对于这种功能统⼀,且使用的地方较多的功能,就可以考虑 AOP来统⼀处理了。

除了统⼀的用户登录判断之外,AOP 还可以实现:

  • 统⼀日志记录:AOP可以在方法调用前后记录日志信息,如方法名、参数、返回值等,从而实现统一的日志记录,方便系统的监控和调试。
  • 统⼀方法执行时间统计:AOP可以用于在方法调用前后记录时间戳,计算方法的执行时间,帮助性能优化和分析。
  • 统⼀的返回格式设置:AOP可以在方法调用后对返回结果进行统一的格式化,如将结果封装成特定的数据结构或添加统一的响应头。
  • 统⼀的异常处理:AOP可以捕获方法执行过程中的异常,并进行统一的异常处理,例如将异常信息记录下来,提供友好的错误提示等。
  • 事务的开启和提交等:AOP可以用于在方法调用前后开启和提交事务,从而实现统一的事务管理,保障数据的一致性。

也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。

三、Spring AOP 应该怎么学习呢?

Spring AOP 学习主要分为以下 3 个部分:

  1. 学习 AOP 是如何组成的?也就是学习 AOP 组成的相关概念。
  2. 学习 Spring AOP 使用。
  3. 学习 Spring AOP 实现原理。

下面我们分别来看。

1、AOP 组成

Ⅰ、切面(Aspect)

切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。

通俗的理解,在程序中就是一个处理某方面具体问题的一个类。类里面包含了很多方法,这些方法就是切点和通知。

切面是包含了:通知、切点和切面的类,相当于 AOP 实现的某个功能的集合

Ⅱ、切点(Pointcut)

Pointcut 是匹配 Join Point 的谓词。

Pointcut 的作用就是提供⼀组规则(使用 AspectJ pointcut expression language 来描述)来匹配 Join Point,给满足规则的 Join Point 添加 Advice。切面可以说就是用来进行主动拦截的规则(配置)。

切点相当于保存了众多连接点的⼀个集合(如果把切点看成⼀个表,而连接点就是表中⼀条⼀条的数据)。

切点表达式说明

AspectJ ⽀持三种通配符

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

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

+ :表示按照类型匹配指定类的所有类,必须跟在类名后面,如 com.cad.Car+ ,表示继承该类的 所有子类包括本身

切点表达式由切点函数组成,其中 execution() 是最常用的切点函数,用来匹配方法,语法为:

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

修饰符和异常可以省略,具体含义如下:

*Ⅲ、*通知(Advice)

切面也是有目标的 ------它必须完成的工作。在 AOP 术语中,切面的工作被称之为通知。

通知:定义了切面是什么,何时使用,其描述了切面要完成的工作,还解决何时执行这个工作的 问题。

Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:

  • 前置通知使用 @Before:在执行目标方法之前执行的方法就叫做前置通知。
  • 后置通知使用 @After:在执行了目标方法之后执行的方法就叫做后置通知。
  • 返回之后通知使用 @AfterReturning:目标方法执行了返回数据(return) 时,执行的通知。
  • 抛异常后通知使用 @AfterThrowing:在执行目标方法出现异常时,执行的通知。
  • 环绕通知使用 @Around:在目标方法执行的周期范围内执行之前、执行中、执行后)部可以执行的方法叫做环绕通知。(使用频率最高)

切点相当于要增强的方法。

Ⅳ、连接点(Join Point)

应用执行过程中能够插入切面的⼀个点,这个点可以是方法调用时,抛出异常时,甚至修改字段 时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

可能会触发 AOP 规则的所有点(所有请求)。
AOP 整个组成部分的概念如下图所示,以多个页面都要访问⽤户登录权限为例:

2、Spring AOP 实现

Spring AOP 实现步骤:

  1. 添加 Spring AOP 依赖
  2. 定义切面(创建切面类)
  3. 定义切点(配置拦截规则)
  4. 定义通知的实现

接下来我们使用 Spring AOP 来实现⼀下 AOP 的功能,完成的目标是拦截所有 UserController 里面的方法,每次调用 UserController 中任意⼀个方法时,都执行相应的通知事件。

Ⅰ、添加 AOP 框架支持

因为在创建项目时候是没有SpringAop框架选择的,所以我们需要在创建好项目之后再添加依赖。

在 pom.xml 中添加如下配置:

然后选择对应的Spring版本

Maven Repository: org.springframework.boot >> spring-boot-starter-aop (mvnrepository.com)

加版本号也可以,不加也可以

XML 复制代码
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

项目引用完成:

Ⅱ、定义切面和切点

切点指的是具体要处理的某⼀类问题,比如用户登录权限验证就是⼀个具体的问题,记录所有方法的执行日志就是⼀个具体的问题,切点定义的是某⼀类问题。 Spring AOP 切点的定义如下,在切点中我们要定义拦截的规则,具体实现如下:

java 复制代码
package com.example.demo.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect // 表明此类为⼀个切⾯
@Component//随着框架的启动而启动
public class UserAspect {
    // 切点(配置拦截规则)
    @Pointcut("execution(* com.example.demo.aop.UserController.*(..))")
    public void pointcut(){
        
    }
}

其中 pointcut 方法为空方法,它不需要有方法体,此方法名就是起到⼀个"标识"的作用,标识下面的 通知方法具体指的是哪个切点(因为切点可能有很多个)。

Ⅲ、定义相关通知

java 复制代码
package com.example.demo.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect // 表明此类为⼀个切⾯
@Component//随着框架的启动而启动
public class UserAspect {
    // 切点(配置拦截规则)
    @Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
    public void pointcut(){
    }
    //前置通知
    @Before("pointcut()")//对应切点方法名
    public void beforeAdvice(){
        System.out.println("执行了前置通知!");
    }
    //后置通知
    @After("pointcut()")//对应切点方法名
    public void afterAdvice(){
        System.out.println("执行了后置通知!");
    }
    //环绕通知
    @Around("pointcut()")//对应切点方法名
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("进入环绕通知了!");
        Object obj=null;
        //执行目标方法
        obj=joinPoint.proceed();
        System.out.println("退出环绕通知了!");
        return obj;
    }
}

UserController类(执行通知):

java 复制代码
package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("hi")
    public String sayHi(String name){
        System.out.println("执行了sayHi方法!");
        return "Hi "+name;
    }
    @RequestMapping("/hello")
    public String sayHello(){
        System.out.println("执行了sayHello方法!");
        return "hello java!";
    }
}

对照类:ArticleController(不执行通知)

java 复制代码
package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/art")
public class ArticleController {
    @RequestMapping("/hi")
    public String sayHi(){
        return "Hi word!";
    }
}

Ⅳ、运行

访问 art/hi 没有通知:

访问 user/hi 有通知

sayHi方法:

sayHello方法:

好了UserController类中的两个方法都可以执行通知。

3、Spring AOP 实现原理

Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截。 Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理。默认情况下,实现了接口的类,使用AOP 会基于 JDK 生成代理类,没有实现接口的类,会基于 CGLIB生成代理类。

Spring默认代理是JDK Proxy(有局限),而Spring Boot进行了优化,默认为CGLIB。

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

织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。 在目标对象的生命周期里有多个点可以进行织入:

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

动态代理

此种实现在设计模式上称为动态代理模式,在实现的技术手段上,都是在 class 代码运行期,动态的织入字节码。 我们学习 Spring 框架中的AOP,主要基于两种方式:JDK 及 CGLIB 的方式。这两种方式的代理目标都是被代理类中的方法,在运行期,动态的织入字节码生成代理类。

  • CGLIB是Java中的动态代理框架,主要作⽤就是根据目标类和方法,动态生成代理类。
  • Java中的动态代理框架,几乎都是依赖字节码框架(如 ASM,Javassist 等)实现的。
  • 字节码框架是直接操作 class 字节码的框架。可以加载已有的class字节码文件信息,修改部 分信息,或动态⽣成⼀个 class。

JDK 和 CGLIB 实现的区别

1、JDK 实现,要求被代理类必须实现接口,之后是通过 InvocationHandler 及 Proxy,在运行时动态的在内存中生成了代理类对象,该代理对象是通过实现同样的接口实现(类似静态代理接口实现的方式),只是该代理类是在运行期时,动态的织入统⼀的业务逻辑字节码来完成。

2、CGLIB 实现,被代理类可以不实现接口,是通过继承被代理类,在运行时动态的生成代理类对象。

相关推荐
archko4 分钟前
telophoto源码查看记录
java·服务器·前端
uhakadotcom7 分钟前
使用 Google Pay API 集成 Web 应用
后端
Asthenia04127 分钟前
为何在用 Netty 实现 Redis 服务时,要封装一个 BytesWrapper?
后端
在京奋斗者18 分钟前
深入源码级别看spring bean创建过程
java·spring
大萌神Nagato35 分钟前
Johnson算法 流水线问题 java实现
java·算法
来自星星的坤41 分钟前
Spring Boot 邮件发送配置遇到的坑:解决 JavaMailSenderImpl 未找到的错误
java·开发语言·spring boot·后端·spring
F_lander1 小时前
蓝桥杯冲刺题单--二分
java·算法·蓝桥杯
PureWT1 小时前
Springboot----@Role注解的作用
java
uhakadotcom1 小时前
将游戏上传至 Steamworks 的简单步骤
后端·面试·github
慕瑾华1 小时前
Go语言的物联网
开发语言·后端·golang