SpringAop新链实现任意无参方法调用
Fir3proof WEB安全 990浏览 · 2025-04-02 07:40

AOP初体验

有必要先了解一些AOP的概念

切面(Aspect): 公共功能的实现。如日志切面、权限切面、验签切面。给Java类使用@Aspect注释修饰,就能被AOP容器识别为切面

通知(Advice): 切面的具体实现,即切面类中的一个方法,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)

连接点(JoinPoint): 程序在运行过程中能够插入切面的地方。Spring只支持方法级的连接点,比如一个目标对象有5个方法,就有5个连接点

切点(PointCut): 用于定义通知应该应用到哪些连接点

引入(Introduction):动态地为类添加新的接口实现

spring-boot-starter-aop这个依赖就能包括spring-aopaspectJWeaver这两个所需依赖

接下来尝试切面织入

1 定义切面类LoggingAspect,下面的logBefore方法即一个通知(advice),并将通知注册到了定义的切点serviceLayer

@Pointcut使用规则匹配来定义通知(advice)要应用到哪些连接点(joint point,即方法)

execution表示匹配方法的执行

第一个*号表示方法有任意返回值

org.test.service为匹配的包名

后面两个*表示包名下的所有类的所有方法

(...)表示方法的参数任意

1定义服务类

可以看到IDEA也会有相应的智能提醒
image.png
图片加载失败
3. 启动SpringBoot项目

打印如下内容,成功通过AOP实现方法级别的hook

从AOP动态代理到方法拦截

JdkDynamicAopProxy这个类最初用于解决Jackson链的不稳定触发(https://xz.aliyun.com/news/1229),但深入研究下去才发现它功能的强大。 这个类是JDK动态代理中的调用处理器,当代理对象Proxy调用方法时,会跳转到该类的invoke方法来处理。

在调用目标对象Target的方法之前,会检查这个方法是否有配置拦截链 在Jackson链中走的是chain.isEmpty()的情况,即未配置拦截链,直接通过反射来调用Target的方法 若配置了拦截链,则先通过拦截链的一层层处理,再到JoinPoint(即target的方法) AdvisedSupport是关于代理对象配置的,先看如何获取用于拦截的advice

readObject会为methodCache分配一个空的Map,因此首次根据MethodCacheKey获取拦截链肯定得到null 因此接着看getInterceptorsAndDynamicInterceptionAdvice
image.png
图片加载失败
从配置获取Advisior(包含有advice和决定advice作用位置匹配的过滤器),有多少个Advisor拦截链interceptorList长度就有多少 接着判断是作用到PointCut的advice还是作用到Introduction的advice,但最终都是通过registry.getInterceptors(advisor)获取Interceptor合并到interceptorList中,registry为DefaultAdvisorAdapterRegistry
image.png
图片加载失败
若advice是MethodInterceptor类型,直接加入拦截器中,顾名思义MethodInterceptor这个接口就是用于拦截前往target的方法调用,往其声明的invoke方法添加拦截时要处理的逻辑(如记录日志)即可。 回到JdkDynamicAopProxy,当chain不为空时,实例化ReflectiveMethodInvocation并调用其proceed
image.png
图片加载失败
currentInterceptorIndex指向当前拦截器在拦截链中的索引,若已经执行完所有拦截器(即索引已到达size-1),则直接invokeJoinpoint调用JoinPoint连接点的方法(即target的方法)。如果当前advice是根据规则动态匹配方法的,则先判断当前方法是否匹配到,若匹配到则调用拦截器的invoke方法。

Advice调用切面方法

现在思考一下第一节AOP的案例最后advice是怎么调用到我们自定义的切面方法呢,我们只是定义了一个接受JoinPoint参数的方法,然后用@Before注释了该方法。可以猜测框架保存了这个Method,最后用反射调用,传入匹配到的JoinPoint作为参数。 接下来就要看AbstractAspectJAdvice这个抽象类,上面提到的几个通知类型都是它的子类(包括Before、After、AfterReturning、AfterThrowing、Around)
image.png
图片加载失败
以After的invoke方法为例

先调用proceed让拦截链继续传递到下一个拦截器,最后再invokeAdviceMethod调用拦截逻辑(AbstractAspectJAdvice中定义) 最终走到invokeAdviceMethodWithGivenArgs

aspectJAdviceMethod即advice对应的方法(如上面实现的logBefore) 注意Method并没有实现Serializable接口,aspectJAdviceMethod属性也是由transient修饰 Method实际上是readObject时通过反射恢复的

调用对象由this.aspectInstanceFactory.getAspectInstance()获取

有多个实例工厂类
image.png
图片加载失败


SimpleAspectInstanceFactory 直接反射调用的无参构造器,可惜这个类不能序列化,否则可以考虑打ClassPathXmlApplicationContext

SingletonAspectInstanceFactory 单例工厂,直接返回对象

SimpleBeanFactoryAwareAspectInstanceFactory 由Bean工厂创建

发现有个SimpleJndiBeanFactory,可惜这个工厂类也不能序列化,不然可能可以打JNDI

因此能利用的只有单例工厂了。 现在反射调用方法的三要素Method和调用对象都齐了,只剩参数了。 AbstractAspectJAdvice#argBinding

jpMatch来自getJoinPointMatch

没找到可以调用ProxyMethodInvocation#setUserAttribute的地方,又想到是否有拦截器可以拦截时添加这一属性,翻了一圈没找到(还是有挺多有意思的拦截器,但都不能序列化QAQ) 所以目前只能调用无参方法了。

任意无参方法调用

直接按上面讲解的来构造,会发现报错了
image.png
图片加载失败
找不到MethodInvocation,也就是在JdkDynamicAopProxy中实例化的ReflectiveMethodInvocation 再看一眼报错所在的类ExposeInvocationInterceptor——也是一个拦截器
image.png
图片加载失败
invoke刚好把MethodInvocation设置进当前上下文进程类。 因此只需多往advice链里注册个ExposeInvocationInterceptor即可。 注意由于是链式调用,需要先注册这个interceptor,再注册advice。 下面以调用TemplatesImpl#newTransformer为例

image.png
图片加载失败


2 条评论
某人
表情
可输入 255
1989430851060349
2025-04-24 20:44 0 回复
lvbanjiaju111
1989430851060349
2025-04-24 20:43 0 回复
方便加个绿泡泡么?