本文旨在簡(jiǎn)單粗暴體驗(yàn)instrumentation attach模式的玩法,給讀者一個(gè)直觀的體驗(yàn),概念方面不多介紹
場(chǎng)景
有一個(gè)spring的http接口定義如下,每次調(diào)用返回一個(gè)隨機(jī)uuid,此處的RandomUtil采用的hutool的工具類。
@GetMapping("/play")
@ResponseBody
public String health() {
return RandomUtil.simpleUUID();
}
期望通過(guò)編寫一個(gè)agent,attach到當(dāng)前進(jìn)程實(shí)現(xiàn)串改程序邏輯,每次調(diào)用都返回hello。
開發(fā)一個(gè)agent jar
入口函數(shù)
先要寫一個(gè)agentmain函數(shù),類似我們寫helloword的main函數(shù)
public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException, InterruptedException {
//注冊(cè)字節(jié)碼轉(zhuǎn)換邏輯
inst.addTransformer(new PlayClassFileTransformer(), true);
//使之生效
inst.retransformClasses(RandomUtil.class);
System.out.println("Agent Main Done");
}
字節(jié)碼轉(zhuǎn)換邏輯
函數(shù)內(nèi)部注冊(cè)了PlayClassFileTransformer, 內(nèi)部實(shí)現(xiàn)邏輯:
- 如果不是
RandomUtil,則返回null,表示不作替換 - 否則替換新的字節(jié)碼內(nèi)容,字節(jié)碼內(nèi)容來(lái)自本地提前準(zhǔn)備好的一個(gè)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;
}
}
}
注意: 此處字節(jié)碼是我提前準(zhǔn)備好的,基于hutool的源碼隨便改了一筆,把simpleUUID函數(shù)的返回值改為了hello。
retransformClasses
注冊(cè)完轉(zhuǎn)換器后,替換邏輯執(zhí)行的時(shí)機(jī)需要依賴于此,所以需要手動(dòng)執(zhí)行這個(gè)函數(shù),否則替換邏輯是不生效的。以下引用一小段ClassFileTransformer的注釋:
the transformer will be called for every new class definition and every class redefinition.
另外,此處因?yàn)樾枰付ㄐ枰猺etransform的類型,所以agent的工程里也引入了對(duì)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
接下來(lái)需要把a(bǔ)gent jar attach到目標(biāo)進(jìn)程上去。
此處我們假設(shè)目標(biāo)進(jìn)程,已啟動(dòng),進(jìn)程號(hào)為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();
}
}
}
效果體驗(yàn)
略