AOP 是(Aspect Oriented Programming),也就是面向切面编程。是⼀种思想,是对某⼀类事情的集中处理。Spring AOP 提供了一种对 AOP 思想的实现。是对 OOP(面向对象思想) 思想的扩充
比如用户登录权限的校验,不使用 AOP 的话,所有需要判断用户登录的页面,都要各自实现或调用用户验证的方法。然而有了 AOP 之后,只需要在某一处配置一下,所有需要验证用户登陆的页面就可以全部实现用户登录验证了,不在需要每个方法都写相同的用户登陆验证了。
出来上面说的这种的用户登录判断外,还可以实现:
AOP 是面向 切面编程,所以这是 AOP 最重要的功能。定义 AOP 是针对哪个统一的功能,这个功能就叫做一个切面。比如用户登录功能或方法的统计日志,他们就各自是一个切面。切面是由 切点 和 通知组成的。
就是所有可能触发 AOP(拦截方法的点),就称为连接点。
切点会提供一个规则,用来匹配连接点,并且来实现通知。也就是定义 AOP 拦截的规则的。
切面要完成的工作就是通知。就是规定 AOP 执行的时机和执行的方法。通知注解如下:
AOP 的整个组成部分概念就像这张图片,以多个页面抖音访问用户登录权限为例:
在创建项目的时候,一般是没有 AOP 框架的,所以就需要在中央仓库里面找到 AOP 依赖,然后导入就好了:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
<version>2.7.2version>
dependency>
然后刷新 pom 就可以了。
先创建一个类,然后加上 @Aspect 注解 就可以了。然后在设置切点。
@Aspect
@Component
public class UserAspect {
}
通过 @Pointcut 注解就可以定义切点了。固定写法如下 @Pointcut("execution(* com.example.springaop.controller.UserController.*(..))")
代码意思如下:
这里切点的语法是 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 支持 JDK Proxy 和 CGLIB 方式实现动态代理。
不使用 AOP 代理的时候,前后端交互如下:
是前端直接调用后端来实现交互的,比如说要有登陆验证,每次都需要和后端进行验证,都需要调用验证的方法。
使用 AOP 代理的话,就相当于是在前后端之间的中间商插手了:
AOP 会把所有符合拦截规则的方法都给拦截了。如果想要交互的话,就得先通过 AOP 的验证,然后才能与后端交互。到了这里,Spring 当中存储的就是动态代理的对象了。
Spring 当中提供了两种 动态代理 的实现方式:
织入 ,与 AOP 的4个定义(切面,切点,连接点,通知) 是 并列的关系。织入,就是 AOP 第5个定义:就是把切⾯应⽤到⽬标对象并创建新的代理对象的过程,切⾯在指定的连接点被织⼊到⽬标对象中。说白了:织入,就是描述 动态代理 是在什么时候生成的。
目标对象在生命周期里有多个点可以进行织入,一共三个点 :
Spring AOP 是在 运行期 生成的动态代理。
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();
}
}
实现的方式和 JDK 的一模一样,只有三处不同:
代码如下:
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();
}
}