目录
1.AOP 组成
1.1 切面(Aspect)
1.2 切点(Pointcut)
1.3 通知(Advice)
1.4 连接点(JoinPoit)
2.Spring AOP 实现步骤
2.1 添加 Spring AOP 依赖
2.2 定义切面和切面
2.3 执行通知
2.3.1 前置通知
2.3.2 前置+后置通知
2.3.3 环绕通知
3.Spring AOP 实现原理——动态代理
3.1 JDK 动态代理
3.2 CGLIB 动态代理
3.3 JDK Proxy VS CGLIB
AOP(Aspect Oriented Programming):面向切面编程,它是⼀种思想,它是对某⼀类事情的集中处理;
AOP 是⼀种思想,而 Spring AOP 是⼀个框架,提供了⼀种对 AOP 思想的实现,它们的关系和 IoC 与 DI 类似
AOP 实现功能:
也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object Oriented Programming,⾯向对象编程)的补充和完善。
定义的是事件(AOP 是做啥的)
切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义;切面是包含了:通知、切点和切面的类,相当于 AOP 实现的某个功能的集合
定义具体规则
Pointcut 是匹配 Join Point 的谓词;Pointcut 的作用就是提供⼀组规则(使⽤ AspectJ pointcut expression language 来描述)来匹配 Join Point,给满足规则的 Join Point 添加 Advice
AOP 执行的具体方法
通知:定义了切面是什么,何时使用,其描述了切面要完成的工作,还解决何时执行这个工作的问题。
Spring 切面类中,可以在⽅法上使⽤以下注解,会设置⽅法为通知方法,在满足条件后会通知本方法进行调用:
有可能触发切点的所有点
应用执行过程中能够插⼊切面的⼀个点,这个点可以是方法调用时,抛出异常时,甚⾄修改字段
例如:切面相当于一个用户登录校验(也相当于老板定义公司方向);切点相当于定义用户登录拦截规则,哪些接口判断用户登录权限?哪些不判断(也相当于中层指定具体的方案);通知相当于获取用户登录信息,如果获取到说明已经登陆,否则未登录(也相当于底层具体业务执行者);连接点相当于所有接口(也相当于招生)
将切面应用到目标对象中的过程,可以在编译时、加载时或运行时进行。
在 pom.xml 添加依赖:
org.springframework.boot
spring-boot-starter-aop
package com.example.demo.common;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect //定义切面
@Component
public class UserAspect {
//定义切点
@Pointcut("Execution(* com.example.demo.controller.UserController.*(..))") //拦截规则
public void pointcut() { }
}
拦截代码:
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("/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";
}
}
其中:pointcut 方法为空方法,它不需要有方法体,此方法名就是起到⼀个“标识”的作⽤,标识下⾯的通知方法具体指的是哪个切点(因为切点可能有很多个
切点表达式(拦截规则):
package com.example.demo.common;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect //定义切面
@Component
public class UserAspect {
//定义切点
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))") //拦截规则
public void pointcut() { }
//前置通知
@Before("pointcut()")
public void doBefore() {
System.out.println("执行了前置通知");
}
}
作为对比,创建一个 ArticleController 类,不做任何拦截:
@RestController
@RequestMapping("/art")
public class ArticleController {
@RequestMapping("/getart")
public String getArticle() {
System.out.println("do getArticle");
return "getArticle";
}
}
运行启动类,访问 localhost:8080/art/getart
只是执行了方法本身,通知没有执行
访问 localhost:8080/user/getuser
package com.example.demo.common;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect //定义切面
@Component
public class UserAspect {
//定义切点
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))") //拦截规则
public void pointcut() { }
//前置通知
@Before("pointcut()")
public void doBefore() {
System.out.println("执行了前置通知");
}
//后置通知
@After("pointcut()")
public void doAfter() {
System.out.println("执行了后置通知");
}
}
访问 localhost:8080/user/getuser
环绕通知需要传一个固定参数:ProceedingJoinPoint ,并且返回值是 Object
@Aspect //定义切面
@Component
public class UserAspect {
//定义切点
@Pointcut("execution(* com.example.demo.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;
}
}
访问 localhost:8080/user/getuser
环绕通知:把整个执行过程放在一个方法中,进行原子性操作
例如统计目标执行的时间,前置和后置通知很难去写,单个统计是ok的,如果是多线程,前置方法和后置方法是非原子性的会出现混乱;但是环绕通知是在一个方法中,通过加锁进行目标执行的时间统计
Spring AOP 是构建在 动态代理 基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截
AOP 常见实现技术有以下两种:
- 静态代理:静态代理是一种在编译时就已经确定代理关系的代理方式。在静态代理中,代理类和被代理类都要实现同一个接口或继承同一个父类,代理类中包含了被代理类的实例,并在调用被代理类的方法前后执行相应的操作。静态代理的优点是实现简单,易于理解和掌握,但是它的缺点是需要为每个被代理类编写一个代理类,当被代理类的数量增多时,代码量会变得很大。
- 动态代理:动态代理是一种在运行时动态生成代理类的代理方式。在动态代理中,代理类不需要实现同一个接口或继承同一个父类,而是通过 Java 反射机制动态生成代理类,并在调用被代理类的方法前后执行相应的操作。动态代理的优点是可以为多个被代理类生成同一个代理类,从而减少了代码量,但是它的缺点是实现相对复杂,需要了解 Java 反射机制和动态生成字节码的技术。
例如:假设一个宿舍有三个人(张三李四王五)同一时间点了同一家外卖,配送到达之后;在没有动态代理,那就是需要自己去取(三个人各自取各自的,需要每人去跑一趟),如果有动态代理,这个时候就可以让赵六(代理)帮忙去取三个人的外卖只需要跑一趟。
在统一的动态代理中,写一个用户拦截方法(例如添加文章、修改方法和删除方法中每个方法都需要写一个用户判断操作),如果有动态代理只需要写一个操作即可
调用者首先来到代理对象中,代理对象会经过一个判定,去代理目标对象实现 AOP
动态代理 使用 JDK Proxy 和 CGLIB 实现
实现了接口的类,使用 AOP 会基于 JDK 生成代理类;没有实现接口的类,会基于 CGLIB 生成代理类(通过实现代理类的子类实现动态代理,被 final 修饰的类是不能被代理)
JDK 动态代理是一种使用 Java 标准库中的 java.lang.reflect.Proxy 类来实现动态代理的技术。在 JDK 动态代理中,被代理类必须实现一个或多个接口,并通过 InvocationHandler 接口来实现代理类的具体逻辑。
具体来说,当使用 JDK 动态代理时,需要定义一个实现 InvocationHandler 接口的类,并在该类中实现代理类的具体逻辑。然后,通过 Proxy.newProxyInstance() 方法来创建代理类的实例。该方法接受三个参数:类加载器、代理类要实现的接口列表和 InvocationHandler 对象,如下代码所示:
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();
}
}
JDK 动态代理的优点是实现简单,易于理解和掌握,但是它的缺点是只能代理实现了接口的类,无法代理没有实现接口的类。
GLIB 动态代理是一种使用 CGLIB 库来实现动态代理的技术。在 CGLIB 动态代理中,代理类不需要实现接口,而是通过继承被代理类来实现代理。 具体来说,当使用 CGLIB 动态代理时,需要定义一个继承被代理类的子类,并在该子类中实现代理类的具体逻辑。然后,通过 Enhancer.create() 方法来创建代理类的实例。该方法接受一个类作为参数,表示要代理的类,如下代码所示:
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();
}
}
CGLIB 动态代理的优点是可以代理没有实现接口的类,但是它的缺点是实现相对复杂,需要了解 CGLIB 库的使用方法。