作者:~小明学编程
文章专栏:Spring框架
格言:热爱编程的,终将被编程所厚爱。
目录
什么是SpringAOP?
AOP 是(Aspect Oriented Programming),也就是面向切面编程。是⼀种思想,是对某⼀类事情的集中处理。Spring AOP 提供了一种对 AOP 思想的实现,是对 OOP(面向对象思想) 思想的扩充。
就拿我之前的项目来说博客系统,里面的任何一个链接你在点击之后都需要先对你的用户名和密码来进行一个验证然后再执行后面的内容,但是问题就来了我们总不能每次添加新的方法或者功能然后都去添加用户验证吧我们想着是把验证这一个功能统一起来,降低与其它代码的耦合性,也就是我们各管各的就行了。
不仅如此:以下这些功能都需要我们去将其统一的处理,不然的话我们每个功能都需要去加这些代码是在是很繁琐。
- 统一日志记录
- 统一方法执行时间统计
- 统一的返回格式设置
- 统一的异常处理
- 事务的开启和提交等
AOP的组成
切面(Aspect)
定义:切⾯(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。
上面是切面的定义,看起来晦涩难懂,通俗的来说的话切面定义了在程序中需要被拦截的一组相关的操作。切面可以是一个类、一个接口或者一个注解。
连接点(Join Point)
应⽤执⾏过程中能够插⼊切⾯的⼀个点,这个点可以是⽅法调⽤时,抛出异常时,甚⾄修改字段
时。切⾯代码可以利⽤这些点插⼊到应⽤的正常流程之中,并添加新的⾏为。
前面我们说了切面但是我们怎么去连接这个切面呢,这就是我们为什么需要连接点,连接点是去连接我们的切面的。
切点(Pointcut)
Pointcut 的作⽤就是提供⼀组规则(使⽤ AspectJ pointcut expression language 来描述)来
匹配 Join Point,给满⾜规则的 Join Point 添加 Advice。
通俗的来说切点就是我们具体的拦截规则,拦截之后然后发送通知。
通知(Advice)
切面要完成的工作就是通知。就是规定 AOP 执行的时机和执行的方法。
常见的通知注解如下:
- 前置通知: 使用 @Before,通知方法会在目标方法调用之前执行。
- 后置通知: 使用 @After,通知方法会在目标方法返回或者抛出异常后调用。
- 返回之后通知: 使用 @AfterReturning,通知方法会在目标方法返回后调用。
- 抛异常后通知: 使用 @AfterThrowing,通知方法会在目标方法抛出异常后调用。
- 环绕通知: 使用 @Around,通知包裹了被通知的方法,在被通知的方法通知之前和调用之后,执行自定义的行为。
SpringAOP的实现
AOP只是一种思想而SpringAOP是用代码将我们的这种思想给实现了。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.7.11</version>
</dependency>
这是我们SpringAOP的依赖,其中版本需要根据自己的springboot版本来进行选择。
定义切面
@Aspect
@Component
public class UserAspect {
}
其中@Aspect注解是将我们的当前类定义成一个切面。
设置切点
//作为切点配置拦截规则
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut() {
}
这里的切点就是配置我们的拦截规则,我们想要拦截什么样子的请求。
AspectJ
上面我们看到Pointcut里面有一串复杂的符号,这些符号是AspectJ的语法下面我们就来说说这些语法。
固定拦截写法:
切点函数:execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
接着我们就是在这里面写上我们具体的内容。
AspectJ ⽀持三种通配符
- * :匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)
- .. :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤。
- + :表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.cad.Car+ ,表示继承该类的所有⼦类包括本身。
修饰符(一般省略):,如public 公共,* 任意。
返回类型(一般不省略):,void 返回没有值,String 返回值字符串,* 任意。
包:(一般情况下要有但是可以省略)
- com.gyf.crm 固定包
- com.gyf.crm.*.service crm包下面子包任意 (例如: com.gyf.crm.staff.service)
- com.gyf.crm.. crm包下面的所有子包 (含自己)
- com.gyf.crm.*.service.. crm包下面任意子包,固定目录service,service目录任意包
类(一般情况下要有但是可以省略):UserServicelmpl 指定类,*Impl 以Impl结尾,User 以User开头。
方法名(不可省略):addUser 固定方法,add* 以add开头,*Do 以Do结尾。
参数:()无参,(int)一个参数,(int,int)两个参数,.. 任意参数。
异常:throws 一般省略。
通知
常见的通知注解有下面几种下面逐一的给大家做演示:
- 前置通知:@Before()
- 后置通知:@After()
- 环绕通知:Around()
@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;
}
}
拦截内容:
@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() {
return "hello world";
}
}
接下来我们就看一下我们访问这些接口的时候是否会给我们通知。
可以看到其中的执行顺序,先执行环绕通知,然后前置通知,执行方法替,再执行后置通知,最后是退出我们的环绕通知。
SpringAOP的原理
Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截,Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理。
我们在前后端交互的时候没有代理的时候前后端是直接进行交互的,但是这样我们就需要去校验前端的一些数据等,如果我们有代理的话那么前端会先将数据传到代理代理做一个处理然后代理再将数据给后端,如此一来我们就可以专注于代码逻辑了。
织入(代理的生成时机)
织⼊是把切⾯应⽤到⽬标对象并创建新的代理对象的过程,切⾯在指定的连接点被织⼊到⽬标对
象中。
在⽬标对象的⽣命周期⾥有多个点可以进⾏织⼊:
- 编译期:切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就是以这种⽅式织⼊切⾯的。
- 类加载期:切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载(ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。
- 运⾏期:切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊切⾯的。
动态代理
动态代理指的是在程序运行时动态地创建一个实现特定接口或一组接口的对象,该对象可以拦截并处理所有传递给它的方法调用。动态代理通常是通过在运行时生成字节码来实现的,从而避免了在编译时手工编写代理类的繁琐过程。
我们学习 Spring 框架中的AOP,主要基于两种⽅式:JDK 及 CGLIB 的⽅式。这两种⽅式的代
理⽬标都是被代理类中的⽅法,在运⾏期,动态的织⼊字节码⽣成代理类。
JDK 及 CGLIB 的⽅式的异同点
相同点:
- 它们都使用了反射机制来实现动态代理。
-
都可以在运行时动态生成代理对象,在代理对象中调用委托类的方法。
区别:
- JDK 是官方提供的,CGLIB 是第三方提供的。
- CGLIB 比 JDK 更高效。
- JDK 实现,要求被代理类必须实现接⼝,之后是通过 InvocationHandler 及 Proxy,在运⾏
时动态的在内存中⽣成了代理类对象,该代理对象是通过实现同样的接⼝实现(类似静态代
理接⼝实现的⽅式),只是该代理类是在运⾏期时,动态的织⼊统⼀的业务逻辑字节码来完
成。CGLIB 实现,被代理类可以不实现接⼝,是通过继承被代理类,在运⾏时动态的⽣成代理类对象所以相对而言更加的灵活。 - CGLIB 是通过实现继承代理对象来实现动态代理的。如果代理的对象是最终类(被 final 修饰的类),Spring AOP 才会去调用 JDK 的方式生成 动态代理。