本文旨在簡單粗暴體驗instrumentation attach模式的玩法贞让,給讀者一個直觀的體驗,概念方面不多介紹
場景
有一個spring的http接口定義如下筋夏,每次調用返回一個隨機uuid,此處的RandomUtil
采用的hutool
的工具類基公。
@GetMapping("/play")
@ResponseBody
public String health() {
return RandomUtil.simpleUUID();
}
期望通過編寫一個agent,attach到當前進程實現串改程序邏輯轰豆,每次調用都返回hello
胰伍。
開發(fā)一個agent jar
入口函數
先要寫一個agentmain
函數,類似我們寫helloword的main函數
public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException, InterruptedException {
//注冊字節(jié)碼轉換邏輯
inst.addTransformer(new PlayClassFileTransformer(), true);
//使之生效
inst.retransformClasses(RandomUtil.class);
System.out.println("Agent Main Done");
}
字節(jié)碼轉換邏輯
函數內部注冊了PlayClassFileTransformer
, 內部實現邏輯:
- 如果不是
RandomUtil
,則返回null
,表示不作替換 - 否則替換新的字節(jié)碼內容酸休,字節(jié)碼內容來自本地提前準備好的一個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é)碼是我提前準備好的斑司,基于hutool的源碼隨便改了一筆渗饮,把simpleUUID函數的返回值改為了hello。
retransformClasses
注冊完轉換器后宿刮,替換邏輯執(zhí)行的時機需要依賴于此互站,所以需要手動執(zhí)行這個函數,否則替換邏輯是不生效的糙置。以下引用一小段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();
}
}
}
效果體驗
略