目录
[2.5切⾯优先级 @Order](#2.5切⾯优先级 @Order)
[2.6.1 @execution表达式](#2.6.1 @execution表达式)
一.SpringAOP是什么?
1.1理论知识点
在学习SpringAOP前,我们需要了解一下什么是AOP?
AOP(Aspect Oriented Programming):⾯向切⾯编程,通过预编译和运行期间动态代理来实现程序功能的统一维护的一种技术。 它是⼀种思想,它是对某⼀类事情的集中处理。
⽐如⽤户登录权限的效验,没学 AOP 之前,我们所有需要判断⽤户登录的⻚⾯(中
的⽅法),都要各⾃实现或调⽤⽤户验证的⽅法,然⽽有了 AOP 之后,我们只需要在某⼀处配置⼀下,所有需要判断⽤户登录⻚⾯(中的⽅法)就全部可以实现⽤户登录验证了,不再需要每个⽅法中都写相同的⽤户登录验证了。
AOP中的基本单元是 Aspect(切面)
1.2简单的AOP例子
理论永远没有代码直观!
引入依赖:
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
定义切面:
java
@Aspect // 定义切面
@Component
public class UserAspect {
// 切点
@Pointcut("execution(* com.example.interview.Controller.UserController.*(..))")
public void pointcut() {
}
// 前置通知通知
@Before("pointcut()")
public void doBefore() {
System.out.println("执行了前置通知");
}
// 后置通知
@After("pointcut()")
public void doAfter() {
System.out.println("执行了后置通知");
}
// 环绕通知
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知执行之前");
// 执行目标方法
Object result = joinPoint.proceed();
System.out.println("环绕通知执行之后");
return result;
}
}
设计的Controller类:
java
package com.example.interview.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getuser")
public String getUser(){
System.out.println("do getUser");
return "get user";
}
@RequestMapping("/deluser")
public String delUser(){
System.out.println("do delUser");
return "del user";
}
}
执行结果:
二.SpringAOP的核心概念
我们接下来分析一下切面代码:
从上面的代码中,我们可以得到哪些要素呢?
- @Aspect:切面类,告诉Spring我这个类是个切面,里面有特殊处理方法
- @Pointcut:切点,告诉Spring我要针对什么
- @Before、@Around、@AfterReturning、@After、@AfterThrowing:通知,告诉Spring针对后要做什么处理
2.1切点(Pointcut)
切点(Pointcut), 也称之为"切⼊点"
Pointcut 的作⽤就是提供⼀组规则 (使⽤ AspectJ pointcut expression language 来描述), 告诉程序对 哪些⽅法来进⾏功能增强.也称:公共切点表达式!
如果我们不使用@Pointcut注释,将会让代码冗余大量的切点表达式!
不使用情况下:
java
@Aspect // 定义切面
@Component
public class UserAspect {
// 前置通知通知
@Before("execution(* com.example.interview.Controller.UserController.*(..))")
public void doBefore() {
System.out.println("执行了前置通知");
}
// 后置通知
@After("execution(* com.example.interview.Controller.UserController.*(..))")
public void doAfter() {
System.out.println("执行了后置通知");
}
// 环绕通知
@Around("execution(* com.example.interview.Controller.UserController.*(..))")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知执行之前");
// 执行目标方法
Object result = joinPoint.proceed();
System.out.println("环绕通知执行之后");
return result;
}
}
我们会发现存在⼤量重复的切点表达 execution(*com.example.interview.Controller.UserController.*(..))")
execution, 也可以说是连接点,就是告诉Spring,该路径下需要控制的方法,*代表的是所有方法,(..)代表任意参数。
注: 当切点定义使⽤private修饰时, 仅能在当前切⾯类中使⽤, 当其他切⾯类也要使⽤当前切点定义时, 就需要把private改为public. 引⽤⽅式为: 全限定类名.⽅法名()
例如:
java
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
//前置通知
@Before("com.example.demo.aspect.AspectDemo.pt()")
public void doBefore() {
log.info("执⾏ AspectDemo2 -> Before ⽅法");
}
}
2.2通知(Advice)
- 通知包括前置通知、后置通知和环绕通知。
- 前置通知在
doBefore()
方法中定义,使用了@Before
注解,在切点方法执行之前被调用。 - 后置通知在
doAfter()
方法中定义,使用了@After
注解,在切点方法执行之后被调用。 - 环绕通知在
doAround()
方法中定义,使用了@Around
注解,在切点方法执行前后都可以进行一些额外的处理。环绕通知方法的参数类型为ProceedingJoinPoint
,可以通过调用proceed()
方法执行目标方法,并在执行前后进行其他操作。
- 前置通知在
例如:
2.3切⾯(Aspect)
注: 切⾯(Aspect) = 切点(Pointcut) + 通知(Advice)
就是整个代码全是切面的知识点
2.4通知类型
Spring中AOP的通知类型有以下⼏种:
- @Around: 环绕通知, 此注解标注的通知⽅法在⽬标⽅法前, 后都被执⾏
- @Before: 前置通知, 此注解标注的通知⽅法在⽬标⽅法前被执⾏
- @After: 后置通知, 此注解标注的通知⽅法在⽬标⽅法后被执⾏, ⽆论是否有异常都会执⾏
- @AfterReturning: 返回后通知, 此注解标注的通知⽅法在⽬标⽅法后被执⾏, 有异常不会执⾏
- @AfterThrowing: 异常后通知, 此注解标注的通知⽅法发⽣异常后执⾏
前面五种,我们都可以通过之前代码看出,但是第五种是异常通知,程序正常运⾏的情况下, @AfterThrowing 标识的通知⽅法不会执⾏。但是如果发生异常了呢?什么会执行,上面不会执行呢?
- @AfterReturning 标识的通知⽅法不会执⾏, @AfterThrowing 标识的通知⽅法执⾏了
- @Around 环绕通知中原始⽅法调⽤时有异常,通知中的环绕后的代码逻辑也不会在执⾏了(因为原始⽅法调⽤出异常了)
2.5切⾯优先级 @Order
当我们在⼀个项⽬中, 定义了多个切⾯类时, 并且这些切⾯类的多个切⼊点都匹配到了同⼀个⽬标⽅法. 当⽬标⽅法运⾏的时候, 这些切⾯类中的通知⽅法都会执⾏, 那么这⼏个通知⽅法的执⾏顺序是什么样的呢?
切面定义三个,分别为AspectDemo2、AspectDemo3、AspectDemo4,为了简易化,只写@Before和@After,而这里只展示一个代码,其他在修改一下类名即可:
java
@Aspect // 定义切面
@Component
public class AspectDemo2 {
@Pointcut("execution(* com.example.interview.Controller.UserController.*(..))")
private void pt(){}
// 前置通知通知
@Before("pt()")
public void doBefore() {
System.out.println("执行了前置通知2");
}
// 后置通知
@After("pt()")
public void doAfter() {
System.out.println("执行了后置通知2");
}
}
UserControer代码:
java
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getuser")
public String getUser(){
System.out.println("do getUser");
return "get user";
}
@RequestMapping("/deluser")
public String delUser(){
System.out.println("do delUser");
return "del user";
}
}
访问对应的接口程序:http://localhost:8080/user/getuser
运行结果如下图:
通过对比我们可以发现:存在多个切⾯类时, 默认按照切⾯类的类名字⺟排序:
- @Before 通知:字⺟排名靠前的先执⾏
- @After 通知:字⺟排名靠前的后执⾏
问:如果我们需要指定某个切面先执行呢?
答: Spring 给我们提供了⼀个新的注解, 来控制这些切⾯通知的执⾏顺序: @Order
使用方式如下:
我们在切面类AspectDemo2、AspectDemo3、AspectDemo4上分别加上注解:@Order(3)、@Order(2)、@Order(1).
例如:
java
@Aspect // 定义切面
@Component
@Order(1)
public class AspectDemo4 {
//代码照旧
}
访问对应的接口程序:http://localhost:8080/user/getuser
运行结果:
通过上述程序的运⾏结果, 得出结论:
@Order 注解标识的切⾯类, 执⾏顺序如下:
- @Before 通知:数字越⼩先执⾏
- @After 通知:数字越⼤先执⾏
@Order 控制切⾯的优先级, 先执⾏优先级较⾼的切⾯, 再执⾏优先级较低的切⾯, 最终执⾏⽬标⽅法.
2.6切点表达式
切点表达式常⻅有两种表达⽅式
- execution(......):根据⽅法的签名来匹配
- @annotation(......) :根据注解匹配
2.6.1 @execution表达式
execution() 是最常⽤的切点表达式, ⽤来匹配⽅法, 语法为:
java
execution(<访问修饰符> <返回类型> <包名.类名.⽅法(⽅法参数)> <异常>)
切点表达式⽀持通配符表达:
- * :匹配任意字符,只匹配⼀个元素(返回类型, 包, 类名, ⽅法或者⽅法参数)
- 包名使⽤ * 表⽰任意包(⼀层包使⽤⼀个*)
- 类名使⽤ * 表⽰任意类
- 返回值使⽤ * 表⽰任意返回值类型
- ⽅法名使⽤ * 表⽰任意⽅法
- 参数使⽤ * 表⽰⼀个任意类型的参数
- ..:匹配多个连续的任意符号, 可以通配任意层级的包, 或任意类型, 任意个数的参数
- 使⽤ .. 配置包名,标识此包以及此包下的所有⼦包
- 可以使⽤ .. 配置参数,任意个任意类型的参数
2.6.2@annotation表达式
execution表达式更适⽤有规则的, 如果我们要匹配多个⽆规则的⽅法呢,
问:如果我们 匹配两个不同类的一个方法,怎么操作呢?
我们可以借助⾃定义注解的⽅式以及另⼀种切点表达式 @annotation 来描述这⼀类的切点
第一步准备测试方法:
java
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@RequestMapping("/t2")
public boolean t2() {
return true;
}
}
java
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/u1")
public String u1(){
return "u1";
}
@RequestMapping("/u2")
public String u2(){
return "u2";
}
}
第二步自定义注解@MyAspect
代码内容:
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}
注解解释:
一.@Target 标识了 Annotation 所修饰的对象范围, 即该注解可以⽤在什么地⽅. 常⽤取值:
- ElementType.TYPE: ⽤于描述类、接⼝(包括注解类型) 或enum声明
- ElementType.METHOD: 描述⽅法
- ElementType.PARAMETER: 描述参数
- ElementType.TYPE_USE: 可以标注任意类型
二. @Retention 指Annotation被保留的时间⻓短, 标明注解的⽣命周期,@Retention 的取值有三种:
- RetentionPolicy.SOURCE:表⽰注解仅存在于源代码中, 编译成字节码后会被丢弃. 这意味着在运⾏时⽆法获取到该注解的信息, 只能在编译时使⽤. ⽐如 @SuppressWarnings , 以及 lombok提供的注解 @Data , @Slf4j
- RetentionPolicy.CLASS:编译时注解. 表⽰注解存在于源代码和字节码中, 但在运⾏时会被丢弃. 这意味着在编译时和字节码中可以通过反射获取到该注解的信息, 但在实际运⾏时⽆法获 取. 通常⽤于⼀些框架和⼯具的注解.
- RetentionPolicy.RUNTIME:运⾏时注解. 表⽰注解存在于源代码, 字节码和运⾏时中. 这意味着在编译时, 字节码中和实际运⾏时都可以通过反射获取到该注解的信息. 通常⽤于⼀些需要 在运⾏时处理的注解, 如Spring的 @Controller @ResponseBody
第三步:切面类定义,将 @execution修改为@annotation ,但是目标源为自定义的注解@MyAspect
java
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
//前置通知
@Before("@annotation(com.example.demo.aspect.MyAspect)")
public void before(){
log.info("MyAspect -> before ...");
}
//后置通知
@After("@annotation(com.example.demo.aspect.MyAspect)")
public void after(){
log.info("MyAspect -> after ...");
}
}
第四步:在测试方法当中添加自定义的注解--@MyAspect
java
@MyAspect
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@MyAspect
@RequestMapping("/u1")
public String u1(){
return "u1";
}
第五步,访问
http://127.0.0.1:8080/test/t1, 切⾯通知执⾏
http://127.0.0.1:8080/user/u1 , 切⾯通知执⾏.
未添加注解:
http://127.0.0.1:8080/test/t2, 切⾯未通知执⾏
http://127.0.0.1:8080/user/u2 , 切⾯未通知执⾏.
总结
Spring AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架中的一个模块,用于实现横切关注点的模块化开发。代理是 Spring AOP 实现的一种方式。
在 Spring AOP 中,代理是实现切面的一种方式之一。通过代理,Spring AOP 可以在目标对象的方法执行前、执行后或抛出异常时,执行额外的逻辑(如日志记录、性能监控、事务管理等)。Spring AOP 使用代理机制来实现横切关注点的织入。
Spring AOP 实现代理的方式有两种:
-
基于 JDK 动态代理: 如果目标对象实现了至少一个接口,Spring AOP 就会使用 JDK 动态代理来为目标对象创建代理。在运行时,Spring AOP 会动态生成一个实现了目标对象所有接口的代理对象,并在代理对象的方法中织入切面逻辑。
-
基于 CGLIB 代理: 如果目标对象没有实现任何接口,Spring AOP 就会使用 CGLIB(Code Generation Library)来为目标对象创建代理。CGLIB 使用字节码生成技术,在运行时生成目标对象的子类,并重写其中的方法来织入切面逻辑。