1. 什么是 Spring AOP
在介绍 Spring AOP 之前,首先要了解一下什么是 AOP?
AOP(Aspect Oriented Programming):面向切面编程,它是一种思想,它是对某一类事情的集中处理。比如用户登录权限的效验,没学 AOP 之前,我们所有需要判断用户登录的页面(中的方法),都要各自实现或调用用户验证的方法,然而有了 AOP 之后,我们只需要在某一处配置一下,所有需要判断用户登录页面(中的方法)就全部可以实现用户登录验证了,不再需要每个方法中都写相同的用户登录验证了。
而 AOP 是一种思想,而 Spring AOP 是一个框架,提供了一种对 AOP 思想的实现,它们的关系和 IoC 与 DI 类似。
2. 为什么要用 AOP
想象一个场景,我们在做后台系统时,除了登录和注册等几个功能不需要做用户登录验证之外,其他几 乎所有页面调用的前端控制器( Controller)都需要先验证用户登录的状态,那这个时候我们要怎么处 理呢?
我们之前的处理方式是每个 Controller 都要写一遍用户登录验证,然而当你的功能越来越多,那么你要 写的登录验证也越来越多,而这些方法又是相同的,这么多的方法就会代码修改和维护的成本。那有没 有简单的处理方案呢?答案是有的,对于这种功能统一,且使用的地方较多的功能,就可以考虑 AOP 来统一处理了。
除了统一的用户登录判断之外,AOP 还可以实现:
统一日志记录
统一方法执行时间统计
统一的返回格式设置
统一的异常处理
事务的开启和提交等
也就是说使用 AOP 可以扩充多个对象的某个能力,所以 AOP 可以说是 OOP(Object Oriented Programming,面向对象编程)的补充和完善。
3. AOP 组成
3.1 切面(Aspect)
切面(Aspect)由切点(Pointcut)和通知(Advice)组成,它既包含了横切逻辑的定义,也包括了连接点的定义。
切面是包含了:通知、切点和切面的类,相当于 AOP 实现的某个功能的集合。
3.2 连接点(Join Point)
应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
连接点相当于需要被增强的某个 AOP 功能的所有方法。
3.3 切点(Pointcut)
Pointcut 是匹配 Join Point 的谓词。
Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述)来匹配 Join Point,给满足规则的 Join Point 添加 Advice。
切点相当于保存了众多连接点的一个集合(如果把切点看成一个表,而连接点就是表中一条一条的数据)。
3.4 通知(Advice)
切面也是有目标的 ——它必须完成的工作。在 AOP 术语中,切面的工作被称之为通知。
通知:定义了切面是什么,何时使用,其描述了切面要完成的工作,还解决何时执行这个工作的问题。 Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
前置通知使用 @Before:通知方法会在目标方法调用之前执行。
后置通知使用 @After:通知方法会在目标方法返回或者抛出异常后调用。
返回之后通知使用 @AfterReturning:通知方法会在目标方法返回后调用。
抛异常后通知使用 @AfterThrowing:通知方法会在目标方法抛出异常后调用。
环绕通知使用 @Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。
AOP 整个组成部分的概念如下图所示,以多个页面都要访问用户登录权限为例:
4. Spring AOP 实现
4.1 添加 AOP 框架支持
在 pom.xml 中添加如下配置
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
4.2 定义切面和切点
切点指的是具体要处理的某一类问题,比如用户登录权限验证就是一个具体的问题,记录所有方法的执行日志就是一个具体的问题,切点定义的是某一类问题。
Spring AOP 切点的定义如下,在切点中我们要定义拦截的规则,具体实现如下:
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect // 表明此类为一个切面 @Component public class UserAspect { // 定义切点,这里使用 AspectJ 表达式语法 @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") public void pointcut(){ } }
其中 pointcut 方法为空方法,它不需要有方法体,此方法名就是起到一个“标识”的作用,标识下面的通知方法具体指的是哪个切点(因为切点可能有很多个)。
4.2.1 切点表达式说明
AspectJ 支持三种通配符:
* :匹配任意字符,只匹配一个元素(包,类,或方法,方法参数)。
*… :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用。
+ :表示按照类型匹配指定类的所有类,必须跟在类名后面,如 com.cad.Car+ ,表示继承该类的所有子类包括本身。
切点表达式由切点函数组成,其中 execution() 是最常用的切点函数,用来匹配方法,语法为:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
修饰符和异常可以省略
4.2.2 表达式示例
execution(* com.cad.demo.User.*(…)):匹配 User 类里的所有方法
execution(* com.cad.demo.User+.*(…)):匹配 User 类子类包括该类的所有方法
execution(* com.cad.*.*(…)):匹配 com.cad 包下的所有类的所有方法
execution(* com.cad…*.*(…)):匹配 com.cad 包下、子孙包的所有类的所有方法
execution(*addUser(String, int)):匹配 addUser 方法,且第一个参数类型是 String,第二个参数类型是 int
4.3 定义相关通知
通知定义的是被拦截的方法具体要执行的业务,比如用户登录权限验证方法就是具体要执行的业务。 Spring AOP 中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
前置通知使用 @Before:通知方法会在目标方法调用之前执行。
后置通知使用 @After:通知方法会在目标方法返回或者抛出异常后调用。
返回之后通知使用 @AfterReturning:通知方法会在目标方法返回后调用。
抛异常后通知使用 @AfterThrowing:通知方法会在目标方法抛出异常后调用。
环绕通知使用 @Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。
具体实现如下:
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; 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("执行 Before 方法"); } // 后置通知 @After("pointcut()") public void doAfter(){ System.out.println("执行 After 方法"); } // return 之前通知 @AfterReturning("pointcut()") public void doAfterReturning(){ System.out.println("执行 AfterReturning 方法"); } // 抛出异常之前通知 @AfterThrowing("pointcut()") public void doAfterThrowing(){ System.out.println("执行 doAfterThrowing 方法"); } // 添加环绕通知 @Around("pointcut()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { try { // 执行拦截方法 System.out.println("beforeMethodInvoked"); Object obj = joinPoint.proceed(); // 这里是真正调用方法的位置 System.out.println("afterReturning"); return obj; } catch (Throwable throwable) { System.out.println("afterThrowing"); throw throwable; } finally { System.out.println("afterMethodInvoked"); } } }
5. Spring AOP 实现原理
Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的支持局限于方法级别的拦截。
Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理。默认情况下,实现了接口的类,使用 AOP 会 基于 JDK 生成代理类,没有实现接口的类,会基于 CGLIB 生成代理类。
5.1 织入(Weaving):代理的生成时机
织入是把切面应用到目标对象并创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中。 在目标对象的生命周期里有多个点可以进行织入:
**编译期:**切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
**类加载期:**切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。
**运行期:**切面在应用运行的某一时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。Spring AOP 就是以这种方式织入切面的。
5.2 动态代理
此种实现在设计模式上称为动态代理模式,在实现的技术手段上,都是在 class 代码运行期,动态的织入字节码。
我们学习 Spring 框架中的 AOP,主要基于两种方式:JDK 及 CGLIB 的方式。这两种方式的代理目标都 是被代理类中的方法,在运行期,动态的织入字节码生成代理类。
CGLIB 是 Java 中的动态代理框架,主要作用就是根据目标类和方法,动态生成代理类。
Java 中的动态代理框架,几乎都是依赖字节码框架(如 ASM,Javassist 等)实现的。
字节码框架是直接操作 class 字节码的框架。可以加载已有的class字节码文件信息,修改部分信 息,或动态生成一个 class。
5.2.1 JDK 动态代理实现
JDK 实现时,先通过实现 InvocationHandler 接口创建方法调用处理器,再通过 Proxy 来创建代理类。 以下为代码实现:
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; // 这个对象是专门代理 Executable 对象的 // Invocation: 调用 // Handler: 句柄、把手 public class ExecutableProxy implements InvocationHandler { // 被代理的对象 private final Executable executable; public ExecutableProxy(Executable executable) { this.executable = executable; } @Override public Object invoke(Object proxy, Method method, Object[] providedArgs) throws Throwable { // 凡是你代理对象的方法调用,都会执行我们的 invoke 方法 // invoke 是 invocation 的动词形式 // proxy: 代理 // method: 反射中的 Method 类型,代表一个方法对象。外部调用的是哪个方法 // args: 外部调用该方法时的参数 // 在方法结束前,对参数进行修改 if (providedArgs == null) { return method.invoke(executable, providedArgs); } else { String[] args = new String[providedArgs.length]; for (int i = 0; i < providedArgs.length; i++) { Object a = providedArgs[i]; args[i] = "@@" + a + "@@"; } Object returnValue = method.invoke(executable, args);// JVM 内部 if (returnValue instanceof Integer) { int i = (int) returnValue; i += 1000; return i; } return returnValue; } } public static void main(String[] args) { SayHelloCommand command = new SayHelloCommand(); ExecutableProxy proxy = new ExecutableProxy(command); // 把 Executable、ExecutableProxy、SayHelloCommand 关联成一体 Executable executable = (Executable) Proxy.newProxyInstance( Executable.class.getClassLoader(), new Class[] { Executable.class }, proxy ); System.out.println(executable.getClass()); // 执行接口下的方法 executable.execute(); } }
5.2.2 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, 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(); } }
5.2.3 JDK 和 CGLIB 的区别
JDK 实现,要求被代理类必须实现接口,之后是通过 InvocationHandler 及 Proxy,在运行时动态 的在内存中生成了代理类对象,该代理对象是通过实现同样的接口实现(类似静态代理接口实现的方式),只是该代理类是在运行期时,动态的织入统一的业务逻辑字节码来完成。
CGLIB 实现,被代理类可以不实现接口,是通过继承被代理类,在运行时动态的生成代理类对象。