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


相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
目录
相关文章
|
18天前
|
XML 安全 Java
使用 Spring 的 @Aspect 和 @Pointcut 注解简化面向方面的编程 (AOP)
面向方面编程(AOP)通过分离横切关注点,如日志、安全和事务,提升代码模块化与可维护性。Spring 提供了对 AOP 的强大支持,核心注解 `@Aspect` 和 `@Pointcut` 使得定义切面与切入点变得简洁直观。`@Aspect` 标记切面类,集中处理通用逻辑;`@Pointcut` 则通过表达式定义通知的应用位置,提高代码可读性与复用性。二者结合,使开发者能清晰划分业务逻辑与辅助功能,简化维护并提升系统灵活性。Spring AOP 借助代理机制实现运行时织入,与 Spring 容器无缝集成,支持依赖注入与声明式配置,是构建清晰、高内聚应用的理想选择。
242 0
|
4月前
|
监控 安全 Java
Spring AOP实现原理
本内容主要介绍了Spring AOP的核心概念、实现机制及代理生成流程。涵盖切面(Aspect)、连接点(Join Point)、通知(Advice)、切点(Pointcut)等关键概念,解析了JDK动态代理与CGLIB代理的原理及对比,并深入探讨了通知执行链路和责任链模式的应用。同时,详细分析了AspectJ注解驱动的AOP解析过程,包括切面识别、切点表达式匹配及通知适配为Advice的机制,帮助理解Spring AOP的工作原理与实现细节。
|
30天前
|
人工智能 监控 安全
Spring AOP切面编程颠覆传统!3大核心注解+5种通知类型,让业务代码纯净如初
本文介绍了AOP(面向切面编程)的基本概念、优势及其在Spring Boot中的使用。AOP作为OOP的补充,通过将横切关注点(如日志、安全、事务等)与业务逻辑分离,实现代码解耦,提升模块化程度、可维护性和灵活性。文章详细讲解了Spring AOP的核心概念,包括切面、切点、通知等,并提供了在Spring Boot中实现AOP的具体步骤和代码示例。此外,还列举了AOP在日志记录、性能监控、事务管理和安全控制等场景中的实际应用。通过本文,开发者可以快速掌握AOP编程思想及其实践技巧。
|
1月前
|
人工智能 监控 安全
如何快速上手【Spring AOP】?核心应用实战(上篇)
哈喽大家好吖~欢迎来到Spring AOP系列教程的上篇 - 应用篇。在本篇,我们将专注于Spring AOP的实际应用,通过具体的代码示例和场景分析,帮助大家掌握AOP的使用方法和技巧。而在后续的下篇中,我们将深入探讨Spring AOP的实现原理和底层机制。 AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架中的核心特性之一,它能够帮助我们解决横切关注点(如日志记录、性能统计、安全控制、事务管理等)的问题,提高代码的模块化程度和复用性。
|
1月前
|
设计模式 Java 开发者
如何快速上手【Spring AOP】?从动态代理到源码剖析(下篇)
Spring AOP的实现本质上依赖于代理模式这一经典设计模式。代理模式通过引入代理对象作为目标对象的中间层,实现了对目标对象访问的控制与增强,其核心价值在于解耦核心业务逻辑与横切关注点。在框架设计中,这种模式广泛用于实现功能扩展(如远程调用、延迟加载)、行为拦截(如权限校验、异常处理)等场景,为系统提供了更高的灵活性和可维护性。
|
8月前
|
XML Java 开发者
Spring Boot中的AOP实现
Spring AOP(面向切面编程)允许开发者在不修改原有业务逻辑的情况下增强功能,基于代理模式拦截和增强方法调用。Spring Boot通过集成Spring AOP和AspectJ简化了AOP的使用,只需添加依赖并定义切面类。关键概念包括切面、通知和切点。切面类使用`@Aspect`和`@Component`注解标注,通知定义切面行为,切点定义应用位置。Spring Boot自动检测并创建代理对象,支持JDK动态代理和CGLIB代理。通过源码分析可深入了解其实现细节,优化应用功能。
407 6
|
7月前
|
XML Java 测试技术
Spring AOP—通知类型 和 切入点表达式 万字详解(通俗易懂)
Spring 第五节 AOP——切入点表达式 万字详解!
370 25
|
7月前
|
XML 安全 Java
Spring AOP—深入动态代理 万字详解(通俗易懂)
Spring 第四节 AOP——动态代理 万字详解!
270 24
|
6月前
|
Java API 微服务
微服务——SpringBoot使用归纳——Spring Boot中的切面AOP处理——Spring Boot 中的 AOP 处理
本文详细讲解了Spring Boot中的AOP(面向切面编程)处理方法。首先介绍如何引入AOP依赖,通过添加`spring-boot-starter-aop`实现。接着阐述了如何定义和实现AOP切面,包括常用注解如`@Aspect`、`@Pointcut`、`@Before`、`@After`、`@AfterReturning`和`@AfterThrowing`的使用场景与示例代码。通过这些注解,可以分别在方法执行前、后、返回时或抛出异常时插入自定义逻辑,从而实现功能增强或日志记录等操作。最后总结了AOP在实际项目中的重要作用,并提供了课程源码下载链接供进一步学习。
748 0
|
6月前
|
Java 开发者 微服务
微服务——SpringBoot使用归纳——Spring Boot中的切面AOP处理——什么是AOP
本文介绍了Spring Boot中的切面AOP处理。AOP(Aspect Oriented Programming)即面向切面编程,其核心思想是分离关注点。通过AOP,程序可以将与业务逻辑无关的代码(如日志记录、事务管理等)从主要逻辑中抽离,交由专门的“仆人”处理,从而让开发者专注于核心任务。这种机制实现了模块间的灵活组合,使程序结构更加可配置、可扩展。文中以生活化比喻生动阐释了AOP的工作原理及其优势。
349 0