文章目录
- 一. 什么是Spring AOP
- 二. 为什么要学习Spring AOP
- 三. 学习 Spring AOP
- 3.1 AOP 的组成
- 3.1.1 切面 (Aspect)
- 3.1.2 切点 (Pointcut)
- 3.1.3 通知 (advice)
- 3.1.4 连接点(Joinpoint)
- 3.2 实现 Spring AOP
- 1. 添加 Spring Boot 的 AOP 框架
- 2. 创建切面
- 3. 创建切点
- 4. 创建通知
- 5. 创建链接点
- 6. 切点表达式说明
- 3.3 Spring AOP 实现原理
- 总结
一. 什么是Spring AOP
AOP(Aspect Oriented Programming): 面向切面编程 , 它是一种思想 , 提供了一种基于切面编程的方式来对应用程序进行模块化和横向切割的方法 , 是对某一类事情的集中处理. 例如用户登录权限的效验 , 没学 AOP 之前 , 我们所有需要判断用户登录页面的方法 , 都要各自实现或调用用户验证的方法 , 这样会导致代码冗余维护成本高 , 而使用 Spring AOP 之后 , 我们只需定义一个切面 , 通过在切面中编写代码实现用户的登录验证 , 具体实现步骤是在切面中定义切点(Pointcut)和通知(Advice),切点指定需要拦截的方法,通知则指定需要在拦截方法前、后或者异常抛出时执行的代码 , 所有需要判断用户登录页面的方法就全部实现用户登录验证了 , 无需在每个方法中都实现用户登录验证.
简而言之 , AOP 是一种思想 , Spring AOP 是一个框架 , 提供了一种对 AOP 思想的实现 , 它们的关系和 loC 和 DI 类似.
二. 为什么要学习Spring AOP
想象一个场景 , 我们做后台系统时 , 除了注册和登录等几个功能无需做用户登录验证之外 , 其他几乎所有页面的 Controller 都需要先验证用户的登录状态 , 那么此时怎么处理呢?
之前的处理方式是所有的 Controller 都写一遍用户登录验证 , 然而当实现的功能越来越复杂 , 那么代码的冗余会越来越多 , 导致代码的可维护性变得非常差 , 那么如果能将这些冗余的代码统一处理 , 工作者只需专注于当前业务 , 开发效率就会大大增加. 因此对于这种功能统一 , 且使用地方较多的功能 , 就可以考虑 AOP 统一处理了.
除了统一用户登录验证之外 , AOP 还可以实现:
- 统一日志记录
- 统一方法执行时间统计
- 统一返回格式设置
- 统一的异常处理
- 事务的开启和提交等
也就是说 AOP 可以扩充多个对象的某个能力 , 所以 AOP 可以说是 OOP (Objec Oriented Programming ) 面向对象编程的补充.
三. 学习 Spring AOP
3.1 AOP 的组成
3.1.1 切面 (Aspect)
切面(Aspect) 由切点(Pointcut) 和通知(Advice) 组成 , 而某一方面的具体内容就是一个切面 , 比如用户的登录判断就是一个切面 , 简而言之 , 切面就是一个特殊的类 , 里面包含了切点和通知.
3.1.2 切点 (Pointcut)
定义一个拦截规则 , 指在应用程序中需要拦截的方法集合,可以使用表达式或者正则表达式等方式进行定义。切点是一个抽象的概念,它并不代表具体的方法,而是代表一组方法,这些方法需要被拦截并执行通知。
3.1.3 通知 (advice)
执行 AOP 具体的业务逻辑 , 指在切点上执行的操作。通知可以在切点执行前、执行后、抛出异常时执行,以便实现不同的功能需求。
通知包括以下几种类型:
-
前置通知(Before advice):在切点执行前执行的通知。
-
后置通知(After advice):在切点执行后执行的通知,不管切点执行成功或者失败都会执行。
-
返回通知(After returning advice):在切点执行成功后执行的通知。
-
异常通知(After throwing advice):在切点抛出异常后执行的通知。
-
环绕通知(Around advice):在切点执行前后都可以执行的通知,可以控制切点的执行。
3.1.4 连接点(Joinpoint)
所有可能触发切点的点就叫做连接点 , 指在应用程序中实际执行的方法,即切点所定义的方法的实际执行过程。连接点是一个具体的概念,它代表了程序中的一个具体的执行点,例如方法的调用、异常抛出、字段的访问等
AspectJ 支持三种通配符
*****: 匹配任意字符串 , 只匹配一个元素 (包 , 类 , 方法 , 方法参数)
… : 匹配任意字符 , 可以匹配多个元素 , 在表示类时 , 必须和 * 联合使用
+: 表示按类型匹配指定类的所有类 , 必须跟在类名后面 , 如 com.cad.Car+ , 表示继承该类的所有子类包括本身
切点表达式由切点函数组成 , 其中 execution() 是最常用的切点函数 , 用来匹配函数 , 语法为:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
3.2 实现 Spring AOP
1. 添加 Spring Boot 的 AOP 框架
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Tips: 虽然 Spring 框架内置了原生 AOP , 但由于我们目前是 SpringBoot 项目 , 所以需要添加 SpringBoot 框架的 AOP.
2. 创建切面
//创建切面
@Aspect//表明此类为一个切面
@Component
public class UserAOP {
}
3. 创建切点
@Aspect
@Component
public class UserAOP {
// 定义切点 , 此处使用 AspectJ 语法 (配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..)")
public void pointcut(){
}
}
4. 创建通知
//创建切面
@Aspect
@Component
public class UserAOP {
// 定义切点 , 此处使用 AspectJ 语法 (配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.* (..))")
public void pointcut(){
}
// 前置通知
@Before("pointcut()")
public void doBefore(){
System.out.println("执行了前置通知: " + LocalDateTime.now());
}
// 后置通知
@After("pointcut()")
public void doAfter(){
System.out.println("执行流后置通知:" + LocalDateTime.now());
}
// 环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("开始执行环绕通知");
Object obj = joinPoint.proceed();
System.out.println("结束执行环绕通知");
return obj;
}
}
5. 创建链接点
@RestController
public class UserController {
@RequestMapping("/user/sayhi")
public String sayhi(){
System.out.println("执行了 sayhi 方法");
return "hi springboot aop";
}
}
6. 切点表达式说明
AspectJ 支持三种通配符
*: 匹配任意字符 , 只匹配一个元素 (包 , 类 , 方法 , 方法参数)
… : 匹配任意字符 , 匹配多个元素 , 在表示类时必须和 * 连用.
+: 表示按照类型匹配指定类的所有类 , 必须跟在类名之后 , 如 com.cad.Car+ , 表示继承该类的所有子类包括本身.
切点表达式由切点函数组成 , 其中 execution() 就是常见的切点函数 , 用来匹配方法 , 语法为:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
修饰符和异常可以省略
表达式示例
- execution(* com.example.demo.UserController.*(…)) 匹配UserController 类里的所有方法
- execution(* com.example.demo.UserController+.*(…)) 匹配该类的子类包括该类的所有方法
3.3 Spring AOP 实现原理
Spring AOP 是构建在动态代理基础上 , 因此 Spring 对 AOP 的支持局限于方法级别的拦截.
Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理. 默认情况下 , 实现了接口的类 , 使用 AOP 会基于 JDK 生成动态代理类 , 没有实现接口的类 , 会基于 CGLIB 生成代理类.
织入(Weaving): 代理的生成时机
织入就是把切面应用到目标对象并创建新的代理的过程 , 切面在指定的连接点被织入到目标对象中.
在目标对象的生命周期里有多个点可以织入:
- 编译期: 切面在目标类编译时被织入. AspectJ 的织入编译器就是以这种方式织入切面的. 例如 lombok
- 类加载期: 切面在目标类加载到 JVM 时被织入. 这种方式需要特殊的类加载器(ClassLoader). 它可以在目标类被引入应用之前增强该目标类的字节码.
- 运行期: 切面在应用运行的某一时刻被织入. 一般情况下 , 在织入切面时 , AOP 容器会为目标对象动态创建一个代理对象. Spring AOP 就是以这种方式实现的.
Spring AOP 底层实现:
1.JDK 动态代理
速度快 , 通过反射调用目标方法
要求被代理的类一定要实现接口
2.CGLIB
通过实现代理类的子类来实现动态代理 , 但不能代理被 final 修饰的类和方法.
综上所述,JDK动态代理和CGLIB动态代理各有优缺点,应根据实际情况选择使用。如果目标类实现了接口,则建议使用JDK动态代理;如果目标类没有实现接口或者需要代理类中的非接口方法,则建议使用CGLIB动态代理。
总结
AOP 是对某方面功能的统一实现 , 它是一种思想 , Spring AOP 是对 AOP 的具体实现 , Spring AOP 可通过 AspectJ 注解的方式来实现 AOP 功能 , Spring AOP 的实现步骤是:
- 添加 AOP 支持框架
- 定义切面和切点
- 定义通知
Spring AOP 是通过动态代理的方式 , 在运行期间将 AOP 代码织入到程序中的 , 实现方式有两种 , JDK Porxy 和 CGLIB.