【Spring】AOP 统一问题处理

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 1. 什么是 Spring AOP2. 为什么要用 AOP3. AOP 组成3.1 切面(Aspect)3.2 连接点(Join Point)3.3 切点(Pointcut)3.4 通知(Advice)4. Spring AOP 实现4.1 添加 AOP 框架支持4.2 定义切面和切点4.2.1 切点表达式说明4.2.2 表达式示例4.3 定义相关通知5. Spring AOP 实现原理5.1 织入(Weaving):代理的生成时机5.2 动态代理5.2.1 JDK 动态代理实现5.2.2 CGLIB 动态代理实现5.2.3 JDK 和 CGLIB 的区别

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 整个组成部分的概念如下图所示,以多个页面都要访问用户登录权限为例:


18.png

18.png


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 生成代理类。


19.png


19.png


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 实现,被代理类可以不实现接口,是通过继承被代理类,在运行时动态的生成代理类对象。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
27天前
|
Java
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
这篇文章是Spring5框架的实战教程,深入讲解了AOP的基本概念、如何利用动态代理实现AOP,特别是通过JDK动态代理机制在不修改源代码的情况下为业务逻辑添加新功能,降低代码耦合度,并通过具体代码示例演示了JDK动态代理的实现过程。
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
|
2月前
|
Java Spring
在Spring Boot中使用AOP实现日志切面
在Spring Boot中使用AOP实现日志切面
|
27天前
|
XML Java 数据格式
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
这篇文章是Spring5框架的AOP切面编程教程,通过XML配置方式,详细讲解了如何创建被增强类和增强类,如何在Spring配置文件中定义切入点和切面,以及如何将增强逻辑应用到具体方法上。文章通过具体的代码示例和测试结果,展示了使用XML配置实现AOP的过程,并强调了虽然注解开发更为便捷,但掌握XML配置也是非常重要的。
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
|
11天前
|
缓存 Java 开发者
Spring高手之路22——AOP切面类的封装与解析
本篇文章深入解析了Spring AOP的工作机制,包括Advisor和TargetSource的构建与作用。通过详尽的源码分析和实际案例,帮助开发者全面理解AOP的核心技术,提升在实际项目中的应用能力。
9 0
Spring高手之路22——AOP切面类的封装与解析
|
29天前
|
安全 Java 开发者
Java 新手入门:Spring 两大利器IoC 和 AOP,小白也能轻松理解!
Java 新手入门:Spring 两大利器IoC 和 AOP,小白也能轻松理解!
27 1
|
29天前
|
Java Spring
Spring的AOP组件详解
该文章主要介绍了Spring AOP(面向切面编程)组件的实现原理,包括Spring AOP的基础概念、动态代理模式、AOP组件的实现以及Spring选择JDK动态代理或CGLIB动态代理的依据。
Spring的AOP组件详解
|
1月前
|
Java API Spring
Spring Boot 中的 AOP 处理
对 Spring Boot 中的切面 AOP 做了详细的讲解,主要介绍了 Spring Boot 中 AOP 的引入,常用注解的使用,参数的使用,以及常用 api 的介绍。AOP 在实际项目中很有用,对切面方法执行前后都可以根据具体的业务,做相应的预处理或者增强处理,同时也可以用作异常捕获处理,可以根据具体业务场景,合理去使用 AOP。
|
11天前
|
Java Spring XML
掌握面向切面编程的秘密武器:Spring AOP 让你的代码优雅转身,横切关注点再也不是难题!
【8月更文挑战第31天】面向切面编程(AOP)通过切面封装横切关注点,如日志记录、事务管理等,使业务逻辑更清晰。Spring AOP提供强大工具,无需在业务代码中硬编码这些功能。本文将深入探讨Spring AOP的概念、工作原理及实际应用,展示如何通过基于注解的配置创建切面,优化代码结构并提高可维护性。通过示例说明如何定义切面类、通知方法及其应用时机,实现方法调用前后的日志记录,展示AOP在分离关注点和添加新功能方面的优势。
24 0
|
20天前
|
缓存 安全 Java
Spring AOP 中两种代理类型的限制
【8月更文挑战第22天】
13 0
|
20天前
|
Java Spring