如何脚踏实地构建Java Agent
在構(gòu)建Plumbr的多年中,我們遇到了許多具有挑戰(zhàn)性的問題。 在其他方面,使Plumbr Java Agent可靠地執(zhí)行而又不危害客戶的應(yīng)用程序,是一個特別棘手的任務(wù)。 從實(shí)時系統(tǒng)中安全地收集所有需要的遙測會帶來很多問題。 其中一些非常簡單,而另一些則非常不明顯。
在此博客文章中,我們想與您分享一些示例,這些示例演示了在為我們的探員需要處理的一些看似簡單的方面提供支持時遇到的復(fù)雜性。 這些示例進(jìn)行了一些簡化,但摘錄自我們前一段時間需要解決的現(xiàn)實(shí)問題。 實(shí)際上,這些只是等待嘗試使用字節(jié)碼工具或JVMTI的人的冰山一角。
示例1:檢測一個簡單的Web應(yīng)用程序
讓我們從一個非常簡單的hello world網(wǎng)絡(luò)應(yīng)用開始 :
@Controller public class HelloWorldController {@RequestMapping("/hello")@ResponseBodyString hello() {return "Hello, world!";} }如果啟動應(yīng)用程序并訪問相關(guān)的控制器,則會看到以下內(nèi)容:
$ curl localhost:8080/hello Hello, world!作為簡單的練習(xí),讓我們將返回值更改為“ Hello,transformed world”。 自然,我們真正的Java代理不會對您的應(yīng)用程序執(zhí)行此類操作:我們的目標(biāo)是在不更改觀察到的行為的情況下進(jìn)行監(jiān)視。 但是為了使這個演示簡短而簡潔,請耐心等待。 要更改返回的響應(yīng),我們將使用ByteBuddy :
public class ServletAgent {public static void premain(String arguments, Instrumentation instrumentation) { // (1)new AgentBuilder.Default().type(isSubTypeOf(Servlet.class)) // (2).transform((/* … */) ->builder.method(named("service")) // (3).intercept(MethodDelegation.to(Interceptor.class) // (4))).installOn(instrumentation); // (5)}}這里發(fā)生了什么事:
las,如果我們嘗試運(yùn)行此命令,則應(yīng)用程序?qū)⒉辉賳?#xff0c;并引發(fā)以下錯誤:
java.lang.NoSuchMethodError: javax.servlet.ServletContext.getVirtualServerName()Ljava/lang/String;at org.apache.catalina.authenticator.AuthenticatorBase.startInternal(AuthenticatorBase.java:1137)at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)發(fā)生了什么? 我們只觸摸了“ Servlet”類上的“ service”方法,但是現(xiàn)在JVM無法在另一個類上找到另一個方法。 腥。 讓我們嘗試看看在這兩種情況下該類的加載位置。 為此,我們可以將-XX:+ TraceClassLoading參數(shù)添加到JVM啟動腳本中。 如果沒有Java代理,則從Tomcat加載有問題的類:
[Loaded javax.servlet.ServletContext from jar:file:app.jar!/BOOT-INF/lib/tomcat-embed-core-8.5.11.jar!/]但是,如果再次啟用Java代理,則會從其他位置加載它:
[Loaded javax.servlet.ServletContext from file:agent.jar]啊哈! 實(shí)際上,我們的代理直接依賴于Gradle構(gòu)建腳本中定義的servlet API:
agentCompile "javax.servlet:servlet-api:2.5"可悲的是,該版本與Tomcat期望的版本不匹配,因此出現(xiàn)錯誤。 我們用這種依賴性指定哪些類儀器:isSubTypeOf(Servlet 類 ),但是這也造成了我們加載的servlet庫的不兼容版本。 要擺脫這種情況實(shí)際上并不那么容易:要檢查我們嘗試檢測的類是否是另一種類型的子類型,我們必須知道其所有父類或接口。
盡管有關(guān)直接父代的信息存在于字節(jié)碼中,但傳遞繼承卻不存在。 實(shí)際上,在進(jìn)行檢測時,相關(guān)的類甚至可能尚未加載。 要解決此問題,我們必須在運(yùn)行時找出客戶端應(yīng)用程序的整個類層次結(jié)構(gòu)。 有效地收集類層次結(jié)構(gòu)是一項(xiàng)艱巨的任務(wù),它本身就有很多陷阱,但是這里的教訓(xùn)很明顯:規(guī)范不應(yīng)加載客戶端應(yīng)用程序可能也要加載的類,尤其是來自不兼容版本的類。
這只是一條小小的龍,當(dāng)您嘗試使用字節(jié)碼或嘗試與類加載器混為一談時,它已遠(yuǎn)離軍團(tuán)等待著您。 我們已經(jīng)看到了許多其他問題:類加載死鎖,驗(yàn)證程序錯誤,多個代理之間的沖突,本機(jī)JVM結(jié)構(gòu)膨脹,您好!
但是,我們的代理并不限于使用Instrumentation API。 要實(shí)現(xiàn)某些功能,我們必須更深入。
示例2:使用JVMTI收集有關(guān)類的信息
有多種方法可以弄清類型層次結(jié)構(gòu),但在本文中,我們僅關(guān)注其中一種-JVMTI (JVM工具接口)。 它使我們能夠編寫一些本機(jī)代碼,以訪問JVM的更底層的遙測和工具功能。 除其他外,可以為應(yīng)用程序或JVM本身中發(fā)生的各種事件訂閱JVMTI回調(diào)。 我們當(dāng)前感興趣的是ClassLoad回調(diào)。 這是一個如何使用它來訂閱類加載事件的示例 :
static void register_class_loading_callback(jvmtiEnv* jvmti) {jvmtiEventCallbacks callbacks;jvmtiError error;memset(&callbacks, 0, sizeof(jvmtiEventCallbacks));callbacks.ClassLoad = on_class_loaded;(*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(callbacks));(*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_CLASS_LOAD, (jthread)NULL); }這將使JVM在類加載的早期階段執(zhí)行我們定義的on_class_loaded函數(shù)。 然后,我們可以編寫此函數(shù),以便它通過JNI調(diào)用代理的java方法,如下所示:
void JNICALL on_class_loaded(jvmtiEnv *jvmti, JNIEnv* jni, jthread thread, jclass klass) {(*jni)->CallVoidMethod(jni, agent_in_java, on_class_loaded_method, klass); }為了簡單起見,在Java Agent中,我們將只打印類的名稱:
public static void onClassLoaded(Class clazz) {System.out.println("Hello, " + clazz); }閉上你的眼睛一分鐘,嘗試想象這里可能出什么問題。
你們中許多人可能以為這將崩潰。 畢竟,您在本機(jī)代碼中犯的每個錯誤都有可能通過段錯誤使整個應(yīng)用程序崩潰。 但是,在這個特定示例中,我們實(shí)際上將獲得一些JNI錯誤和一個Java異常:
Error: A JNI error has occurred, please check your installation and try again Error: A JNI error has occurred, please check your installation and try again Hello, class java.lang.Throwable$PrintStreamOrWriter Hello, class java.lang.Throwable$WrappedPrintStream Hello, class java.util.IdentityHashMap Hello, class java.util.IdentityHashMap$KeySet Exception in thread "main" java.lang.NullPointerExceptionAt JvmtiAgent.onClassLoaded(JvmtiAgent.java:23)讓我們暫時將JNI錯誤放在一邊,然后集中討論Java異常。 真令人驚訝 在這里什么可以為空? 選項(xiàng)不多,所以讓我們檢查一下并再次運(yùn)行:
public static void onClassLoaded(Class clazz) {if(System.out == null) {throw new AssertionError("System.out is null");}if(clazz == null) {throw new AssertionError("clazz is null");}System.out.println("Hello, " + clazz); }但是,a,我們?nèi)匀粫龅较嗤漠惓?#xff1a;
Exception in thread "main" java.lang.NullPointerExceptionAt JvmtiAgent.onClassLoaded(JvmtiAgent.java:31)讓我們稍等一下,然后對代碼進(jìn)行另一個簡單的更改:
public static void onClassLoaded(Class clazz) {System.out.println("Hello, " + clazz.getSimpleName()); }輸出格式的這種看似微不足道的變化導(dǎo)致了行為上的巨大變化:
Error: A JNI error has occurred, please check your installation and try again Error: A JNI error has occurred, please check your installation and try again Hello, WrappedPrintWriter Hello, ClassCircularityError # # A fatal error has been detected by the Java Runtime Environment: # # Internal Error (systemDictionary.cpp:806), pid=82384, tid=0x0000000000001c03 # guarantee((!class_loader.is_null())) failed: dup definition for bootstrap loader?啊,終于崩潰了! 真高興! 實(shí)際上,這為我們提供了很多信息,有助于查明根本原因。 具體來說,現(xiàn)在明顯的ClassCircularityError和內(nèi)部錯誤消息非常明顯。 如果要查看JVM源代碼的相關(guān)部分,您會發(fā)現(xiàn)一種用于解析類的極其復(fù)雜且混雜的算法。 它確實(shí)可以單獨(dú)工作,但仍然很脆弱,但是很容易因做一些不尋常的事情而被破壞,例如重寫ClassLoader.loadClass或拋出一些JVMTI回調(diào)。
我們在這里所做的是將類加載潛入加載類的中間,這似乎是一項(xiàng)冒險的業(yè)務(wù)。 跳過故障排除過程,而該故障排除過程將自己撰寫一篇博客文章,涉及很多本機(jī)挖掘工作,讓我們僅概述第一個示例中發(fā)生的事情:
好吧,那很復(fù)雜。 但是畢竟,JVMTI文檔非常明確地說我們應(yīng)該格外小心:
“此事件是在加載課程的早期階段發(fā)送的。 因此,該類應(yīng)謹(jǐn)慎使用。 請注意,例如,方法和字段尚未加載,因此對方法,字段,子類等的查詢不會給出正確的結(jié)果。 請參見Java語言規(guī)范中的“類和接口的加載”。 對于大多數(shù)目的, ClassPrepare 事件將更加有用。”
確實(shí),如果我們使用此回調(diào),那么就不會有這樣的困難。 但是,在設(shè)計(jì)用于監(jiān)視目的的Java代理時,有時會被迫進(jìn)入JVM的非常暗的區(qū)域以支持我們所需的產(chǎn)品功能,而開銷卻足以用于生產(chǎn)部署。
帶走
這些示例說明了一些看似無辜的設(shè)置和天真的方法來構(gòu)建Java代理如何以令人驚訝的方式讓您大吃一驚。 實(shí)際上,以上內(nèi)容幾乎不涉及我們多年來發(fā)現(xiàn)的內(nèi)容。
再加上數(shù)量眾多的不同平臺,此類代理將需要完美運(yùn)行(不同的JVM供應(yīng)商,不同的Java版本,不同的操作系統(tǒng)),并且本來就很復(fù)雜的任務(wù)變得更具挑戰(zhàn)性。
但是,通過盡職調(diào)查和適當(dāng)?shù)谋O(jiān)視,構(gòu)建可靠的Java代理是一項(xiàng)可以由一組敬業(yè)工程師解決的任務(wù)。 我們在自己的產(chǎn)品中自信地運(yùn)行Plumbr Agent,并且不會因此而睡不著。
翻譯自: https://www.javacodegeeks.com/2017/06/shoot-foot-building-java-agent.html
總結(jié)
以上是生活随笔為你收集整理的如何脚踏实地构建Java Agent的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 天融信防火墙配置
- 下一篇: c#编译时提高兼容性_幻像类型提高了编译