本文旨在简单粗暴体验instrumentation attach模式的玩法,给读者一个直观的体验,概念方面不多介绍
场景
有一个spring的http接口定义如下,每次调用返回一个随机uuid,此处的RandomUtil
采用的hutool
的工具类。
@GetMapping("/play")
@ResponseBody
public String health() {
return RandomUtil.simpleUUID();
}
期望通过编写一个agent,attach到当前进程实现串改程序逻辑,每次调用都返回hello
。
开发一个agent jar
入口函数
先要写一个agentmain
函数,类似我们写helloword的main函数
public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException, InterruptedException {
//注册字节码转换逻辑
inst.addTransformer(new PlayClassFileTransformer(), true);
//使之生效
inst.retransformClasses(RandomUtil.class);
System.out.println("Agent Main Done");
}
字节码转换逻辑
函数内部注册了PlayClassFileTransformer
, 内部实现逻辑:
- 如果不是
RandomUtil
,则返回null
,表示不作替换 - 否则替换新的字节码内容,字节码内容来自本地提前准备好的一个class文件
public class PlayClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (!"cn/hutool/core/util/RandomUtil".equals(className)){
return null;
}
return getBytesFromFile("/Users/***/code/***/instrument-play/docs/RandomUtil.class");
}
public static byte[] getBytesFromFile(String fileName) {
try {
// precondition
File file = new File(fileName);
InputStream is = new FileInputStream(file);
long length = file.length();
byte[] bytes = new byte[(int) length];
// Read in the bytes
int offset = 0;
int numRead = 0;
while (offset <bytes.length
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
offset += numRead;
}
if (offset < bytes.length) {
throw new IOException("Could not completely read file "
+ file.getName());
}
is.close();
return bytes;
} catch (Exception e) {
System.out.println("error occurs in _ClassTransformer!"
+ e.getClass().getName());
return null;
}
}
}
注意: 此处字节码是我提前准备好的,基于hutool的源码随便改了一笔,把simpleUUID函数的返回值改为了hello。
retransformClasses
注册完转换器后,替换逻辑执行的时机需要依赖于此,所以需要手动执行这个函数,否则替换逻辑是不生效的。以下引用一小段ClassFileTransformer
的注释:
the transformer will be called for every new class definition and every class redefinition.
另外,此处因为需要指定需要retransform的类型,所以agent的工程里也引入了对hutool的依赖:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>4.1.3</version>
<scope>provided</scope>
</dependency>
manifest文件
jar包生成后需要manifest文件,所以添加如下maven配置:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifestEntries>
<Agent-Class>org.example.instrument.AgentMain</Agent-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<goals>
<goal>attached</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
打包
mvn clean package
ATTACH
接下来需要把agent jar attach到目标进程上去。
此处我们假设目标进程,已启动,进程号为8888,则attach的代码如下:
public static void main(String[] args) throws InterruptedException, IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
VirtualMachine vmObj = null;
try {
vmObj = VirtualMachine.attach("8888");
if (vmObj != null) {
vmObj.loadAgent("<jar path>/instrument-play-1.0-SNAPSHOT-jar-with-dependencies.jar", null);
}
} finally {
if (null != vmObj) {
vmObj.detach();
}
}
}
效果体验
略