摘要:java-agent是應用於java的trace工具,核心是對JVMTI(JVM Tool Interface)的調用。
本文分享自華為雲社區《Java動態trace技術:java-agent》,原文作者:技術火炬手 。
動態trace技術是在應用部署之后監控程序的調用,獲取其中的變量內容,甚至可以插入或替換部分代碼。業界的trace工具很多,ptrace,strace,eBPF,btrace,java-agent等等。這次應用的目的是監控kafka服務中publish與consume的調用,獲取依賴關系。鑒於kafka是通過Scala語言編寫,所以采用了java-agent技術。
java-agent是應用於java的trace工具,核心是對JVMTI(JVM Tool Interface)的調用。JVMTI是java虛擬機對外開放的一系列接口函數,通過JVMTI可以獲取java虛擬機當前運行的狀態。java-agent程序運行時會在java虛擬機中掛載一個agent進程,通過JVMTI監控所掛載的java應用。通過agent程序可以完成java代碼的熱替換,類加載的過程監控等功能。
java-agent的掛載方式有兩種,一種是靜態掛載,一種是動態掛載。靜態掛載中,agent與java應用一起啟動,在java應用初始化前agent就已經掛載完成,並開始監控java應用。動態掛載則是在應用運行過程中,通過進程ID確定掛載對象,動態的將agent掛載在目標進程上。
靜態掛載
首先編寫java-agent的監控程序,靜態掛載的入口函數為premain。premain函數有兩種,區別是傳入參數不同。通常選擇帶有Instrumentation參數,可以使用該變量完成代碼的熱替換。
public static void premain(String agentArgs, Instrumentation inst); public static void premain(String agentArgs);
下面是一個簡單的例子。在premain函數中,使用Instrumentation增加一個transformer。當監控的java應用每次加載class的時候都會調用transformer。DefineTransformer是一個transformer,是ClassFileTransformer的實現。在它的transform函數的入參中會給出當前加載的類名,類加載器等信息。樣例中我們只是打印了加載的類名。
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import javassist.*; public class PreMain { public static void premain(String agentArgs, Instrumentation inst) { System.out.println("agentArgs : " + agentArgs); inst.addTransformer(new DefineTransformer(), true); } static class DefineTransformer implements ClassFileTransformer{ @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer){ System.out.println("premain load Class:" + className); return classfileBuffer; } } }
運行java-agent需要將上述程序打包成一個jar文件,在jar文件的MANIFEST.MF中需要包含以下幾項
Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.huawei.PreMain
Premain-Class聲明了這個jar的premain函數所在的類,java-agent加載jar包時會在PreMain類中尋找premain。Can-Redefine-Classes與Can-Retransform-Classes聲明為true,表示允許這段程序修改java應用的代碼。
如果你是使用Maven的項目,可以使用增加下面的插件來自動添加MANIFEST.MF
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>2.6</version> <configuration> <appendAssemblyId>false</appendAssemblyId> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Premain-Class>com.huawei.PreMain</Premain-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive> </configuration> <executions> <execution> <id>assemble-all</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>
輸出jar文件之后,編寫一個hello world的java應用編譯為hello.class,在啟動應用時使用如下命令
java -javaagent:/root/Java-Agent-Project-Path/target/JavaAgentTest-1.0-SNAPSHOT.jar hello
在執行中就可以打印java虛擬機在運行hello.class所加載的所有類。
java-agent的功能不僅限於輸出類的加載過程,通過下面這個樣例可以實現代碼的熱替換。首先編寫一個測試類。
public class App { public static void main( String[] args ) { try{ System.out.println( "main start!" ); App test = new App(); int x1 = 1; int x2 = 2; while(true){ System.out.println(Integer.toString(test.add(x1, x2))); Thread.sleep(2000); } } catch (InterruptedException e) { e.printStackTrace(); System.out.println("main end"); } } private int add(int x1, int x2){ return x1+x2; } }
然后我們修改PreMain類中transformer,並通過Instrumentation添加這個transformer。與DefineTransformer一樣。
static class MyClassTransformer implements ClassFileTransformer { @Override public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) { // 如果當前加載的類是我們編寫的測試類,進入修改。 if ("com/huawei/App".equals(className)) { try { // 從ClassPool獲得CtClass對象 final ClassPool classPool = ClassPool.getDefault(); final CtClass clazz = classPool.get("com.huawei.App"); //打印App類中的所有成員函數 CtMethod[] methodList = clazz.getDeclaredMethods(); for(CtMethod method: methodList){ System.out.println("premain method: "+ method.getName()); } // 獲取add函數並替換,$1表示函數的第一個入參 CtMethod convertToAbbr = clazz.getDeclaredMethod("add"); String methodBody = "{return $1 + $2 + 11;}"; convertToAbbr.setBody(methodBody); // 在add函數體之前增加一段代碼,同理也可以在函數尾部添加 String methodBody = "System.out.println(Integer.toString($1));"; convertToAbbr.insertBefore(methodBody); // 返回字節碼,並且detachCtClass對象 byte[] byteCode = clazz.toBytecode(); //detach的意思是將內存中曾經被javassist加載過的Date對象移除,如果下次有需要在內存中找不到會重新走javassist加載 clazz.detach(); return byteCode; } catch (Exception ex) { ex.printStackTrace(); } } // 如果返回null則字節碼不會被修改 return null; } }
之后的步驟與之前相同,運行會發現add函數的邏輯已經被替換了。
動態掛載
動態掛載是在應用運行過程中動態的添加agent。技術原理是通過socket與目標進程通訊,發送load指令在目標進程掛載指定jar文件。agent執行過程中的功能與靜態過載是完全相同的。在實施過程中,有幾點不同。首先入口函數名不同,動態掛載的函數名是agentmain。與premain類似,有兩種格式。但通常采用帶有Instrumentation的那種。如下例所示
public class AgentMain { public static void agentmain(String agentArgs, Instrumentation instrumentation) throws UnmodifiableClassException { instrumentation.addTransformer(new MyClassTransformer(), true); instrumentation.retransformClasses(com.huawei.Test.class); } static class MyClassTransformer implements ClassFileTransformer { @Override public byte[] transform(final ClassLoader loader, final String className, final Class<?> classBeingRedefined, final ProtectionDomain protectionDomain, final byte[] classfileBuffer) { // 如果當前加載的類是我們編寫的測試類,進入修改。 if ("com/huawei/App".equals(className)) { try { // 從ClassPool獲得CtClass對象 final ClassPool classPool = ClassPool.getDefault(); final CtClass clazz = classPool.get("com.huawei.App"); //打印App類中的所有成員函數 CtMethod[] methodList = clazz.getDeclaredMethods(); for(CtMethod method: methodList){ System.out.println("premain method: "+ method.getName()); } // 獲取add函數並替換,$1表示函數的第一個入參 CtMethod convertToAbbr = clazz.getDeclaredMethod("add"); String methodBody = "{return $1 + $2 + 11;}"; convertToAbbr.setBody(methodBody); // 返回字節碼,並且detachCtClass對象 byte[] byteCode = clazz.toBytecode(); //detach的意思是將內存中曾經被javassist加載過的Date對象移除,如果下次有需要在內存中找不到會重新走javassist加載 clazz.detach(); return byteCode; } catch (Exception ex) { ex.printStackTrace(); } } // 如果返回null則字節碼不會被修改 return null; } } }
功能與靜態加載相同。需要注意的是,Instrumentation增加了transformer之后,調用了retransformClasses函數。這是由於transformer只有在Java虛擬機加載class時才會調用。如果是通過動態加載的方式,需要監控的class文件可能已經加載完成了。所以需要調用retransformClasses重新加載。
另外一點不同是MANIFEST.MF文件需要添加Agent-Class,如下所示
Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.huawei.PreMain Agent-Class: com.huawei.AgentMain
最后一點不同是加載方式不同。動態掛載需要編寫一個加載腳本。如下所示,在這段腳本中,首先遍歷所有的java進程,通過啟動類名辨識需要監控的進程。通過進程id獲取VirtualMachine實例,並加載agentmain的jar文件。
import com.sun.tools.attach.*; import java.io.IOException; import java.util.List; public class TestAgentMain { public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException{ //獲取當前系統中所有 運行中的 虛擬機 System.out.println("running JVM start "); List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { System.out.println(vmd.displayName()); String aim = "com.huawei.App"; if (vmd.displayName().endsWith(aim)) { System.out.println(String.format("find %s, process id %s", vmd.displayName(), vmd.id())); VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); virtualMachine.loadAgent("/root/Java-Agent-Project-Path/target/JavaAgentTest-1.0-SNAPSHOT.jar"); virtualMachine.detach(); } } } }
Scala程序監控
Scala與Java兼容性很好,所以使用java-agent監控scala應用也是可行的。但是仍然需要注意一些問題。第一點是程序替換只對class有作用,對object是無效的。第二個問題是,動態替換中是將程序編譯為字節碼之后再去替換的。java-agent使用的是java的編譯規則,所以替換程序要使用java的語言規則,否則會出現編譯錯誤。例如示例中使用System.out.println輸出參數信息,如果使用scala的println會出現編譯錯誤。
參考資料: