🎉🎉🎉点进来你就是我的人了
博主主页:🙈🙈🙈戳一戳,欢迎大佬指点!欢迎志同道合的朋友一起加油喔🤺🤺🤺
目录
1. 什么是Spring AOP?
2. 为什么要使用AOP?
3. AOP相关组成的概念
切面(类)
连接点(所有可能触发切点的点)
切点 (方法)
通知 (方法具体实现代码)
4. Spring AOP 的实现
1. 添加SpringBoot AOP 框架支持
2. 定义切面和切点(设置拦截规则)
2.1 Aspect 语法中的通配符
2.2 AspectJ 语法(Spring AOP 切点的匹配方法)
3. 定义通知
4. 创建连接点 (所有触发拦截方法的点)
5. Spring AOP 的实现原理
织入(Weaving)
JDK动态代理与CGLIB的区别:
1. 什么是Spring AOP?
想要知道Spring AOP,就得先了解AOP
AOP是面向切面编程,是一种思想,是对某一类事情的集中处理,其核心思想是将那些与业务逻辑无关,但是被多处业务逻辑模块共享的代码(比如日志管理,权限检查,事务管理等)抽取出来,通过预编译方式和运行期动态代理实现程序功能的统一维护的方式。这样,开发者可以将更多的精力放在处理核心业务逻辑上。
AOP是一种思想,Spring AOP是一种具体实现的框架(就类似 Spring IoC 和DI 的关系一样.)
2. 为什么要使用AOP?
对一些功能统一,使用较多,我们就可以考虑使用AOP思想进行统一处理,如登录校验,使得我们不用在每一处需要做登录校验的地方进行相同逻辑的代码实现了(下面详细解释)
- 在一个应用程序中,可能有很多操作都需要在执行前验证用户是否已经登录。如果不使用AOP,你可能需要在每个需要验证的方法中都写一段校验代码。这不仅使得代码重复,而且如果以后需要更改验证逻辑,就需要去修改每一个方法中的代码。
- 但是,如果使用了AOP,你就可以将校验逻辑抽取出来定义成一个切面,然后通过配置,让这个切面在需要验证的方法执行前运行。这样,你就只需要在一个地方编写和维护验证逻辑,大大提高了代码的可维护性和可读性。
除了登录校验,AOP还可以用在这些地方:
- 统一日志记录
- 统一方法执行时间统计
- 统一返回格式
- 统一异常处理
- 事务开启和提交
3. AOP相关组成的概念
切面(类)
指的是某一类事情的具体内容,比如用户登录校验就是一个切面,日志统一记录也是一个切面,切面由切点和通知组成,通常切面是一个类
- 切⾯是包含了:通知、切点和切⾯的类,相当于
AOP 实现的某个功能的集合
。- 可以把切面看作是一个模块,它的目标是完成一些特定的工作,这些工作通过通知实现,而切点则确定了这些工作应当在何处执行。
连接点(所有可能触发切点的点)
所有可能触发 切点的点(拦截方法的点)就称之为连接点
连接点是程序执行的某个特定位置,如类的某个方法调用前、调用后、方法捕获到异常后等。在Spring AOP中,一个连接点总是代表一个方法的执行
切点 (方法)
切点用来定义AOP拦截的规则的(如登录拦截规则),通常是类中的一个方法(该方法没有实现)
- 切点实际上定义了你的通知应该在何处执行。这个"何处"就是由满足切点规则的那些连接点组成的集合。
- 你可以将切点视为一个包含了多个连接点的集合,这个集合中的每个元素(即每个连接点)都将应用通知(执行通知中的方法)
通知 (方法具体实现代码)
"通知"是一个重要的概念,它是具体的业务逻辑,定义了何时(触发的条件)和如何(执行的操作)进行拦截。
- 切⾯也是有⽬标的 ——它必须完成的⼯作。在 AOP 术语中,
切⾯的⼯作被称之为通知
。 - Spring 切⾯类中,可以在⽅法上使⽤以下注解,会设置⽅法为通知⽅法,在满⾜条件后会通知本⽅法进⾏调⽤
- 前置通知@Before:这个注解标注的方法会在目标方法(实际要执行的方法)被调用前执行
- 后置通知@After:这个注解标注的方法会在目标方法完成后执行,无论目标方法是否成功完成。
- 环绕通知@Around:这个注解标注的方法会在目标方法调用前后都执行,可以自行决定何时执行目标方法。
- 异常通知@AfterThrowing:这个注解标注的方法会在目标方法抛出异常后执行。
- 方法返回通知@AfterReturning:这个注解标注的方法会在目标方法成功返回后执行
举个例子,假设你有一个服务类,包含了一个
login
方法,你希望在这个方法执行前进行日志记录,那么login
方法就是你的"目标方法"。你可以定义一个切点来匹配这个方法,然后通过一个前置通知(@Before)来在这个方法执行前记录日志。
AOP 整个组成部分的概念如下图所示,以多个⻚⾯都要访问⽤户登录权限为例:
4. Spring AOP 的实现
1. 添加 SpringBoot AOP 框架支持.➡️ 中央仓库链接
2. 定义切面和切点.
3. 定义通知.
1. 添加SpringBoot AOP 框架支持
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.7.11</version>
</dependency>
2. 定义切面和切点(设置拦截规则)
- 其中
pointcut
⽅法为空⽅法,它不需要有⽅法体,此⽅法名就是起到⼀个“标识
”的作⽤,标识下⾯的通知⽅法具体指的是哪个切点(因为切点可能有很多个)
@Aspect //标识当前类为一个切面
@Component //不能省略
public class UserAop {
// 定义一个切点(设置拦截规则)
@Pointcut("execution(* com.example.demo.common.controller.UserController.*(..))")
public void pointcut(){
}
}
2.1 Aspect 语法中的通配符
- * : 表示匹配任意的内容,用在返回值,包名,类名,方法都可以使用
- .. : 匹配任意字符,可以使用在方法参数上,如果用在类上需要配合 * 一起使用
- + : 表示匹配指定类及其它底下的所有子类,比如 com.Car+ 表示匹配 Car 及其所有子类
2.2 AspectJ 语法(Spring AOP 切点的匹配方法)
切点表达式由切点函数组成,其中 execution() 是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
其中修饰符和异常可以省略,下面是具体含义:
- 修饰符(一般省略):public(公共方法),*(任意)
- 返回类型(不能省略):void,String,int,*(任意)
- 包:com.demo(固定包),com.*(com包下所有),com.demo..(com.demo包下所有子包含自己)
- 类:Test(固定类),Test*(以之开头),*test(以之结尾),*(任意)
- 方法名(不能省略):addUser(固定方法),add*(以add开头),*add(以add结尾),*(任意)
- 参数:(),(int),(int,String),(..)任意参数
- 异常(可省略,一般不写)
3. 定义通知
- 切点,通知都在 UserAop 这个类中.
// 前置通知
@Before("pointcut()")
public void doBefore() {
System.out.println("执行了前置通知 " + LocalDateTime.now());
}
// 后置通知
@After("pointcut()")
public void doAfter() {
System.out.println("执行了后置通知 " + LocalDateTime.now());
}
// return 之前通知
@AfterReturning("pointcut()")
public void doAfterReturning() {
System.out.println("执行了返回之后通知 " + LocalDateTime.now());
}
// 抛出异常之前通知
@AfterThrowing("pointcut()")
public void doAfterThrowing() {
System.out.println("执行了返回之后通知 " + LocalDateTime.now());
}
// 环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) { // 拿到目标方法的执行对象
// 这个对象是框架能否继续执行后续流程的对象, 与目标方法是否返回值, 以及返回类型无关
Object res = null;
// 前置业务代码
System.out.println("执行了环绕通知的前置方法 " + LocalDateTime.now());
try {
// 执行目标方法
res = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
// 后置业务代码
System.out.println("执行了环绕通知的后置方法 " + LocalDateTime.now());
return res;
}
4. 创建连接点 (所有触发拦截方法的点)
//创建连接点
@RestController
public class UserController {
@RequestMapping("/sayHi")
public String sayHi() {
System.out.println("执行了 sayHi 方法");
return "hi, spring boot aop.";
}
}
【分析】
- 切点的拦截规则表示,拦截 UserController 类里面的所有方法. (后面细讲拦截规则)
- 切点里的 pointcut() 方法是为了给后面的通知使用该方法名.
- 前四种通知都很简单, 除了注解不一样, 其他都一样. 主要是环绕通知, 它带有参数, 参数 joinPoint 的意义就是拿到目标方法中的执行对象, 也就是 UserController 中的所有方法的执行对象. 用这个对象调用 proceed() 就是执行 UserController 中的所有方法. 环绕通知的返回值 res , 和目标方法的方法类型无关, 它只决定框架能否继续执行后续流程.
- ProceedingJoinPoint 接口定义了以下方法:
- Object[] getArgs():获取目标方法的参数数组。
- Signature getSignature():获取目标方法的签名对象。
- Object getTarget():获取目标对象。
- Object proceed() throws Throwable:调用目标方法,并返回方法的返回值。如果目标方法抛出异常,则抛出该异常。
- Object getThis():获取当前对象,即 AOP 框架生成的代理对象。
通过 ProceedingJoinPoint 接口,我们可以自由地控制目标方法的执行,实现对目标方法的增强、修改或拦截。
【测试】
- 现在我们测试一下我们的 AOP 是否可以拦截 UserController 中的方法.
- 当我们通过浏览器访问 sayHi() 方法时, 观察控制台的信息:
【结论】
- 成功拦截了 UserController 中的方法.
- 环绕通知的前置方法在最前面执行, 环绕通知的后置方法在最后执行.
- 通过上面 AOP的简单实现, 我们大概也就知道了如何对 "用户登录效验功能进行统一处理了. 只需要在同一将所有调用用户登录校验的方法写在一个类中, 或者一个文件夹下, 然后设置对应的拦截规则即可.
如果想进一步验证 "拦截规则" 是否正确, 可以在 controller 包下再建一个 TestController 类, 然后写一个方法, 并通过浏览器访问, 观察是否还打印了这几个通知方法. (答案肯定是没有, 下来可以自己试一下)
5. Spring AOP 的实现原理
由于Spring AOP 的实现建在动态代理基础上的, Spring 对 AOP 的支持局限于方法级别的拦截.
动态代理呢就是当调用者调用目标对象的时候,它不会与目标对象接触,而是由代理类进行调用
织入(Weaving)
简单的来说就是什么时候生成代理对象
织入就是把切面应用到目标对象中并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中,一共有以下三种织入时期:
- 编译期间:切面在类编译时被织入
- 类加载期间:切面在类被加载到jvm时被织入
- 运行期:切面在程序运行的某一时刻被织入,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP就是以这种方式织入切面的
Spring AOP支持JDK Proxy和CGLIB方式实现动态代理,这两种方式都是在程序运行期,动态的将切面织入字节码形成代理对象
JDK动态代理与CGLIB的区别:
- JDK动态代理要求被代理的类必须实现接口,因此它只能代理接口中定义的方法,当你通过代理对象调用接口中的方法时,这个调用会被转发到一个InvocationHandler,然后通常会通过反射来调用被代理对象的原始方法. CGLIB动态代理不要求被代理的类实现接口,而是通过继承被代理类
- JDK动态代理性能相对较高,生成代理对象速度较快,而CGLIB动态代理性能相对较低,生成代理对象速度较慢
- CGLIB动态代理无法代理final类和final方法,而JDK动态代理可以代理任意类的方法
综上所述,JDK Proxy 和 CGLIB 都有自己的优缺点和适用场景。如果目标对象实现了接口并且需要代理的方法较少,则建议使用 JDK Proxy;如果目标对象没有实现接口或需要代理的方法较多,则建议使用 CGLIB。