🎉🎉🎉点进来你就是我的人了
博主主页:🙈🙈🙈戳一戳,欢迎大佬指点!欢迎志同道合的朋友一起加油喔🤺🤺🤺
目录
2.2 AspectJ 语法(Spring AOP 切点的匹配方法)
1. 什么是Spring AOP?
想要知道Spring AOP,就得先了解AOP
AOP是面向切面编程,是一种思想,是对某一类事情的集中处理,其核心思想是将那些与业务逻辑无关,但是被多处业务逻辑模块共享的代码(比如日志管理,权限检查,事务管理等)抽取出来,通过预编译方式和运行期动态代理实现程序功能的统一维护的方式。这样,开发者可以将更多的精力放在处理核心业务逻辑上。
AOP是一种思想,Spring AOP是一种具体实现的框架(就类似 Spring IoC 和DI 的关系一样.)
2. 为什么要使用AOP?
对一些功能统一,使用较多,我们就可以考虑使用AOP思想进行统一处理,如登录校验,使得我们不用在每一处需要做登录校验的地方进行相同逻辑的代码实现了(下面详细解释)
- 在一个应用程序中,可能有很多操作都需要在执行前验证用户是否已经登录。如果不使用AOP,你可能需要在每个需要验证的方法中都写一段校验代码。这不仅使得代码重复,而且如果以后需要更改验证逻辑,就需要去修改每一个方法中的代码。
- 但是,如果使用了AOP,你就可以将校验逻辑抽取出来定义成一个切面,然后通过配置,让这个切面在需要验证的方法执行前运行。这样,你就只需要在一个地方编写和维护验证逻辑,大大提高了代码的可维护性和可读性。
除了登录校验,AOP还可以用在这些地方:
- 统一日志记录
- 统一方法执行时间统计
- 统一返回格式
- 统一异常处理
- 事务开启和提交
3. AOP相关组成的概念
横切关注点(目标方法的附加功能)
- 从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
- 这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
切面(类)
- 切面是一个封装横切关注点逻辑的模块。它由通知(Advice)和切点(Pointcut)组成。
- 通知定义了横切关注点的具体行为,即在何时(事件时机),何地(执行环境)以及如何执行。
- 切点则定义了通知应用的位置,即哪些连接点(Join point)应当被通知影响。
连接点(能够插入切面的点)
- 程序在运行过程中能够插入切面的点。例如,方法调用、异常抛出等。Spring只支持方法级的连接点。一个类的所有方法前、后、抛出异常时等都是连接点。
切点 (定义通知应该切入到哪些连接点上)
- 用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的,比如execution(* com.spring.service.impl.*.*(..))。
通知 (方法具体实现代码)
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
- 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)
将切面应用到目标对象从而创建一个新的代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。
- 编译期间:切面在类编译时被织入
- 类加载期间:切面在类被加载到jvm时被织入
- 运行期:切面在程序运行的某一时刻被织入,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP就是以这种方式织入切面的
目标对象(Target)
- 目标对象是那些将被AOP切面逻辑所影响的对象。这些对象通常专注于业务本身的逻辑。在AOP的架构中,目标对象本身不包含任何关于如何处理横切关注点的代码;这些逻辑被抽离出来,由AOP框架管理。
代理对象(Proxy)
- 当AOP框架在目标对象上应用切面时,它会创建一个代理对象。这个代理对象包装了目标对象,并在执行目标对象的方法时,根据切面的配置,自动执行相应的前置通知、后置通知、环绕通知等。
- 代理对象对客户端是透明的,也就是说,从客户端的角度看,它们调用的还是目标对象的方法。实际上,他们是通过代理对象来调用这些方法的,代理对象在调用前后执行额外的横切逻辑。
- 在Spring中,代理对象的实现可以通过JDK动态代理(针对接口)或者CGLIB代理(针对类)来完成。选择哪种类型的代理通常取决于目标对象的类型(是否实现接口等)。
Spring AOP支持JDK Proxy和CGLIB方式实现动态代理,这两种方式都是在程序运行期,动态的将切面织入字节码形成代理对象
-
JDK 动态代理:当被代理的目标对象实现了至少一个接口时,Spring AOP 默认使用 JDK 动态代理。在这种模式下,Spring AOP 会为目标对象实现的接口创建一个代理对象,这个代理对象会拦截所有接口方法的调用。这种方式不需要额外的库支持,因为它使用了 Java 核心库中的
java.lang.reflect.Proxy
类。 -
CGLIB 代理:当被代理的目标对象没有实现任何接口时,Spring AOP 会回退到使用 CGLIB 来创建代理对象。CGLIB(Code Generation Library)是一个第三方代码生成库,它通过在运行时动态生成被代理对象的子类来实现代理。与 JDK 动态代理相比,CGLIB 能够代理没有实现接口的类。
JDK动态代理与CGLIB的区别:
综上所述,JDK Proxy 和 CGLIB 都有自己的优缺点和适用场景。如果目标对象实
-
JDK动态代理要求被代理的类必须实现接口,因此它只能代理接口中定义的方法,当你通过代理对象调用接口中的方法时,这个调用会被转发到
InvocationHandler
的的invoke
方法,然后通常会通过反射来调用被代理对象的原始方法. CGLIB动态代理不要求被代理的类实现接口,而是通过继承被代理类。 -
JDK动态代理在生成代理对象的速度上更快,因为JDK动态代理直接使用了Java自带的API,而CGLIB则需要通过字节码技术动态生成新的类。
-
CGLIB 无法代理
final
类和final
方法,因为CGLIB通过继承目标类来创建子类,而Java语言规定final
类不能被继承,final
方法不能被重写。而JDK动态代理可以代理任意类的方法。
综上所述,JDK Proxy 和 CGLIB 都有自己的优缺点和适用场景。如果目标对象实现了接口并且需要代理的方法较少,则建议使用 JDK Proxy;如果目标对象没有实现接口或需要代理的方法较多,则建议使用 CGLIB。