【Spring AOP 源码解析前篇】什么是 AOP | 通知类型 | 切点表达式| AOP 如何使用

前言(关于源码航行)

在准备面试和学习的过程中,我阅读了还算多的源码,比如 JUC、Spring、MyBatis,收获了很多代码的设计思想,也对平时调用的 API 有了更深入的理解;但过多散乱的笔记给我的整理复习带来了比较大的麻烦。

📋 在 C 站零零散散发了 JUC 的源码解析和集合源码解析,收到了很多朋友的喜爱,这里我准备将一些源码解析的文章整合起来,为了方便阅读和归纳在这里整合成目录:源码航行阅读目录,大家感兴趣的话可以关注一下!


第一篇:基础知识介绍

这一部分我们来谈一下关于 Spring AOP 的浮在表面上的知识,比如什么是 AOP、它有什么好处、如何使用等等

为什么需要 AOP?

AOP(Aspect Oriented Programming),意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。

利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

举一个简单的例子,如果我们想要在每次调用一个类中的方法之前输出一个日志,那其实是挺容易实现的,画出流程图就是这个样子:

那如果我们要在这些方法之前加一个鉴权呢?同样,按照前面的方式,无非就是多 copy 一次嘛

就这样,通过多重 copy 的方式,最终完成了这个业务,而这也就代表着,每次扩展新的方法,你都要去进行 n 次复制粘贴;每次要更改鉴权或者日志的逻辑,你都要去进行 n 次复制粘贴;每次。。。。。。

听起来都让人非常头大,但如果我们将逻辑改成这样呢?

我们将这两个方法封装成一个公共方法,每次在方法之前去调用这个公共的方法,不就实现了可维护性吗,这其实就有一点切面的感觉了。

这样,我们虽然每次都不用去复制代码了,但还是需要在我们需要的位置去调用这个接口,而调用这个代码的位置其实就是 切面。我们用具体的代码来看一下,下面实现了一个统一的接口 BeforeAdvice,然后在每个方法之前去调用它。

java 复制代码
public interface BeforeAdvice {
    void before();
}

public class LoggingBeforeAdvice implements BeforeAdvice {
    @Override
    public void before() {
        System.out.println("方法调用之前的日志");
    }
}

public class MyService {
    private BeforeAdvice beforeAdvice = new LoggingBeforeAdvice();

    public void myMethodA() {
        beforeAdvice.before();
        // 业务逻辑
        System.out.println("执行 myMethod1A");
    }

    public void myMethodB() {
        beforeAdvice.before();
        // 业务逻辑
        System.out.println("执行 myMethodB");
    }
}

到这里,我们就自己实现了一个简单的 AOP;但每次都这样去创建一个这样的类,其实也是非常复杂的,这时候我们就可以借助 Spring AOP 的力量,它帮我们实现了简单、直观、极其易于配置的切面编程方式。

AOP 概念辨析

在正式讲解如何使用 Spring AOP 之前,我们来讲点无聊的内容,关于 AOP 的概念辨析;这部分内容是一些术语,但是如果能理解它们,就会对 AOP 整个流程有较为清晰的把握。

连接点(Join Point) :连接点是程序执行中的一个点,这个点可以是方法调用、方法执行、异常抛出等。在 Spring AOP 中,连接点主要是指方法的调用或执行。连接点是通知的实际应用点。

切点(PointCut) :由于连接点可能很多(比如一个类中的所有方法),想要把所有连接点罗列出现显然有些困难;切点则定义了在应用通知的连接点的集合 。切点通过切点表达式(例如:execution(* com.example.service.*.*(..)))来指定匹配的方法和类 。切点表达式用于筛选连接点,使得通知只在特定的连接点上执行。

通知(Advice) :通知是在切点处 执行的代码。通知定义了具体的横切逻辑,决定了在方法执行的什么阶段(之前、之后、环绕等)插入横切逻辑。通知有五种类型,我们会在下一部分进行详细的了解;通知就是在 何时 执行 怎样 的逻辑。

切面(Aspect):切面是 AOP 的核心模块,它封装了跨越多个类的关注点,例如日志记录、事务管理或安全控制。切面通过通知(Advice)和切点(Pointcut)来定义在何时、何地应用这些关注点;可以将切面看作是切点(Pointcut)和通知(Advice)的组合。切面定义了在何处(切点)以及何时(通知)应用横切逻辑。

五种通知类型

在 Spring AOP 中,通知(Advice)是指在程序执行过程中插入的代码,它定义了在何时以及在什么情况下进行切面的操作。通知是切面中的实际动作部分,是横切关注点的具体实现;直观来说就是要插入的那一组方法。

除了上面提到的在执行方法之前执行的 Before Advice,还有其他四种类型的通知,也就是说 Spring AOP 为我们提供了五个插入代码的位置选择。

前置通知(Before Advice)

在目标方法执行之前执行的通知。可以用来执行日志记录、安全检查等。

java 复制代码
@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void beforeAdvice() {
        System.out.println("前置通知:方法调用之前执行");
    }
}

后置通知(After Advice)

在目标方法执行之后执行的通知,无论方法是成功返回还是抛出异常。常用于清理资源等。

java 复制代码
@Aspect
@Component
public class LoggingAspect {
    @After("execution(* com.example.service.*.*(..))")
    public void afterAdvice() {
        System.out.println("后置通知:方法调用之后执行");
    }
}

返回后通知(After Returning Advice)

在目标方法成功返回结果之后执行的通知。可以用来记录返回值或对返回值进行处理。

java 复制代码
@Aspect
@Component
public class LoggingAspect {
    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void afterReturningAdvice(Object result) {
        System.out.println("返回后通知:方法返回值为 " + result);
    }
}

抛出异常后通知(After Throwing Advice)

在目标方法抛出异常后执行的通知。可以用来记录异常信息或执行异常处理逻辑。

java 复制代码
@Aspect
@Component
public class LoggingAspect {
    @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "exception")
    public void afterThrowingAdvice(Exception exception) {
        System.out.println("抛出异常后通知:异常为 " + exception.getMessage());
    }
}

环绕通知(Around Advice)

环绕通知在目标方法执行的前后都执行,可以完全控制目标方法的执行,包括决定是否执行目标方法,以及在目标方法执行前后添加自定义逻辑。环绕通知最为强大和灵活。

java 复制代码
@Aspect
@Component
public class LoggingAspect {
    @Around("execution(* com.example.service.*.*(..))")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("环绕通知:方法调用之前");
        Object result = joinPoint.proceed();  // 执行目标方法
        System.out.println("环绕通知:方法调用之后");
        return result;
    }
}

这五种通知方式的执行顺序是这样的:

  • 前置通知(Before Advice)
  • **环绕通知(Around Advice)**的前半部分
  • 目标方法执行
  • **环绕通知(Around Advice)**的后半部分
  • 返回后通知(After Returning Advice)(如果目标方法成功返回)
  • 抛出异常后通知(After Throwing Advice)(如果目标方法抛出异常)
  • 后置通知(After Advice)

切点表达式

前面提到过,切面直观来讲就是插入方法的位置;在前面五种通知类型中,我们已经看到了如何通过注解选择方法的执行位置,但是诸如
* com.example.service.*.*(..)) 这样,定位方法位置的格式其实是没有提及的,这一部分重点来讲一下如何配置方法的位置。

切点则表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。

切点表达式由以下几部分组成:

java 复制代码
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

其中,各部分的含义如下,注意上面的问号(?)表示可选

  • execution:指定切点类型为方法执行。
  • modifiers-pattern :可选,方法的访问修饰符,如 publicprotectedprivate。通常省略不写,表示任意访问修饰符。
  • ret-type-pattern :方法的返回类型模式,例如 voidString、``(任意返回类型)。
  • declaring-type-pattern :可选,方法所在类的全限定名称模式,如 com.example.service.*。指定要匹配的类或包。
  • name-pattern :方法名称模式,如 Serviceget*。支持通配符 ``(匹配任意字符序列)和 ..(匹配任意数量和类型的参数)。
  • param-pattern :方法参数模式,如 ()(无参数)、(*)(一个任意类型的参数)、(..)(任意数量和类型的参数)。
  • throws-pattern:可选,方法可能抛出的异常模式。

下面来做几个小练习

1)指定 com.example.service 包下的所有类的所有方法:

java 复制代码
@Pointcut("execution(* com.example.service.*.*(..))")
private void serviceMethods() {}

2)com.example.service 包下所有类的方法中第一个参数为 String 类型的方法

java 复制代码
@Pointcut("execution(* com.example.service.*.*(String, ..))")
private void stringParamMethods() {}

通过上面的练习,相信大家对切点表达式的书写方式有了一定的掌握,下面我们来看看,除了 execution 方法执行切点,Spring 还为我们提供了哪些指定切点的方式

within :限定匹配特定类型的连接点。within(com.example.service.*) 匹配 com.example.service 包及其子包中所有方法。

this :限定匹配特定类型的 bean。this(com.example.service.MyService) 匹配实现 com.example.service.MyService 接口的 bean。

target :限定匹配特定类型的目标对象。target(com.example.service.MyService) 匹配目标对象是 com.example.service.MyService 的连接点。

args :限定匹配特定参数类型的方法。args(String, ..) 匹配第一个参数是 String 类型的方法。

@annotation :限定匹配特定注解的方法。@annotation(org.springframework.transaction.annotation.Transactional) 匹配标注有 @Transactional 注解的方法。

因为 within、this 和 target,都可以通过 execution 作为一定程度上的替代,所以这里我们重点关注一下匹配特定注解的方式,即 @annotation 即可。

但同时,这些表达式其实是可以共用的,比如通过这样的方式:

java 复制代码
@Before("execution(* com.example.service.UserService.getUserById(int)) && args(userId)")
    public void logBeforeGetUserById(JoinPoint joinPoint, int userId) {
        System.out.println("Before calling getUserById with userId: " + userId);
    }

上面的 userId 参数是通过切点表达式中的 args(userId) 指定的,所以在方法体内可以直接使用 userId 参数来获取方法执行时的具体值;但如果仅仅使用 joinPoint 的话就需要 getArgs() 再拿取参数了;这里只涉及写法上的偏好,我们平时使用的大部分的内容通过 execution 和 @annotation 都是可以实现的。

正式使用

通过前面的介绍,我们已经辨析了 AOP 的基本概念,了解了控制何时执行逻辑的通知类型(Advice) ,定义在什么位置执行的切点表达式(PointCut),下面我们正式来尝试使用 AOP 来解决一些现实的问题。

就举一个前面提到的日志记录的 AOP 吧

定义一个简单的服务类

java 复制代码
@Service
public class UserService {

    public String getUserById(int userId) {
        // 模拟方法体
        return "User: " + userId;
    }
}

创建日志记录的切面类

java 复制代码
@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.UserService.getUserById(int))")
    public void logBeforeGetUserById(JoinPoint joinPoint) {
		    // 获取方法参数
		    Object[] args = joinPoint.getArgs();
        System.out.println("Before calling getUserById with userId: " + args[0]);
    }

    @AfterReturning(pointcut = "execution(* com.example.service.UserService.getUserById(int))", returning = "result")
    public void logAfterReturningGetUserById(JoinPoint joinPoint, Object result) {
        System.out.println("After returning from getUserById with result: " + result);
    }
}

然后我们去做一个简单的测试,可以看到如下的输出:

java 复制代码
Before calling getUserById with userId: 123
After returning from getUserById with result: User: 123
User retrieved: User: 123

关于 JoinPoint 和 ProceedingJoinPoint

在 Spring AOP 中,JoinPointProceedingJoinPoint 是两个重要的接口,用于在切面中获取方法执行时的信息和控制方法执行;我们在书写切面逻辑的时候,需要的大部分参数或者方法信息等,都是从这里面获取的。

JoinPoint 接口

JoinPoint 接口是 Spring AOP 提供的一个核心接口,用于描述正在执行的连接点(join point),它可以用来获取方法的签名、参数等信息,但是不能直接控制方法的执行流程。

常用方法

java 复制代码
Signature getSignature(); // 获取代表被通知方法签名的对象,可以进一步获取方法名、声明类型等信息。

Object[] getArgs(); // 获取被通知方法的参数对象数组。

Object getTarget(); // 获取目标对象,即被通知的目标类实例。

Object getThis(); // 获取代理对象的引用,即代理对象本身。

Object[] getArgs(); // 获取调用方法时传递的参数

其中有个特殊一点的是 Signature,方法签名接口:

java 复制代码
public interface Signature {
    String toString(); // 返回方法的字符串表示形式。

    String toShortString(); // 返回方法的简短字符串表示形式。

    String toLongString(); // 返回方法的长字符串表示形式。

    String getName();// 获取方法名。 

    int getModifiers(); // 获取方法的修饰符,返回一个整数,具体取值需要通过 java.lang.reflect.Modifier 类来解析。

    Class getDeclaringType(); // 获取声明该方法的类的 Class 对象。

    String getDeclaringTypeName(); // 获取声明该方法的类的全限定名。
}

大家可以自己写个方法测试一下,这里就不过多赘述了。

ProceedingJoinPoint 接口

ProceedingJoinPoint 接口继承自 JoinPoint 接口,它扩展了 JoinPoint 接口,提供了控制方法执行流程的能力。通常在 Around Advice 中使用 ProceedingJoinPoint 来调用目标方法,并可以控制是否继续执行该方法,以及在执行前后进行额外的处理。

常用方法

java 复制代码
Object proceed() throws Throwable; // 继续执行连接点(即目标方法),返回方法的返回值。

Object proceed(Object[] args) throws Throwable; // 按照给定的参数继续执行连接点。

由于是继承,所以 JoinPoint 提供的方法,也都可以使用。

区别和用途

  • JoinPoint 主要用于获取方法的元数据信息,如方法名、参数等,不具备控制方法执行流程的能力。
  • ProceedingJoinPoint 继承自 JoinPoint,可以控制方法的执行流程,在 Around Advice 中使用,可以决定是否继续执行目标方法,以及在执行前后进行额外的处理。
相关推荐
confiself12 分钟前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
Wlq041517 分钟前
J2EE平台
java·java-ee
XiaoLeisj24 分钟前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee
鹿屿二向箔35 分钟前
基于SSM(Spring + Spring MVC + MyBatis)框架的汽车租赁共享平台系统
spring·mvc·mybatis
豪宇刘38 分钟前
SpringBoot+Shiro权限管理
java·spring boot·spring
Elaine20239143 分钟前
02多线程基础知识
java·多线程
gorgor在码农1 小时前
Redis 热key总结
java·redis·热key
百事老饼干1 小时前
Java[面试题]-真实面试
java·开发语言·面试
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS医院管理系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源·intellij-idea