java探針利用了javaAgent + ASM字節碼注入工具實現了動態修改類文件的功能。像skywalking和arthas都使用到了這個技術。
具體原理為:
jdk1.5以后引入了javaAgent技術,javaAgent是運行方法之前的攔截器。我們利用javaAgent和ASM字節碼技術,在JVM加載class二進制文件的時候,利用ASM動態的修改加載的class文件,在監控的方法前后添加計時器功能,用于計算監控方法耗時,同時將方法耗時及內部調用情況放入處理器,處理器利用棧先進后出的特點對方法調用先后順序做處理,當一個請求處理結束后,將耗時方法軌跡和入參map輸出到文件中,然后根據map中相應參數或耗時方法軌跡中的關鍵代碼區分出我們要抓取的耗時業務。最后將相應耗時軌跡文件取下來,轉化為xml格式并進行解析,通過瀏覽器將代碼分層結構展示出來,方便耗時分析。
上篇我們介紹了JavaAgent的基本使用,下面介紹如何去動態的修改類的字節碼文件,這個才是agent實現更強大功能的核心所在!
Instrumentation接口
Instrumentation接口位于jdk1.6包java.lang.instrument包下,Instrumentation指的是可以獨立于應用程序之外的代理程序,可以用來監控和擴展JVM上運行的應用程序,相當于是JVM層面的AOP。
功能:
監控和擴展JVM上的運行程序,它可以替換和修改java類的字節碼以便采集數據,用于監控,性能統計,覆蓋率分析,事件記錄等。可以用在程序啟動時,也可以用于程序運行時動態attach。
比如說一個Java程序在JVM上運行,這時如果需要監控JVM的狀態,除了使用JDK自帶的jps等命令之外,就可以通過instrument來更直觀的獲取JVM的運行情況;
或者一個Java方法在JVM中執行,如果我想獲取這個方法的執行時間又不想改代碼,常用的做法是通過Spring的AOP來實現,而AOP通過面向切面編程,而instrument是在JVM層面上直接改動java方法來實現。
public interface Instrumentation{void addTransformer(ClassFileTransformer transformer
, boolean canRetransform
);void addTransformer(ClassFileTransformer transformer
);boolean removeTransformer(ClassFileTransformer transformer
);boolean isRetransformClassesSupported();void redefineClasses(ClassDefinition
... definitions
)throws ClassNotFoundException
, UnmodifiableClassException
;boolean isModifiableClass(Class
<?> theClass
);@SuppressWarnings("rawtypes")Class
[] getAllLoadedClasses();@SuppressWarnings("rawtypes")Class
[] getInitiatedClasses(ClassLoader loader
);long getObjectSize(Object objectToSize
);void appendToBootstrapClassLoaderSearch(JarFile jarfile
);void appendToSystemClassLoaderSearch(JarFile jarfile
);boolean isNativeMethodPrefixSupported();void setNativeMethodPrefix(ClassFileTransformer transformer
, String prefix
);
}
要是定義了操作java類的class文件方法,這里又涉及到了ClassFileTransformer接口,這個接口的作用是改變Class文件的字節碼,返回新的字節碼數組,源碼如下:
public interface ClassFileTransformer{byte[] transform(ClassLoader loader
, String className
, Class
<?> classBeingRedefined
,ProtectionDomain protectionDomain
, byte[] classfileBuffer
) throws IllegalClassFormatException
;
}
ClassFileTransformer接口只有一個方法,就是改變指定類的Class文件,該接口沒有默認實現,很顯然如果需要改變Class文件的內容,需要改成什么樣需要使用者自己來實現。
如:
import javassist
.ClassPool
;
import javassist
.CtClass
;
import javassist
.CtMethod
;
import javassist
.CtNewMethod
;import java
.lang
.instrument
.ClassFileTransformer
;
import java
.lang
.instrument
.IllegalClassFormatException
;
import java
.security
.ProtectionDomain
;
import java
.util
.ArrayList
;
import java
.util
.HashMap
;
import java
.util
.List
;
import java
.util
.Map
;public class MyTransformer implements ClassFileTransformer {final static String prefix
= "\nlong startTime = System.currentTimeMillis();\n";final static String postfix
= "\nlong endTime = System.currentTimeMillis();\n";final static Map
<String
, List
<String>> methodMap
= new HashMap<>();public MyTransformer() {add("com.jun.sail.myservice.service.HelloService.say");add("com.jun.sail.myservice.service.HelloService.say2");}private void add(String methodString
) {String className
= methodString
.substring(0, methodString
.lastIndexOf("."));String methodName
= methodString
.substring(methodString
.lastIndexOf(".") + 1);List
<String> list
= methodMap
.computeIfAbsent(className
, k
-> new ArrayList<>());list
.add(methodName
);}@Overridepublic byte[] transform(ClassLoader loader
, String className
, Class
<?> classBeingRedefined
,ProtectionDomain protectionDomain
, byte[] classfileBuffer
) {className
= className
.replace("/", ".");if (methodMap
.containsKey(className
)) { CtClass ctclass
= null
;try {ctclass
= ClassPool
.getDefault().get(className
);for (String methodName
: methodMap
.get(className
)) {String outputStr
= "\nSystem.out.println(\"this method [" + methodName
+ "] cost:\" +(endTime - startTime) +\"ms.\");";CtMethod ctmethod
= ctclass
.getDeclaredMethod(methodName
);CtMethod newMethod
= CtNewMethod
.copy(ctmethod
, methodName
, ctclass
, null
);String oldMethodName
= methodName
+ "$old";ctmethod
.setName(oldMethodName
);StringBuilder bodyStr
= new StringBuilder();bodyStr
.append("{");bodyStr
.append(prefix
);bodyStr
.append(oldMethodName
).append("($$);\n");bodyStr
.append(postfix
);bodyStr
.append(outputStr
);bodyStr
.append("}");newMethod
.setBody(bodyStr
.toString());newMethod
.setName(methodName
);ctclass
.addMethod(newMethod
);}return ctclass
.toBytecode();} catch (Exception e
) {System
.out
.println("AAAAA" + e
.getMessage());e
.printStackTrace();}}return null
;}
}
然后在permain或agentmain方法中inst.addTransformer(new MyTransformer());,其他步驟同之前,不再贅述。
Instrumentation接口相當于一個代理,當執行premain方法時,通過Instrumentation提供的API可以動態的添加管理JVM加載的Class文件,Instrumentation管理著ClassFileTransformer。
ClassFileTransformer接口可以動態的改變Class文件的字節碼,在加載字節碼的時候可以將字節碼進行動態修改,具體實現需要自定義實現類來實現ClassFileTransformer接口
Java字節碼生成框架大致有ASM、Javassist和byte buddy三種
-
ASM框架介紹及使用
ASM是一種Java字節碼操控框架,能夠以二進制形式修改已有的類或是生成類,ASM可以直接生成二進制class文件也可以在類被加載入JVM之前動態改變類,只不過ASM在創建class字節碼時說底層JVM的匯編指令,需要使用者對class組織結構和JVM匯編指令有一定的了解。由于Java 類存儲在.class文件中,這些類文件中包含有:類名稱、方法、屬性及字節碼,ASM從類文件中讀入信息后改變類行為、分析類信息或者直接創建新的類。
著名的使用到ASM的案例便是lambda表達式、CGLIB動態代理類
ASM框架核心類包含
ClassReader:該類用來解析編譯過的class字節碼文件
ClassWriter:該類用來重新構建編譯后的類,比如修改類名、屬性、方法或者根據要求創建新的字節碼文件
ClassAdapter:實現了ClassVisitor接口,將對它的方法調用委托給另一個ClassVisitor對象
-
Javassist及使用
Javassit相比于ASM要簡單點,Javassit提供了更高級的API,當時執行效率上比ASM要差,因為ASM上直接操作的字節碼。功能和JDK自帶的反射功能類似,但是比反射要強大。
Javassist核心類包括ClassPool:
一個基于HashMap實現的CtClass對象容器,key上類名,value上這個類的CtClass對象
CtClass:表示一個類,可以從ClassPool中獲取
CtMethods:表示一個類的方法
CtFields:表示類中的屬性
-
Byte Buddy及使用
byte buddy是一個提供了API用于生成任意Java類工具包,可以生成和修改字節碼。
3. Instrumentation的實現原理
說起Instrumentation的原理,就不得不先提起JVMTI:
JVMTI官網文檔
JVMTI
JVMTI 是JVM Tool Interface 的縮寫,是 JVM 暴露出來給用戶擴展使用的接口集合,JVMTI 是基于事件驅動的,JVM每執行一定的邏輯就會調用一些事件的回調接口,這些接口可以給用戶自行擴展來實現自己的邏輯。JVMTI是實現 Debugger、Profiler、Monitor、Thread Analyser 和coverage analysis等工具的統一基礎,在主流 Java 虛擬機中都有實現。
JVMTIAgent
JVMTI 是一套本地代碼接口,因此使用 JVMTI 需要我們與 C/C++ 以及 JNI 打交道。事實上,開發時一般采用建立一個 Agent 的方式來使用 JVMTI,它使用 JVMTI 函數,設置一些回調函數,并從 Java 虛擬機中得到當前的運行態信息,并作出自己的判斷,最后還可能操作虛擬機的運行態。把 Agent 編譯成一個動態鏈接庫之后,我們就可以在 Java 程序啟動的時候來加載它(啟動加載模式)
主要有三個函數:
- Agent_OnLoad方法:如果agent是在啟動時加載的,那么在JVM啟動過程中會執行這個agent里的Agent_OnLoad函數
- Agent_OnAttach方法:如果agent不是在啟動時加載的,而是attach到目標程序上,然后通過load命令來加載agent,由ClassFileLoadHook event提供回調,調用Agent_OnAttach方法
- Agent_OnUnload方法:在agent卸載時調用
回到主題,Instrument 就是一種 JVMTIAgent,它實現了Agent_OnLoad和Agent_OnAttach兩個方法,也就是在使用時,Instrument既可以在啟動時加載,也可以在運行時動態加載
- 啟動時加載就是在啟動時添加JVM參數:-javaagent:XXXAgent.jar的方式
- 運行時加載是通過JVM的attach機制來實現,通過發送load命令來加載,這種方式明顯更加靈活,對監控目標啟動也無限制,arthas的attach就是基于此
private void attachAgent(Configure configure
) throws Exception
{VirtualMachineDescriptor virtualMachineDescriptor
= null
;for (VirtualMachineDescriptor descriptor
: VirtualMachine
.list()) {String pid
= descriptor
.id();if (pid
.equals(Integer
.toString(configure
.getJavaPid()))) {virtualMachineDescriptor
= descriptor
;}}VirtualMachine virtualMachine
= null
;try {if (null
== virtualMachineDescriptor
) { virtualMachine
= VirtualMachine
.attach("" + configure
.getJavaPid());} else {virtualMachine
= VirtualMachine
.attach(virtualMachineDescriptor
);}Properties targetSystemProperties
= virtualMachine
.getSystemProperties();String targetJavaVersion
= targetSystemProperties
.getProperty("java.specification.version");String currentJavaVersion
= System
.getProperty("java.specification.version");if (targetJavaVersion
!= null
&& currentJavaVersion
!= null
) {if (!targetJavaVersion
.equals(currentJavaVersion
)) {AnsiLog
.warn("Current VM java version: {} do not match target VM java version: {}, attach may fail.",currentJavaVersion
, targetJavaVersion
);AnsiLog
.warn("Target VM JAVA_HOME is {}, try to set the same JAVA_HOME.",targetSystemProperties
.getProperty("java.home"));}}virtualMachine
.loadAgent(configure
.getArthasAgent(),configure
.getArthasCore() + ";" + configure
.toString());} finally {if (null
!= virtualMachine
) {virtualMachine
.detach();}}}
通過 VirtualMachine , 可以attach到當前指定的jvm pid上,然后 virtualMachine.loadAgent()將編寫好的agent用于監控目標。
總結:
Instrumentation相當于一個JVM級別的AOP
Instrumentation在JVM啟動的時候監聽事件,如類加載事件,JVM觸發來指定的事件通過回調通知,并創建一個 Instrumentation接口的實例,然后找到MANIFEST.MF中配置的實現了premain方法的Class,然后將Instrumentation實例傳入premain方法中
premain方法會在main方法之前執行,可以添加ClassFileTransfer來實現對Class文件字節碼的動態修改(并不會修改Class文件中的字節碼,而是修改已經被JVM加載的字節碼)
修改字節碼的技術可以使用開源的 ASM、javassist、byteBuddy等
https://blog.csdn.net/u010862794/article/details/87773434
總結
以上是生活随笔為你收集整理的java探针之修改类字节码文件的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。