AOP(Aspect Oriented Programming):面向切面编程,是一种思想,是对某一类事情的集中处理。
比如,我们需要进行登录权限校验,我们之前是在所有需要判断是否登录的方法中,各自实现验证用户登录的方法,当有了AOP之后,我们只需要在某一处配置一下,不再需要每个方法中都写相同的用户登录验证了。
AOP是一种思想,而 Spring AOP是一个框架,提供了对AOP的实现,与IOC与DI的关系类似
AOP可以实现的功能有:
1.统一的用户登录判断
2.统一日志记录
3.统一方法执行时间统计
4.统一的返回格式设置
5.统一的异常处理
6.事务的开启和提交
我们也可以说AOP可以扩充多个对象的某个能力,AOP是OOP(面向对象编程)的补充和完善,我们可以将横切关注点从应用程序的主业务逻辑中分离出来,使得这些关注点可以集中处理,从而提高代码复用性、可维护性和系统可扩展性。
1.切面(Aspect): 是实现特定横切关注点的类,它包含了一组横切关注点的描述,以及与这些横切关注点相关的行为。例如,日志切面就是一个常见的切面,它用于在方法执行前后打印日志。切面由切点(Pointcut)和通知(Advice)组成
2.连接点(Joinpoint): 是一个在应用程序执行过程中能够插入切面的点。在 Java 中,连接点通常表示应用程序中的方法调用。例如,在 Spring 框架中,Spring 的 AOP 模块可以通过代理的方式,将切面织入到目标对象的方法调用中,从而实现连接点的选择。
3.切点(Pointcut): :是一组连接点的集合,它定义了哪些方法需要被切面所拦截。切入点可以使用表达式来定义,例如,可以使用表达式 “execution(* com.example.controller…*(…))” 来定义所有 com.example.controller 包及其子包下的所有方法作为切入点。
4.通知(Advice): 是在某个连接点上执行的动作。通知类型有多种,包括前置通知(Before Advice)、后置通知(After Advice)、环绕通知(Around Advice)、异常通知(Throwing Advice)和最终通知(Finally Advice)等。
5.织入(Weaving): 是把切面代码插入到目标对象上的过程。织入分为编译期织入、类装载期织入和运行时织入三种方式,其中运行时织入最为常用。
1.添加Spring Boot AOP依赖支持
我们Spring Boot AOP在创建项目添加依赖那块是没有的,我们需要手动去maven Repository去搜一下
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
<version>2.7.2version>
dependency>
我们在Spring Boot项目中导入AOP依赖时可以不设置版本号,SpringBoot会帮助我们自动适配
@Aspect //表明该类为切面
@Component //交给IOC容器管理
public class MyAOP {
//定义切点(配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointCut() {
}
}
pointCut方法为空方法,不需要有方法体,此方法就是未来起表示作用,让我们通知明确指向的是那个切点(因为我们可能有很多个切点)
4.创建通知:
我们这里创建一个简单的前置通知演示下效果:
5.创建连接点:
@RestController
public class UserController {
@RequestMapping("/user/hello")
public String hello() {
System.out.println("hello!");
return "hello,SpringBoot AOP";
}
}
我们方法访问多次,通知也会执行多次。以上代码就是一个简单的AOP实现流程了。
我们的通知有这么多种,那么通知的执行顺序是什么样呢?
@Aspect //表明该类为切面
@Component //交给IOC容器管理
public class MyAOP {
//定义切点(配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointCut() {
}
//前置通知
@Before("pointCut()")
public void doBefore() {
System.out.println("前置通知已执行: ");
}
//后置通知
@After("pointCut()")
public void doAfter() {
System.out.println("后置通知已执行: ");
}
//return返回通知
@AfterReturning("pointCut()")
public void doAfterReturning() {
System.out.println("return返回通知已执行: ");
}
//异常返回通知
@AfterThrowing("pointCut()")
public void doAfterThrowing() {
System.out.println("异常返回通知已执行: ");
}
//定义环绕通知
@Around("pointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object obj = null;
System.out.println("Around通知开始执行");
//执行拦截方法
try {
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("Around通知执行结束");
return obj;
}
}
我们可以发现环绕通知的参数类型是ProceedingJoinPoint,ProceedingJoinPoint 是在 Spring AOP 中用于连接通知和目标方法的重要接口之一。它是 Joinpoint 接口的子接口,它提供了更强大、更灵活的连接点控制能力。
ProceedingJoinPoint 接口定义了以下方法:
1.Object[] getArgs():获取目标方法的参数数组。
2.Signature getSignature():获取目标方法的签名对象。
3.Object getTarget():获取目标对象。
4.Object proceed() throws Throwable:调用目标方法,并返回方法的返回值。如果目标方法抛出异常,则抛出该异常。
5.Object getThis():获取当前对象,即 AOP 框架生成的代理对象。
通过 ProceedingJoinPoint 接口,我们可以自由地控制目标方法的执行,实现对目标方法的增强、修改或拦截。
大家可以观察一下通知的执行顺序:先执行Around的前置通知,当执行连接点方法时,除发Before通知,然后执行连接点方法,当连接点方法执行完毕正常返回值触发AfterReturning方法,然后触发After方法,然后执行Around后置通知。
如果连接点方法出错了呢?
我们可以发现,及时出异常了,我们的通知还是会全部执行的,只是不再是执行AfterReturning返回异常,而是AfterThrowing异常,通知执行顺序保持不变
如果有多个切面都匹配到了目标方法,目标方法运行时,多个通知方法都会执行,执行顺序如下:
1.不同的切面类中,默认按照切面类的类名排序
目标方法前的通知方法:字母排名在前的先执行
目标方法后的通知方法:字母排名在前的后执行、
2.用 @Order(数字)加在切面类上控制执行顺序
目标方法前的通知方法:数字小的先执行
目标方法后的通知方法:数字小的后执行
Spring AOP是构建在动态代理的基础上,因为Spring对AOP的支持局限于方法级别的拦截
Spring AOP支持JDK Proxy和CGLIB方式实现动态代理。默认情况下,实现了接口的类,AOP会基于JDK Proxy生成代理类,没有实现接口的类,会基于CGLIB生成代理类
简单的来说就是什么时候生成代理对象
织入就是把切面应用到目标对象中并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中,一共有以下三种织入时期:
1.编译期:切面在目标类编译时被织入
2.类加载时期:切面在目标类被加载到JVM时被织入
3.运行期:切面在应用运行时的某一时刻被织入,在织入切面时,AOP会为目标对象创建一个代理对象。SpringAOP就是以这种方式织入切面的。
JDK动态代理与CGLIB都是我们Spring AOP应用的动态代理技术,但它们有以下区别:
1.JDK Proxy 只能为实现了接口的类创建代理对象,而 CGLIB 可以为任意一个类创建代理对象,包括没有实现任何接口的类。
2.JDK Proxy 是通过接口来实现代理的,因此它只能代理接口中定义的方法,如果要代理其他方法,则需要使用反射机制。而 CGLIB 则是通过生成子类的方式来实现代理,因此它能够代理类中所有非 final 的方法。
3.JDK Proxy 创建代理对象的速度相对较快,因为它是直接利用 Java API 生成代理类的字节码。而 CGLIB 创建代理对象的速度则相对较慢,因为它需要通过 ASM 技术生成字节码,并进行优化和缓存。
4.JDK Proxy 在运行时性能方面相对较好,因为它利用了 Java 系统库中已有的相关 API,并且不需要引入额外的第三方库。而 CGLIB 的运行时性能相对较差,因为它需要引入额外的第三方库,并且在生成代理类的过程中会增加一定的开销。
综上所述,JDK Proxy 和 CGLIB 都有自己的优缺点和适用场景。如果目标对象实现了接口并且需要代理的方法较少,则建议使用 JDK Proxy;如果目标对象没有实现接口或需要代理的方法较多,则建议使用 CGLIB。