【Spring AOP 源码解析】彻底搞懂 AOP 是如何执行的

前言(关于源码航行)
在这里插入图片描述

在准备面试和学习的过程中,我阅读了还算多的源码,比如 JUC、Spring、MyBatis,收获了很多代码的设计思想,也对平时调用的 API 有了更深入的理解;但过多散乱的笔记给我的整理复习带来了比较大的麻烦。
📋 在 C 站零零散散发了 JUC 的源码解析和集合源码解析,收到了很多朋友的喜爱,这里我准备将一些源码解析的文章整合起来,为了方便阅读和归纳在这里整合成目录:源码航行阅读目录,大家感兴趣的话可以关注一下!

在正式开始源码解析之前,我们先来构建一个简单的 Spring Boot 项目,以便更好的观察我们定义的切面逻辑是如何执行的。
如果对 AOP 的基本概念或者如何使用有疑惑的话可以去看一下这篇文章【Spring AOP 源码解析前篇】什么是 AOP | 通知类型 | 切点表达式| AOP 如何使用,本篇是关于源码的详细解析,就不会涉及这些基础的内容啦。

项目构建

我们先来构建一个简单的项目,便于更好的观察源码的执行流程。

@RestController
public class MessageController {

    @Resource
    HelloService service;

    @ResponseBody
    @RequestMapping("/aop")
    public String aop() {
        service.aop();
        return "接口请求成功";
    }

    @ResponseBody
    @RequestMapping("/non_aop")
    public String nonAop() {
        service.nonAop();
        return "接口请求成功";
    }

}

@Service
public class HelloServiceImpl implements HelloService {

    @Override
    public void aop() {
        System.out.println("Hello AOP!");
    }

    @Override
    public void nonAop() {
        System.out.println("Hello Non-AOP!");
    }

}

代理 AOP,这里先来看一下前置通知的源码,其他的通知类型和前置的处理方式是几乎完全相同的。

@Aspect
@Component
public class LogHandler {

    @Pointcut("execution(* com.kq.springaop.service.HelloService.aop())")
    public void pointCut(){}

    @Before("pointCut()")
    public void Before(JoinPoint joinPoint) {
        System.out.println("前置通知");
    }

}

在上面的项目中,我们为 Service 层的接口添加了一个简单的日志功能。

Spring 在真正的通过反射去执行实际调用的方法之前,会有一个名为 intercept() 的方法,执行一些拦截的逻辑;我们构造的这些切面方法实际上就是在这部分中执行的;Spring 在创建 Bean 的时候会根据 AOP 注解生成一个由一组 interceptor 组成的链式结构,然后在调用实际方法之前,通过循环的方式按照顺序去执行这些 interceptor 中的代码,这样就在合适的时机去执行了合适的代码。

在下面这一部分的源码追踪中,我们会看到代理类执行一个方法的完整过程;关于这些拦截器是在何时构造的放到下个部分去讲解。

第一部分:AOP 代码执行流源码追踪

我们先将断点打在使用 AOP 的那个接口中,然后发送一个请求,看一下完整的执行流程:

    @ResponseBody
    @RequestMapping("/aop")
    public String aop() {
        service.aop(); // 断点
        return "接口请求成功";
    }

当请求过来的时候,可以在 Thread & Variables 中看到 service 的具体类型:

在这里插入图片描述

可以看到,这个对象是一个通过CGLIB生成的Spring AOP代理对象。CGLIB(Code Generation Library)是一个在Java中生成动态代理类的库,用于在运行时创建一个类的子类,并在子类中拦截方法调用。这个代理对象包含多个CGLIB生成的回调(CALLBACK),每个回调都是不同类型的拦截器,例如 DynamicAdvisedInterceptorStaticUnadvisedInterceptor。这些拦截器负责处理不同的AOP(面向切面编程)增强逻辑。

这些回调的作用方法的作用是这样的:

  1. DynamicAdvisedInterceptor:用于处理动态代理对象的方法调用,它会根据代理对象的增强配置来执行相应的逻辑。
  2. StaticUnadvisedInterceptor:处理不包含增强逻辑的方法调用。
  3. SerializableNoOp:一个序列化时的占位符,不执行任何操作。
  4. StaticDispatcher:静态调度器,用于处理一些静态方法调用。
  5. AdvisedDispatcher:用于处理代理对象的增强配置。
  6. EqualsInterceptor:拦截 equals 方法调用。
  7. HashCodeInterceptor:拦截 hashCode 方法调用。

当我们调用 GCLIB 生成的代理对象的时候,这个方法就会被拦截:

		@Override
		@Nullable // 表示这个方法可以返回 null
		public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
			Object oldProxy = null;
			boolean setProxyContext = false;
			Object target = null;
			TargetSource targetSource = this.advised.getTargetSource(); // 获取当前 AOP 执行的目标方法
			try {
				if (this.advised.exposeProxy) {
				// (1)检测当前方法是否将代理对象公开
					oldProxy = AopContext.setCurrentProxy(proxy);
					setProxyContext = true;
				}
				target = targetSource.getTarget();
				Class<?> targetClass = (target != null ? target.getClass() : null);
				// (2)获取当前代理对象该方法的的拦截器链
				List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
				Object retVal;
				if (chain.isEmpty()) {
				// 如果没有拦截器链,这直接通过反射去调用方法
					Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
					retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
				} else {
				// (3)执行拦截器中的方法
					retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
				}
				// 对返回值进行处理,并返回
				return processReturnType(proxy, target, method, args, retVal);
			}
			finally {
				if (target != null && !targetSource.isStatic()) {
					targetSource.releaseTarget(target);
				}
				if (setProxyContext) {
						AopContext.setCurrentProxy(oldProxy);
				}
			}
		}

上面我们提到,AOP 执行实际上是将方法前后要执行的拦截器串成一个链,然后在合适的时机执行,这个链(chain)就是上面 (2) 标注的 chain 变量。
在这里插入图片描述
我们此时只设置了一个前置通知,但可以发现,在调用之前还会有一个特殊的拦截器 ExposeInvocationInterceptor (方法调用公开拦截器),这是一个用于拦截方法调用的组件,并且在拦截过程中会将 MethodInvocation 对象公开,使其可以在方法执行期间被其他代码访问;可以将其视作 AOP 调用链的一个入口,它会将方法执行信息 MethodInvocation 对象存储到当前线程的 ThreadLocal 中,以便其他方法能够快速的通过 currentInvocation() 方法来访问当前方法执行对象。

最后就是上面 (3) 位置所标注的位置,在这里会去创建一个 CglibMethodInvocation ,也就是 Cglib 的方法执行对象去执行调用链的方法。


这个方法就大致讲明白了,这里说点题外话,关于上面代码 (1) 位置的 AopContext.setCurrentProxy(proxy) 方法:

这个方法会在我们将 this.advised.exposeProxy 设置为 true 的时候调用,用来将 proxy 代理对象公开;具体来说,当代理对象的一个方法被调用时,该方法内部可能会调用目标对象的其他方法。如果这些内部调用希望也能够触发AOP拦截器(即被增强),那么这些调用需要通过代理对象来完成。这就要求代理对象在方法内部是可以被访问的;比如通过这样的方式就可以在 methodA() 中去通过动态代理对象去调用 methodB()

public class MyService {
    public void methodA() {
        System.out.println("Executing methodA");
        // Call methodB through proxy to apply AOP advice
        ((MyService) AopContext.currentProxy()).methodB();
    }

    public void methodB() {
        System.out.println("Executing methodB");
    }
}

常用的开启方式是在配置类上加入注解,类似如下的这种形式:

@Configuration
@EnableAspectJAutoProxy(exposeProxy = true)
public class AppConfig {
    // Bean definitions
}

接下来让我们继续回到拦截器链调用的位置,也就是上面的代码 (3) ,它首先通过 new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy) 语句去创建了一个 CglibMethodInvocation 类的实例对象,通过追踪可以发现,这个类是当前类 CglibAopProxy 的一个私有静态内部类:

private static class CglibMethodInvocation extends ReflectiveMethodInvocation

内部只提供了一个 proceed() 执行方法,用来调用父类中的同名方法;并在父类的基础上拓展了对异常的处理功能。

然后我们正式进入实际执行的步骤,也就是上面提到的 proceed() 方法;一路追踪,最后来到了 ReflectiveMethodInvocation 类中:

	@Override
	@Nullable
	public Object proceed() throws Throwable {
		// (1)按照下标来循环执行
		if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
			return invokeJoinpoint();
		}

		Object interceptorOrInterceptionAdvice =
				this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
		if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher dm) {
			// (2)判断是否是动态方法匹配器
			Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
			if (dm.matcher().matches(this.method, targetClass, this.arguments)) {
				return dm.interceptor().invoke(this);
			}
			else {
				// 动态方法匹配失败,跳过这个方法继续执行下一个拦截器
				return proceed();
			}
		}
		else {
			// (3)普通的拦截器,直接执行
			return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
		}

在代码 (1) 的位置 currentInterceptorIndex 在类刚开始创建的时候会被设置为 -1 ,在执行的时候通过 ++ 自增运算符来更新索引位置。

private int currentInterceptorIndex = -1;

// 更新索引位置
Object interceptorOrInterceptionAdvice =
		this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);

在获取到 interceptorOrInterceptionAdvice 拦截器或者拦截通知的时候,会去判断这个拦截器是否是动态的拦截逻辑,动态的拦截器允许在运行时根据方法和参数的实际情况动态地决定是否应用某个拦截器。通常在 AOP 配置中,静态部分已经确定了哪些方法匹配哪些切入点,而动态部分可以在方法执行时再进行更细致的匹配。

当你定义一个切入点时,有时候不仅仅需要在编译时静态地确定哪些方法匹配,还需要在运行时根据方法参数来进一步筛选。例如下面这个案例:

@Aspect
public class MyAspect {
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void myPointcut() {}

    @Around("myPointcut() && args(name)")
    public Object aroundAdvice(ProceedingJoinPoint pjp, String name) throws Throwable {
        // 动态匹配逻辑
        if (name.startsWith("test")) {
            return pjp.proceed();
        } else {
            return null;
        }
    }
}

在这个例子中,args(name) 部分就是动态匹配的条件,只有当参数 name 满足某些条件时,才会执行切面逻辑;当执行这样的方法的时候就会使用到 InterceptorAndDynamicMethodMatcher 这个类,它的定义通常是这样的:

public class InterceptorAndDynamicMethodMatcher {
    private final MethodInterceptor interceptor;
    private final MethodMatcher methodMatcher;

    public InterceptorAndDynamicMethodMatcher(MethodInterceptor interceptor, MethodMatcher methodMatcher) {
        this.interceptor = interceptor;
        this.methodMatcher = methodMatcher;
    }

    public MethodInterceptor getInterceptor() {
        return this.interceptor;
    }

    public MethodMatcher getMethodMatcher() {
        return this.methodMatcher;
    }
}

其中除了 interceptor 之外,还有一个 methodMatcher 方法匹配器,在通过它提供的 matches() 方法就可以判断出此时的切面逻辑是否要执行。

所以我们可以看到,在 process() 方法中,当判断是一个动态拦截器的时候,就会先获取 macher ,根据是否匹配去决定是执行 interceptor 中的方法,亦或是直接跳过。


但很显然,我们定义的 Before Advice 是一个静态的拦截器,它走的就是这个位置:

else { return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); }

然后就走到了我们上面已经提到过的 ExposeInvocationInterceptor ,也就是将 MethodInvocation 暴露到线程的 ThreadLocal 中的方法:

	@Override
	@Nullable
	public Object invoke(MethodInvocation mi) throws Throwable {
		MethodInvocation oldInvocation = invocation.get();
		invocation.set(mi);
		try {
			return mi.proceed();
		}
		finally {
			invocation.set(oldInvocation);
		}
	}

finally 块中,无论方法调用成功与否,都会将之前保存的 oldInvocation 恢复到当前线程的上下文中。这确保了上下文的清理,避免了线程泄漏或不一致的状态。

接着进入上面的 process() 方法,还是相同的逻辑(可以看到此时的 currentInterceptorIndex 变成了 1 ),此时我们就进入了前置通知拦截器中,这个类的名称叫做MethodBeforeAdviceInterceptor

	@Override
	@Nullable
	public Object invoke(MethodInvocation mi) throws Throwable {
		this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
		return mi.proceed();
	}

在这里,通过 advice 去执行我们实际编写的方法
AspectJMethodBeforeAdvice 中的 before 方法,在其中调用的其父类 AbstractAspectJAdviceinvokeAdviceMethod 方法

	@Override
	public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
		invokeAdviceMethod(getJoinPointMatch(), null, null);
	}

最终就到了这个位置:

	protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
		Object[] actualArgs = args;
		if (this.aspectJAdviceMethod.getParameterCount() == 0) {
			actualArgs = null;
		}
		try {
		// (1)调用方法
			ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
			return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
		}
		catch (IllegalArgumentException ex) {
			throw new AopInvocationException("Mismatch on arguments to advice method [" +
					this.aspectJAdviceMethod + "]; pointcut expression [" +
					this.pointcut.getPointcutExpression() + "]", ex);
		}
		catch (InvocationTargetException ex) {
			throw ex.getTargetException();
		}
	}

最终,在上面的代码 (1) 位置执行了我们前面定义的方法,其中的 aspectJAdviceMethod 就已经是 Java 的反射对象 Method

protected transient Method aspectJAdviceMethod;

最终,方法的执行来到这个位置,也就是我们案例中实际写到的方法:

    @Before("pointCut()")
    public void Before(JoinPoint joinPoint) {
        joinPoint.getArgs();
        System.out.println("前置通知");
    }

随后,当重新返回到 process 方法的时候,此时的索引就变成了 1,此时就会去执行第一个 if 中的内容,在这里面去执行实际调用的方法:

	@Override
	@Nullable
	public Object proceed() throws Throwable {
		// We start with an index of -1 and increment early.
		if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
			return invokeJoinpoint();
		}

		// ...
	}
	
	@Nullable
	protected Object invokeJoinpoint() throws Throwable {
		return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments);
	}

这里也是通过 Java 反射去调用方法,最终将返回值返回给调用方;这是普通的拦截器,但是还有一个比较特殊的 Around Advice,它可以对方法的返回值做一个处理:

Object proceed = joinPoint.proceed();

当执行到上面的方法时,会先去执行目标方法来获取返回值;剩余的地方与其他拦截器的逻辑是完全相同的,这样也正好顺应了这些通知的执行顺序:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

*Soo_Young*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值