您好,登錄后才能下訂單哦!
這篇文章主要講解了“實現Java探針中遇到的問題有哪些”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“實現Java探針中遇到的問題有哪些”吧!
Java探針可以在Java應用運行時毫無感知的切入應用代碼,是一種用于監聽代碼行為或改變代碼行為的工具。
分布式調用鏈路追蹤的實現無非兩種方式,代碼侵入式和非代碼侵入式,基于Java探針實現的屬于非代碼侵入式。
運行在Java虛擬機上的編程語言所編寫的代碼,都有一種統一的中間格式:class文件格式。實現動態修改class字節碼插入額外行為的代碼,可實現非代碼侵入式的應用調用行為收集。
得益于Java SE 6提供的Instrumentation接口。基于Instrumentation可開發運行時修改class字節碼的Java Agent應用(Java探針),可在類加載之前替換類的字節碼、或在類加載之后通過重新加載類方式修改類的字節碼。
只是實現運行時修改class字節碼還不足以稱為“探針”。基于Instrumentation開發的Java Agent,只需要在Java應用啟動命令上加上虛擬機參數“-javaagent”指定Java Agent應用jar包的位置,而不需要在工程項目中引入其jar包,即可將探針插入應用代碼的各個角落。通過與應用使用不同的類加載實現環境隔離,讓人有種Java Agent是吸附在應用上運行的錯覺。
Instrumentation之所以難駕馭,在于需要了解Java類加載機制以及字節碼,一不小心就能遇到各種陌生的Exception。筆者在實現Java探針時就踩過不少坑,其中一類就是類加載相關的問題,也是本篇所要跟大家分享的。
由父類加載器加載的類,不能引用子類加載器加載的類,否則會拋出NoClassDefFoundError。
怎么理解這句話呢?這其實也是道面試題。
JDK提供的java.*
類都由啟動類加載器加載。如果我們在java agent中修改java包下的類,插入調用logback打印日記的代碼,結果會怎樣?由于java agent包下的logback由AppClassLoader(應用類加載器,也稱為系統類加載器)加載,而加載java包下的類的啟動類加載器是AppClassLoader的父類加載器,在java包下的類中插入調用logback打印日記的代碼,首先在加載java包下的類時,jvm會查看啟動類加載器有沒有加載過這個類,如果沒有加載過嘗試加載,但啟動類加載器加載不了logback包的類,而啟動類加載器不會向子類加載器去詢問,任何類加載器都不會向子類加載器詢問子類加載器是否能加載,即使子類加載器加載了這個類。所以就會出現NoClassDefFoundError。
如果非要修改java包下的類,且非要在java包下的類中訪問項目中我們編寫的類或者第三方jar包提供的類、或者我們編寫的javaagent包下的類,如何避免NoClassDefFoundError呢?
筆者遇到這個問題網上找過很多資源,遺憾的是并未找到。于是筆者想起自己電腦上下載有Arthas的源碼,不如學習下Arthas是如何解決的。
Arthas是Alibaba開源的一款Java診斷工具,非常適合用于線上問題排查。
參考Alibaba開源的Arthas的解決方案:
用于接收埋點代碼上報事件的類(Spy):
public final class Spy {
public static void before(String className, String methodName, String descriptor, Object[] params) {
}
public static void complete(Object returnValueOrThrowable, String className, String methodName, String descriptor) {
}
}
before:方法執行之前上報;
complete:方法return之前或者拋出異常之前上報,當方法拋出異常時,第一個參數為異常,否則第一個參數為返回值;
將Spy放在一個獨立的jar包下,在premain、agentmain方法中調用Instrumentation的appendToBootstrapClassLoaderSearch方法,將Spy類所在的jar包交由啟動類加載器掃描加載,如下代碼所示。
// agent-spy.jar
String agentSpyJar = jarPath[1];
File spyJarFile = new File(agentSpyJar);
instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile));
在Spy類中打印類加載器,如果打印的結果為null,則說明Spy類是由啟動類加載器加載的。
public final class Spy {
static {
System.out.println("Spy class loader is " + Spy.class.getClassLoader());
}
//.......
}
最后,給Spy注入上報方法,在Spy中通過反射調用上報方法,完整的Spy類的代碼如下。
public final class Spy {
public static Method beforMethod;
public static Method completeMethod;
public static void before(String className, String methodName, String descriptor, Object[] params) {
if (beforMethod != null) {
try {
beforMethod.invoke(null, className, methodName, descriptor, params);
} catch (IllegalAccessException | InvocationTargetException e) {
}
}
}
public static void complete(Object returnValueOrThrowable, String className, String methodName, String descriptor) {
if (completeMethod != null) {
try {
completeMethod.invoke(null, returnValueOrThrowable, className, methodName, descriptor);
} catch (IllegalAccessException | InvocationTargetException e) {
}
}
}
}
通過反射調用對性能會有所影響,特別是調用鏈路上每個方法都需要反射調用兩個上報方法。
可能不完全理解正確,但筆者試過這個方案確實可行。
為什么要實現隔離?
隔離是避免Agent污染應用自身,使開發Java Agent無需考慮引入的jar包是否與目標應用引入的jar包沖突。
Java Agent與Spring Boot應用相遇時會發生什么?
Spring Boot應用打包后,將Agent附著到應用啟動可能會拋出醒目的NoClassDefFoundError異常,這在IDEA中測試是不會發生的,而背后的原因是Agent與打包后的Spring Boot應用使用了不同的類加載器。
我們可能會在Agent中調用被監控的SpringBoot應用的代碼,也可能調用Agent依賴的第三方jar包的API,而這些jar包恰好在SpringBoot應用中也有導入,就可能會出現NoClassDefFoundError。
Agent的jar包由AppClassLoader類加載器(系統類加載器)所加載。
在IDEA中,項目的class文件和第三方庫是通過AppClassLoader加載的,而使用-javaagent指定的jar也是通過AppClassLoader加載,所以在idea中測試不會遇到這個問題。
SpringBoot應用打包后,JVM進程啟動入口不再是我們寫的main方法,而是SpringBoot生成的啟動類。SpringBoot使用自定義的類加載器(LaunchedClassLoader)加載jar中的類和第三方jar包中的類,該類加載器的父類加載器為AppClassLoader。
也就是說,SpringBoot應用打包后,加載javaagent包下的類使用的類加載器是SpringBoot使用的類加載器的父類加載器。
如何實現隔離?
讓加載agent包不使用AppClassLoader加載器加載,而是使用自定義的類加載器加載。
參考Alibaba開源的Arthas的實現,自定義URLClassLoader加載agent包以及agent依賴的第三方jar包。
由于premain或者agentmain方法所在的類由jvm使用AppClassLoader所加載,所以必須將agent拆分為兩個jar包。核心功能放在agent-core包下,premain或者agentmain方法所在的類放在agent-boot包下。在premain或者agentmain方法中使用自定義的URLClassLoader類加載器加載agent-core。
第一步:
自定義類加載器OnionClassLoader,繼承URLClassLoader,如下代碼所示:
public class OnionClassLoader extends URLClassLoader {
public OnionClassLoader(URL[] urls) {
super(urls, ClassLoader.getSystemClassLoader().getParent());
}
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
final Class<?> loadedClass = findLoadedClass(name);
if (loadedClass != null) {
return loadedClass;
}
// 優先從parent(SystemClassLoader)里加載系統類,避免拋出ClassNotFoundException
if (name != null && (name.startsWith("sun.") || name.startsWith("java."))) {
return super.loadClass(name, resolve);
}
try {
Class<?> aClass = findClass(name);
if (resolve) {
resolveClass(aClass);
}
return aClass;
} catch (Exception e) {
// ignore
}
return super.loadClass(name, resolve);
}
}
同時在構造方法中指定OnionClassLoader的父類加載器為AppClassLoader的父類加載器。
ClassLoader.getSystemClassLoader()
:獲取系統類加載器(AppClassLoader)
第二步:
在premain或者agentmain方法中使用OnionClassLoader類加載器加載agent-core。
// 1
File agentJarFile = new File(agentJar);
final ClassLoader agentLoader = new OnionClassLoader(new URL[]{agentJarFile.toURI().toURL()});
// 2
Class<?> transFormer = agentLoader.loadClass("com.msyc.agent.core.OnionClassFileTransformer");
// 3
Constructor<?> constructor = transFormer.getConstructor(String.class);
Object instance = constructor.newInstance(opsParams);
// 4
instrumentation.addTransformer((ClassFileTransformer) instance);
1、根據agent-core.jar所在絕對路徑構造OnionClassLoader;
2、加載agent-core.jar下的ClassFileTransformer;
3、使用反射創建ClassFileTransformer實例;
4、將ClassFileTransformer添加到Instrumentation;
OnionClassFileTransformer類所依賴的agent-core包下的類,自然也會被使用OnionClassLoader類加載器加載,包括agent-core依賴的第三方jar包。
生成分布式調用鏈日記的難點在于方法埋點和方法調用日記串連。
分布式調用鏈日記串連的方式有多種,筆者采用的是最簡單的方式:打點id+打點時間。
對于同進程內的同線程,可用打點id將調用的方法串連起來,根據打點時間與一個累加器的值排序方法調用日記。
對于不同進程,通過傳遞打點id可將不同應用的打點日記串連起來,根據打點時間排序。
例如,適配webmvc框架的目的是從請求頭獲取調用來源傳遞過來的打點ID(事務ID)。對DispatcherServlet#doDispatch方法插樁,從HttpServletRequest參數獲取請求頭“S-Tid”。“S-Tid”是自定義的請求頭參數,用于傳遞打點ID。
筆者在實現適配webmvc和openfeign時都遇到了同樣的問題,如在適配webmvc時,修改DispatcherServlet的doDispatch方法時,asm框架拋出java.lang.TypeNotPresentException。
java.lang.TypeNotPresentException:當應用程序試圖使用表示類型名稱的字符串對類型進行訪問,但無法找到帶有指定名稱的類型定義時,拋出該異常。
其原因是,使用asm框架改寫DispatcherServlet類時,asm會使用Class.forName方法加載符號引用的類,如果加載不到目標類則拋出TypeNotPresentException。
默認asm會使用加載自身的類加載器去嘗試加載當前改寫類所依賴的一些類,而加載asm框架使用的類加載器與加載agent-core包使用的是同一個類加載器,DispatcherServlet則由SpringBoot的LaunchedClassLoader類加載器所加載。
好在ClassFileTransformer#transform方法傳遞了用于加載當前類的類加載器:
public class OnionClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,byte[] classfileBuffer) {
// ......
}
}
如果當前需要改寫的類是DispatcherServlet,則transform方法的第一個參數為即將用于加載DispatcherServlet類的類加載器;
我們只需要指定asm使用ClassFileTransformer#transform方法傳遞進來的類加載器加載DispatcherServlet依賴的類即可。
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES) {
@Override
protected ClassLoader getClassLoader() {
return loader;
}
};
如代碼所示,我們重寫asm的ClassWriter類的getClassLoader方法,返回的類加載器是ClassFileTransformer#transform方法傳遞進來的類加載器。
感謝各位的閱讀,以上就是“實現Java探針中遇到的問題有哪些”的內容了,經過本文的學習后,相信大家對實現Java探針中遇到的問題有哪些這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。