這篇文章主要講解了“實(shí)現(xiàn)Java探針中遇到的問題有哪些”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“實(shí)現(xiàn)Java探針中遇到的問題有哪些”吧!
陽春網(wǎng)站制作公司哪家好,找成都創(chuàng)新互聯(lián)公司!從網(wǎng)頁設(shè)計(jì)、網(wǎng)站建設(shè)、微信開發(fā)、APP開發(fā)、成都響應(yīng)式網(wǎng)站建設(shè)等網(wǎng)站項(xiàng)目制作,到程序開發(fā),運(yùn)營(yíng)維護(hù)。成都創(chuàng)新互聯(lián)公司自2013年創(chuàng)立以來到現(xiàn)在10年的時(shí)間,我們擁有了豐富的建站經(jīng)驗(yàn)和運(yùn)維經(jīng)驗(yàn),來保證我們的工作的順利進(jìn)行。專注于網(wǎng)站建設(shè)就選成都創(chuàng)新互聯(lián)公司。
Java探針可以在Java應(yīng)用運(yùn)行時(shí)毫無感知的切入應(yīng)用代碼,是一種用于監(jiān)聽代碼行為或改變代碼行為的工具。
分布式調(diào)用鏈路追蹤的實(shí)現(xiàn)無非兩種方式,代碼侵入式和非代碼侵入式,基于Java探針實(shí)現(xiàn)的屬于非代碼侵入式。
運(yùn)行在Java虛擬機(jī)上的編程語言所編寫的代碼,都有一種統(tǒng)一的中間格式:class文件格式。實(shí)現(xiàn)動(dòng)態(tài)修改class字節(jié)碼插入額外行為的代碼,可實(shí)現(xiàn)非代碼侵入式的應(yīng)用調(diào)用行為收集。
得益于Java SE 6提供的Instrumentation接口?;贗nstrumentation可開發(fā)運(yùn)行時(shí)修改class字節(jié)碼的Java Agent應(yīng)用(Java探針),可在類加載之前替換類的字節(jié)碼、或在類加載之后通過重新加載類方式修改類的字節(jié)碼。
只是實(shí)現(xiàn)運(yùn)行時(shí)修改class字節(jié)碼還不足以稱為“探針”?;贗nstrumentation開發(fā)的Java Agent,只需要在Java應(yīng)用啟動(dòng)命令上加上虛擬機(jī)參數(shù)“-javaagent”指定Java Agent應(yīng)用jar包的位置,而不需要在工程項(xiàng)目中引入其jar包,即可將探針插入應(yīng)用代碼的各個(gè)角落。通過與應(yīng)用使用不同的類加載實(shí)現(xiàn)環(huán)境隔離,讓人有種Java Agent是吸附在應(yīng)用上運(yùn)行的錯(cuò)覺。
Instrumentation之所以難駕馭,在于需要了解Java類加載機(jī)制以及字節(jié)碼,一不小心就能遇到各種陌生的Exception。筆者在實(shí)現(xiàn)Java探針時(shí)就踩過不少坑,其中一類就是類加載相關(guān)的問題,也是本篇所要跟大家分享的。
由父類加載器加載的類,不能引用子類加載器加載的類,否則會(huì)拋出NoClassDefFoundError。
怎么理解這句話呢?這其實(shí)也是道面試題。
JDK提供的java.*
類都由啟動(dòng)類加載器加載。如果我們?cè)趈ava agent中修改java包下的類,插入調(diào)用logback打印日記的代碼,結(jié)果會(huì)怎樣?由于java agent包下的logback由AppClassLoader(應(yīng)用類加載器,也稱為系統(tǒng)類加載器)加載,而加載java包下的類的啟動(dòng)類加載器是AppClassLoader的父類加載器,在java包下的類中插入調(diào)用logback打印日記的代碼,首先在加載java包下的類時(shí),jvm會(huì)查看啟動(dòng)類加載器有沒有加載過這個(gè)類,如果沒有加載過嘗試加載,但啟動(dòng)類加載器加載不了logback包的類,而啟動(dòng)類加載器不會(huì)向子類加載器去詢問,任何類加載器都不會(huì)向子類加載器詢問子類加載器是否能加載,即使子類加載器加載了這個(gè)類。所以就會(huì)出現(xiàn)NoClassDefFoundError。
如果非要修改java包下的類,且非要在java包下的類中訪問項(xiàng)目中我們編寫的類或者第三方j(luò)ar包提供的類、或者我們編寫的javaagent包下的類,如何避免NoClassDefFoundError呢?
筆者遇到這個(gè)問題網(wǎng)上找過很多資源,遺憾的是并未找到。于是筆者想起自己電腦上下載有Arthas的源碼,不如學(xué)習(xí)下Arthas是如何解決的。
Arthas是Alibaba開源的一款Java診斷工具,非常適合用于線上問題排查。
參考Alibaba開源的Arthas的解決方案:
用于接收埋點(diǎn)代碼上報(bào)事件的類(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:方法執(zhí)行之前上報(bào);
complete:方法return之前或者拋出異常之前上報(bào),當(dāng)方法拋出異常時(shí),第一個(gè)參數(shù)為異常,否則第一個(gè)參數(shù)為返回值;
將Spy放在一個(gè)獨(dú)立的jar包下,在premain、agentmain方法中調(diào)用Instrumentation的appendToBootstrapClassLoaderSearch方法,將Spy類所在的jar包交由啟動(dòng)類加載器掃描加載,如下代碼所示。
// agent-spy.jar
String agentSpyJar = jarPath[1];
File spyJarFile = new File(agentSpyJar);
instrumentation.appendToBootstrapClassLoaderSearch(new JarFile(spyJarFile));
在Spy類中打印類加載器,如果打印的結(jié)果為null,則說明Spy類是由啟動(dòng)類加載器加載的。
public final class Spy {
static {
System.out.println("Spy class loader is " + Spy.class.getClassLoader());
}
//.......
}
最后,給Spy注入上報(bào)方法,在Spy中通過反射調(diào)用上報(bào)方法,完整的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) {
}
}
}
}
通過反射調(diào)用對(duì)性能會(huì)有所影響,特別是調(diào)用鏈路上每個(gè)方法都需要反射調(diào)用兩個(gè)上報(bào)方法。
可能不完全理解正確,但筆者試過這個(gè)方案確實(shí)可行。
為什么要實(shí)現(xiàn)隔離?
隔離是避免Agent污染應(yīng)用自身,使開發(fā)Java Agent無需考慮引入的jar包是否與目標(biāo)應(yīng)用引入的jar包沖突。
Java Agent與Spring Boot應(yīng)用相遇時(shí)會(huì)發(fā)生什么?
Spring Boot應(yīng)用打包后,將Agent附著到應(yīng)用啟動(dòng)可能會(huì)拋出醒目的NoClassDefFoundError異常,這在IDEA中測(cè)試是不會(huì)發(fā)生的,而背后的原因是Agent與打包后的Spring Boot應(yīng)用使用了不同的類加載器。
我們可能會(huì)在Agent中調(diào)用被監(jiān)控的SpringBoot應(yīng)用的代碼,也可能調(diào)用Agent依賴的第三方j(luò)ar包的API,而這些jar包恰好在SpringBoot應(yīng)用中也有導(dǎo)入,就可能會(huì)出現(xiàn)NoClassDefFoundError。
Agent的jar包由AppClassLoader類加載器(系統(tǒng)類加載器)所加載。
在IDEA中,項(xiàng)目的class文件和第三方庫是通過AppClassLoader加載的,而使用-javaagent指定的jar也是通過AppClassLoader加載,所以在idea中測(cè)試不會(huì)遇到這個(gè)問題。
SpringBoot應(yīng)用打包后,JVM進(jìn)程啟動(dòng)入口不再是我們寫的main方法,而是SpringBoot生成的啟動(dòng)類。SpringBoot使用自定義的類加載器(LaunchedClassLoader)加載jar中的類和第三方j(luò)ar包中的類,該類加載器的父類加載器為AppClassLoader。
也就是說,SpringBoot應(yīng)用打包后,加載javaagent包下的類使用的類加載器是SpringBoot使用的類加載器的父類加載器。
如何實(shí)現(xiàn)隔離?
讓加載agent包不使用AppClassLoader加載器加載,而是使用自定義的類加載器加載。
參考Alibaba開源的Arthas的實(shí)現(xiàn),自定義URLClassLoader加載agent包以及agent依賴的第三方j(luò)ar包。
由于premain或者agentmain方法所在的類由jvm使用AppClassLoader所加載,所以必須將agent拆分為兩個(gè)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;
}
// 優(yōu)先從parent(SystemClassLoader)里加載系統(tǒng)類,避免拋出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);
}
}
同時(shí)在構(gòu)造方法中指定OnionClassLoader的父類加載器為AppClassLoader的父類加載器。
ClassLoader.getSystemClassLoader()
:獲取系統(tǒng)類加載器(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、根據(jù)agent-core.jar所在絕對(duì)路徑構(gòu)造OnionClassLoader;
2、加載agent-core.jar下的ClassFileTransformer;
3、使用反射創(chuàng)建ClassFileTransformer實(shí)例;
4、將ClassFileTransformer添加到Instrumentation;
OnionClassFileTransformer類所依賴的agent-core包下的類,自然也會(huì)被使用OnionClassLoader類加載器加載,包括agent-core依賴的第三方j(luò)ar包。
生成分布式調(diào)用鏈日記的難點(diǎn)在于方法埋點(diǎn)和方法調(diào)用日記串連。
分布式調(diào)用鏈日記串連的方式有多種,筆者采用的是最簡(jiǎn)單的方式:打點(diǎn)id+打點(diǎn)時(shí)間。
對(duì)于同進(jìn)程內(nèi)的同線程,可用打點(diǎn)id將調(diào)用的方法串連起來,根據(jù)打點(diǎn)時(shí)間與一個(gè)累加器的值排序方法調(diào)用日記。
對(duì)于不同進(jìn)程,通過傳遞打點(diǎn)id可將不同應(yīng)用的打點(diǎn)日記串連起來,根據(jù)打點(diǎn)時(shí)間排序。
例如,適配webmvc框架的目的是從請(qǐng)求頭獲取調(diào)用來源傳遞過來的打點(diǎn)ID(事務(wù)ID)。對(duì)DispatcherServlet#doDispatch方法插樁,從HttpServletRequest參數(shù)獲取請(qǐng)求頭“S-Tid”?!癝-Tid”是自定義的請(qǐng)求頭參數(shù),用于傳遞打點(diǎn)ID。
筆者在實(shí)現(xiàn)適配webmvc和openfeign時(shí)都遇到了同樣的問題,如在適配webmvc時(shí),修改DispatcherServlet的doDispatch方法時(shí),asm框架拋出java.lang.TypeNotPresentException。
java.lang.TypeNotPresentException:當(dāng)應(yīng)用程序試圖使用表示類型名稱的字符串對(duì)類型進(jìn)行訪問,但無法找到帶有指定名稱的類型定義時(shí),拋出該異常。
其原因是,使用asm框架改寫DispatcherServlet類時(shí),asm會(huì)使用Class.forName方法加載符號(hào)引用的類,如果加載不到目標(biāo)類則拋出TypeNotPresentException。
默認(rèn)asm會(huì)使用加載自身的類加載器去嘗試加載當(dāng)前改寫類所依賴的一些類,而加載asm框架使用的類加載器與加載agent-core包使用的是同一個(gè)類加載器,DispatcherServlet則由SpringBoot的LaunchedClassLoader類加載器所加載。
好在ClassFileTransformer#transform方法傳遞了用于加載當(dāng)前類的類加載器:
public class OnionClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className,Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,byte[] classfileBuffer) {
// ......
}
}
如果當(dāng)前需要改寫的類是DispatcherServlet,則transform方法的第一個(gè)參數(shù)為即將用于加載DispatcherServlet類的類加載器;
我們只需要指定asm使用ClassFileTransformer#transform方法傳遞進(jìn)來的類加載器加載DispatcherServlet依賴的類即可。
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES) {
@Override
protected ClassLoader getClassLoader() {
return loader;
}
};
如代碼所示,我們重寫asm的ClassWriter類的getClassLoader方法,返回的類加載器是ClassFileTransformer#transform方法傳遞進(jìn)來的類加載器。
感謝各位的閱讀,以上就是“實(shí)現(xiàn)Java探針中遇到的問題有哪些”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對(duì)實(shí)現(xiàn)Java探針中遇到的問題有哪些這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是創(chuàng)新互聯(lián),小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
文章標(biāo)題:實(shí)現(xiàn)Java探針中遇到的問題有哪些
鏈接URL:http://jinyejixie.com/article34/pggcse.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供搜索引擎優(yōu)化、虛擬主機(jī)、網(wǎng)頁設(shè)計(jì)公司、全網(wǎng)營(yíng)銷推廣、服務(wù)器托管、定制開發(fā)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)