目录
一、初识AOP
1.1 什么是AOP?
AOP(Aspect Oriented Programming):面向切面编程,它是⼀种思想,它是对某⼀类事情的
集中处理。在我们想要对某一件事情进行集中处理,就可以使用到AOP,它提供一种将程序中的横切关注点模块化的方式。在 AOP 中,我们将这些横切关注点称为“切面”,它们独立于业务逻辑模块,但是可以在程序运行的不同阶段被织入到业务逻辑中。
简单来说,AOP 就是对某一件事进行集中处理的思想方式~
1.2 AOP的组成
1.2.1 切面(Aspect)
切⾯(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包
括了连接点的定义。相当于处理某方面具体问题的一个类,包含多个方法,而这些方法就是切点和通知。
1.2.2 切点(Pointcut)
Pointcut 的作⽤就是提供⼀组规则来匹配连接点(Join Point),给满足规则的连接点添加通知(Advice),可以理解为用来进行主动拦截的规则(配置)
1.2.3 连接点(Join Point)
应⽤执⾏过程中能够插⼊切⾯的⼀个点,连接点可以理解为可能会触发AOP规则的所有点。(所有请求)
1.2.4 通知(Advice)
在AOP术语中,切面的工作被称之为通知。通知是切面在连接点上执行的动作。它定义了在何时(例如在方法调用之前或之后)以及如何(例如打印日志或进行性能监控)应用切面的行为。即,程序中被拦截请求触发的具体动作。
Spring 切⾯类中,可以在方法上使⽤以下注解,会设置⽅法为通知方法,在满⾜条件后会通知本
⽅法进⾏调⽤:
- 前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执行。
- 后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。
- 返回之后通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。
- 抛异常后通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。
- 环绕通知使⽤ @Around:通知包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后执行⾃定义的行为。
1.3 AOP的使用场景
在做任何一个系统都需要登录功能,那么几乎想要使用这个系统都需要我们进行验证用户登录状态,我们之前的处理⽅式是每个 Controller 都要写⼀遍⽤户登录验证,然⽽当你的功能越来越多,那么你要写的登录验证也越来越多,⽽这些⽅法⼜是相同的,这么多的⽅法就会代码修改和维护的成本。对于这种功能统⼀,且使⽤的地⽅较多的功能,就可以考虑 AOP来统⼀处理了。
除了统一登录判断外,使用AOP还可以实现:
- 用户登录验证
- 统⼀⽇志记录
- 统⼀⽅法执⾏时间统计
- 统⼀的返回格式设置
- 统⼀的异常处理
- 事务的开启和提交等
二、Srping AOP 实现
Spring AOP 的实现步骤如下:
- 添加 Spring AOP 框架⽀持
- 定义切⾯和切点:(1)创建切面类(2)配置拦截规则
- 定义通知
2.1 添加Spring AOP 依赖
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-bo
ot-starter-aop -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 定义切面和切点
使用 @Aspect
注解表明当前类为一个切面,而在切点中,我们要定义拦截的规则,具体实现如下:
@Component // 随着框架的启动而启动
@Aspect // 告诉框架我是一个切面类
public class UserAspect {
// 定义切点(配置拦截规则)
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){
}
}
在上述实现代码中,pointcut 为一个空方法,只是起到一个“标识”的作用,标识下面的通知方法具体指的是哪个切点,切点可以有多个。
切点表达式由切点函数组成,其中 execution()
是最常⽤的切点函数,⽤来匹配⽅法,语法为:
execution(<修饰符><返回类型><包.类.⽅法(参数)><异常>)
修饰符和异常可以省略
常见的切点表达式的示例:
- 匹配特定类的所有方法:
- execution(* com.example.MyClass.*(..)):匹配 com.example.MyClass 类中的所有方法。
- 匹配特定包下的所有方法:
- execution(* com.example.*.*(..)):匹配 com.example 包及其子包下的所有方法。
- 匹配特定方法名的方法:
- execution(* com.example.MyClass.myMethod(..)):匹配 com.example.MyClass 类中名为 myMethod 的方法。
- 匹配特定方法参数类型的方法:
- execution(* com.example.MyClass.myMethod(String, int)):匹配 com.example.MyClass 类中具有一个 String 参数和一个 int 参数的 myMethod 方法。
- 匹配特定返回类型的方法:
- execution(String com.example.MyClass.myMethod(..)):匹配 com.example.MyClass 类中返回类型为 String 的 myMethod 方法。
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/hi")
public String sayHi(String name){
System.out.println("执行了Hi");
return "Hi," + name;
}
@RequestMapping("/hello")
public String sayHello(){
System.out.println("执行了Hello");
return "Hello,world";
}
}
2.3 定义通知
通知定义的是被拦截方法具体要执行的业务。我们上面列出了可以使用哪些通知~这里举出例子
package com.example.demo.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component // 随着框架的启动而启动
@Aspect // 告诉框架我是一个切面类
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("执行了后置通知~");
}
/**
* 环绕通知
* @param joinPoint
* @return
*/
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint){
System.out.println("进入了环绕通知~");
Object obj = null;
try {
// 执⾏拦截⽅法
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("退出了环绕通知~");
return obj;
}
}
环绕通知是在前置通知之前和后置通知之后运行的~
三、Spring AOP 实现原理
Spring AOP 是通过动态代理的⽅式,在运⾏期将 AOP 代码织⼊到程序中的,它的实现⽅式有两种:JDK Proxy
和 CGLIB
。因此,Spring 对 AOP 的支持局限于方法级别的拦截。
- 默认情况下,实现了接⼝的类,使⽤ AOP 会基于 JDK ⽣成代理类
- 没有实现接⼝的类,会基于 CGLIB ⽣成代理类
3.1 什么是动态代理?
动态代理(Dynamic Proxy)是一种设计模式,它允许 在运行时创建代理对象,并将方法调用转发给实际的对象。 动态代理可以用于实现横切关注点(如日志记录、性能监控、事务管理等)的功能,而无需修改原始对象的代码。
在Java中,动态代理通常使用 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口来实现。
调用者在调用方法时,会先转发给代理类创建的代理对象,随后再由代理对象转发给目标对象。
以下是使用动态代理的一般步骤:
- 创建一个实现InvocationHandler接口的类,该类将作为代理对象的调用处理程序。在InvocationHandler接口的invoke方法中,可以定义在方法调用前后执行的逻辑。
- 使用Proxy类的newProxyInstance方法创建代理对象。该方法接受三个参数:类加载器、代理接口数组和调用处理程序。它将返回一个实现指定接口的代理对象。
- 使用代理对象调用方法。当调用代理对象的方法时,实际上会调用调用处理程序的invoke方法,并将方法调用转发给实际的对象。
3.2 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代理对象
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//1.安全检查
System.out.println("安全检查");
//2.记录⽇志
System.out.println("记录⽇志");
//3.时间统计开始
System.out.println("记录开始时间");
//通过反射调⽤被代理类的⽅法
Object retVal = method.invoke(target, args);
//4.时间统计结束
System.out.println("记录结束时间");
return retVal;
}
public static void main(String[] args) {
PayService target= new AliPayService();
//⽅法调⽤处理器
InvocationHandler handler =
new PayServiceJDKInvocationHandler(target);
//创建⼀个代理类:通过被代理类、被代理实现的接⼝、⽅法调⽤处理器来创建
PayService proxy = (PayService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{PayService.class},
handler
);
proxy.pay();
}
}
3.3 CGLIB 动态代理实现
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, Method
Proxy 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(),n
ew PayServiceCGLIBInterceptor(target));
proxy.pay();
}
}
3.4 JDK 和 CGLIB 实现的区别
- JDK 实现,要求被代理类必须实现接口, 之后是通过 InvocationHandler 及 Proxy,在运⾏时动态的在内存中⽣成了代理类对象,该代理对象是通过实现同样的接⼝实现(类似静态代理接⼝实现的⽅式),只是该代理类是在运⾏期时,动态的织⼊统⼀的业务逻辑字节码来完成。
- CGLIB 实现,被代理类可以不实现接口, 是通过继承被代理类,在运⾏时动态的⽣成代理类对象。