Spring AOP
什么是 Spring AOP
AOP 是(Aspect Oriented Programming),也就是面向切面编程。是⼀种思想,是对某⼀类事情的集中处理。Spring AOP 提供了一种对 AOP 思想的实现。是对 OOP(面向对象思想) 思想的扩充
比如用户登录权限的校验,不使用 AOP 的话,所有需要判断用户登录的页面,都要各自实现或调用用户验证的方法。然而有了 AOP 之后,只需要在某一处配置一下,所有需要验证用户登陆的页面就可以全部实现用户登录验证了,不在需要每个方法都写相同的用户登陆验证了。
出来上面说的这种的用户登录判断外,还可以实现:
- 统一日志记录
- 统一方法执行时间统计
- 统一的返回格式设置
- 统一的异常处理
- 事务的开启和提交等
AOP 的组成
切面
AOP 是面向 切面编程,所以这是 AOP 最重要的功能。定义 AOP 是针对哪个统一的功能,这个功能就叫做一个切面。比如用户登录功能或方法的统计日志,他们就各自是一个切面。切面是由 切点 和 通知组成的。
连接点
就是所有可能触发 AOP(拦截方法的点),就称为连接点。
切点
切点会提供一个规则,用来匹配连接点,并且来实现通知。也就是定义 AOP 拦截的规则的。
通知
切面要完成的工作就是通知。就是规定 AOP 执行的时机和执行的方法。通知注解如下:
- 前置通知: 使用 @Before,通知方法会在目标方法调用之前执行。
- 后置通知: 使用 @After,通知方法会在目标方法返回或者抛出异常后调用。
- 返回之后通知: 使用 @AfterReturning,通知方法会在目标方法返回后调用。
- 抛异常后通知: 使用 @AfterThrowing,通知方法会在目标方法抛出异常后调用。
- 环绕通知: 使用 @Around,通知包裹了被通知的方法,在被通知的方法通知之前和调用之后,执行自定义的行为。
AOP 的整个组成部分概念就像这张图片,以多个页面抖音访问用户登录权限为例:
Spring AOP 实现
- 在项目中添加 Spring AOP 框架。
- 定义切面。
- 定义切点。
- 实现通知。
添加 Spring AOP 框架
在创建项目的时候,一般是没有 AOP 框架的,所以就需要在中央仓库里面找到 AOP 依赖,然后导入就好了:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.7.2</version>
</dependency>
然后刷新 pom 就可以了。
定义切面
先创建一个类,然后加上 @Aspect 注解 就可以了。然后在设置切点。
@Aspect
@Component
public class UserAspect {
}
设置切点
通过 @Pointcut 注解就可以定义切点了。固定写法如下 @Pointcut("execution(* com.example.springaop.controller.UserController.*(..))")
代码意思如下:
这里切点的语法是 AspectJ 表达式语法。
AspectJ 语法
我们设置 切点 的语法就是 AspectJ 语法,也就是 Spring AOP 切点的匹配语法:
修饰符和异常通常都是省略的。public 就是公共方法,* 就是任意方法。
返回值不能省略,void 表示没有返回值,String 表示返回字符串,* 表示任意返回值。
Aspect 语法中的通配符:
- * :表示匹配任意的内容,用在返回值,包名,类名,方法名都可以使用。
- .. :匹配任意字符,可以使用在方法参数上,如果用在类上需要配合 * 一起使用。
- + :表示按照类型匹配指定类的所有类,必须挂在类名后面,如
com.cad.Car+
表示继承该类的所有子类,也包括本身。
包,类,方法如下 :
表达式实例 :
使用通知
前置通知
通过 @Before 来实现前置通知:
@Before("pointcut()")
public void doBefore() {
System.out.println("执行前置通知");
}
前置通知里面针对的切点就是我们之前设置的 pointcut 。设置 controller 类当中的方法:
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/sayhi")
public String sayHi() {
System.out.println("sayHi");
return "hello word";
}
@RequestMapping("/sayhello")
public String sayHello() {
System.out.println("sayHello");
return "word hello";
}
}
访问结果如下:
IDEA 当中的日志如下,就是先执行 前置通知,然后才是方法内容:
然后执行 sayhello:
IDEA 的日志如下:
也是先执行前置通知。
后置通知
就是执行力目标方法之后在执行通知。通过 @After 注解就可以实现。代码如下:
@After("pointcut()")
public void doAfter() {
System.out.println("执行后置通知");
}
然后启动项目访问,日志如下:
返回之后通知
通过 @AfterReturning 注解来实现,就是在 return 之前执行,执行完之后,才会执行 后置通知:
@AfterReturning("pointcut()")
public void doAfterReturning() {
System.out.println("AfterReturning 返回之后通知");
}
访问结果如下:
抛出异常之后通知
通过 @AfterThrowing 注解来实现,就是抛出异常之后才会执行此通知:
@AfterThrowing("pointcut()")
public void doAfterThrowing() {
System.out.println("doAfterThrowing 抛出异常后通知");
}
然后在访问路径的方法当中制造一个异常:
@RequestMapping("/sayhello")
public String sayHello() {
System.out.println("sayHello");
int num = 10/0;
return "word hello";
}
访问结果如下:
因为抛出异常了,所以就没有返回,也就没有返回通知了。
环绕方法通知
通过 @Around 注解来实现,环绕通知可以实现统计方法运行的时间,但是使用的时候 必须要有 ProceedingJoinPoint 参数:
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object result = null;
System.out.println("Around 前置通知");
try {
result = joinPoint.proceed();
} catch (Throwable e) {
throw new RuntimeException(e);
}
System.out.println("Around 后置通知");
return result;
}
访问结果如下:
基于环绕通知统计方法执行时间
通过 Spring 提供的 StopWatch 来统计时间:
@Around("pointcut()")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object result = null;
StopWatch stopWatch = new StopWatch();
System.out.println("Around 前置通知");
try {
stopWatch.start();
result = joinPoint.proceed();
stopWatch.stop();
} catch (Throwable e) {
throw new RuntimeException(e);
}
System.out.println("Around 后置通知");
System.out.println(joinPoint.getSignature().getName() + " 方法时间:" + stopWatch.getTotalTimeMillis() + "ms");
return result;
}
访问结果如下:
Spring AOP 实现原理
Spring AOP 是构建在 动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截。Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理。
不使用 AOP 代理
不使用 AOP 代理的时候,前后端交互如下:
是前端直接调用后端来实现交互的,比如说要有登陆验证,每次都需要和后端进行验证,都需要调用验证的方法。
使用 AOP 代理
使用 AOP 代理的话,就相当于是在前后端之间的中间商插手了:
AOP 会把所有符合拦截规则的方法都给拦截了。如果想要交互的话,就得先通过 AOP 的验证,然后才能与后端交互。到了这里,Spring 当中存储的就是动态代理的对象了。
动态代理的实现
Spring 当中提供了两种 动态代理 的实现方式:
- JDK Proxy(JDK 动态代理)
- CGLIB Proxy。默认情况下,Spring AOP 都会采用 CGLIB 来实现动态代理。是通过 继承代理对象 来实现动态代理的(子类拥有父类的所有功能)。但是不能代理最终类(也就是被 final 修饰的类),此时就需要 JDK Proxy 来实现动态代理了。
代理过程如下:
织入(Weaving):代理的生成时机
织入 ,与 AOP 的4个定义(切面,切点,连接点,通知) 是 并列的关系。织入,就是 AOP 第5个定义:就是把切⾯应⽤到⽬标对象并创建新的代理对象的过程,切⾯在指定的连接点被织⼊到⽬标对象中。说白了:织入,就是描述 动态代理 是在什么时候生成的。
目标对象在生命周期里有多个点可以进行织入,一共三个点 :
- 编译期:切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就是以这种⽅式织⼊切⾯的。
- 类加载器:切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载器(ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。
- 运行期:切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊切⾯的。
Spring AOP 是在 运行期 生成的动态代理。
JDK 动态代理实现(依靠反射)
JDK 实现时,先通过实现 InvocationHandler 接⼝创建⽅法调⽤处理器,再通过 Proxy 来创建代理类:
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
//动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被代理类必须实现接⼝
public class PayServiceJDKInvocationHandler implements InvocationHandler
{
//⽬标对象即就是被代理对象
private Object target;
public PayServiceJDKInvocationHandler( Object target) {
this.target = target;
}
//proxy代理对象,method 执行的目标方法,args 执行方法所需的参数
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过反射调⽤被代理类的⽅法 - 重点
// invoke 就是实例反射的意思,把 目标对象 target 和 响应的参数args,传进去
Object retVal = method.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
// PayService 它是一个接口,但对接的类 需要根据实际情况来决定
// 下面就是 对应着 阿里的支付服务的实体类
PayService target= new AliPayService();
//⽅法调⽤处理器
InvocationHandler handler =
new PayServiceJDKInvocationHandler(target);
//创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
PayService proxy = (PayService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{PayService.class},
handler
);
// 调用 代理类
proxy.pay();
}
}
CGLIB 动态代理实现
实现的方式和 JDK 的一模一样,只有三处不同:
- 实现接口换成了 MethodInterceptor
- 重写方法的传参发生了改变
- 调用的时候,比较简单一些
代码如下:
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.example.demo.service.AliPayService;
import org.example.demo.service.PayService;
import java.lang.reflect.Method;
public class PayServiceCGLIBInterceptor implements MethodInterceptor {
//被代理对象
private Object target;
public PayServiceCGLIBInterceptor(Object target){
this.target = target;
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过cglib的代理⽅法调⽤
Object retVal = methodProxy.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
PayService target= new AliPayService();
PayService proxy= (PayService) Enhancer.create(target.getClass(),
new PayServiceCGLIBInterceptor(target));
proxy.pay();
}
}
JDK 和 CGLIB 的区别
- JDK 是官方提供的,CGLIB 是第三方提供的。
- CGLIB 比 JDK 更高效。
- CGLIB 是通过 实现 继承 代理对象 来实现 动态代理的。(如果代理的对象是 最终类(被 final 修饰的类),Spring AOP 才会去调用 JDK 的方式生成 动态代理。)