java agent_如何脚踏实地构建Java Agent
java agent
在構建Plumbr的多年中,我們遇到了許多具有挑戰性的問題。 在其他方面,使Plumbr Java Agent可靠地執行而不會危及客戶的應用程序,是一個特別棘手的問題。 從實時系統中安全地收集所有需要的遙測會帶來很多問題。 其中一些非常簡單,而另一些則非常不明顯。
在此博客文章中,我們想與您分享一些示例,這些示例演示了在為我們的探員需要處理的一些看似簡單的方面提供支持時遇到的復雜性。 這些示例進行了簡化,但摘錄自我們前一段時間需要解決的現實問題。 實際上,這些只是等待嘗試使用字節碼工具或JVMTI的人的冰山一角。
示例1:檢測一個簡單的Web應用程序
讓我們從一個非常簡單的hello world網絡應用開始 :
@Controller public class HelloWorldController {@RequestMapping("/hello")@ResponseBodyString hello() {return "Hello, world!";} }如果啟動應用程序并訪問相關的控制器,則會看到以下內容:
$ curl localhost:8080/hello Hello, world!作為一個簡單的練習,讓我們將返回值更改為“ Hello,transformed world”。 自然,我們真正的Java代理不會對您的應用程序執行此類操作:我們的目標是在不更改觀察到的行為的情況下進行監視。 但是為了使這個演示簡短而簡潔,請與我們聯系。 要更改返回的響應,我們將使用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)}}這里發生了什么事:
las,如果我們嘗試運行此命令,則應用程序將不再啟動,并引發以下錯誤:
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)發生了什么? 我們只觸摸了“ Servlet”類上的“ service”方法,但是現在JVM無法在另一個類上找到另一個方法。 腥。 讓我們嘗試看看在兩種情況下該類的加載位置。 為此,我們可以將-XX:+ TraceClassLoading參數添加到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]啊哈! 實際上,我們的代理直接依賴于Gradle構建腳本中定義的servlet API:
agentCompile "javax.servlet:servlet-api:2.5"可悲的是,此版本與Tomcat期望的版本不匹配,因此出現錯誤。 我們用這種依賴性指定哪些類儀器:isSubTypeOf(Servlet 類 ),但是這也造成了我們加載的servlet庫的不兼容版本。 要擺脫這種情況實際上并不那么容易:要檢查我們嘗試檢測的類是否是另一種類型的子類型,我們必須知道其所有父類或接口。
盡管有關直接父代的信息存在于字節碼中,但傳遞繼承卻不存在。 實際上,在進行檢測時,相關的類甚至可能尚未加載。 要解決此問題,我們必須在運行時找出客戶端應用程序的整個類層次結構。 有效地收集類層次結構是一項艱巨的任務,它本身就有很多陷阱,但是這里的教訓很明顯:規范不應加載客戶端應用程序可能也要加載的類,尤其是來自不兼容版本的類。
這只是一條小小的龍,當您嘗試使用字節碼或嘗試與類加載器混為一談時,它已遠離軍團等待著您。 我們已經看到了許多其他問題:類裝入死鎖,驗證程序錯誤,多個代理之間的沖突,本機JVM結構膨脹,就這樣吧!
但是,我們的代理并不限于使用Instrumentation API。 要實現某些功能,我們必須更深入。
示例2:使用JVMTI收集有關類的信息
可以采用多種不同方法來確定類型層次結構,但是在本文中,我們僅關注其中之一-JVMTI (JVM工具接口)。 它使我們能夠編寫一些本機代碼,以訪問JVM的更底層的遙測和工具功能。 除其他外,可以為應用程序或JVM本身中發生的各種事件訂閱JVMTI回調。 我們當前感興趣的是ClassLoad回調。 這是一個如何使用它來訂閱類加載事件的示例 :
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在類加載的早期階段執行我們定義的on_class_loaded函數。 然后,我們可以編寫此函數,以便它通過JNI調用代理的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); }閉上你的眼睛一分鐘,嘗試想象這里可能出什么問題。
你們中許多人可能認為這將崩潰。 畢竟,您在本機代碼中犯的每個錯誤都有可能通過段錯誤使整個應用程序崩潰。 但是,在這個特定示例中,我們實際上將獲得一些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異常。 真令人驚訝 在這里什么可以為空? 選項不多,所以讓我們檢查一下并再次運行:
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,我們仍然會遇到相同的異常:
Exception in thread "main" java.lang.NullPointerExceptionAt JvmtiAgent.onClassLoaded(JvmtiAgent.java:31)讓我們稍等一下,然后對代碼進行另一個簡單的更改:
public static void onClassLoaded(Class clazz) {System.out.println("Hello, " + clazz.getSimpleName()); }輸出格式的這種看似微不足道的變化導致了行為上的巨大變化:
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?啊,終于崩潰了! 真高興! 實際上,這為我們提供了很多信息,有助于查明根本原因。 具體來說,現在明顯的ClassCircularityError和內部錯誤消息非常明顯。 如果要查看JVM源代碼的相關部分,您會發現一個非常復雜且混合在一起的算法,用于解析類。 它確實可以單獨運行,但仍然很脆弱,但是通過執行一些不尋常的操作(如覆蓋ClassLoader.loadClass或拋出一些JVMTI回調)很容易被破壞。
我們在這里所做的是將類加載潛入加載類的中間,這似乎是一項冒險的業務。 跳過故障排除過程,而該故障排除過程將自己撰寫一篇博客文章,涉及很多本機挖掘工作,讓我們僅概述第一個示例中發生的事情:
好吧,那很復雜。 但是畢竟,JVMTI文檔非常明確地表示我們應該格外小心:
“此事件是在加載課程的早期階段發送的。 因此,該類應謹慎使用。 請注意,例如,方法和字段尚未加載,因此對方法,字段,子類等的查詢不會給出正確的結果。 請參見Java語言規范中的“類和接口的加載”。 對于大多數目的, ClassPrepare 事件將更加有用?!?
確實,如果我們使用此回調,那么就不會有這樣的困難。 但是,在設計用于監視目的的Java代理時,有時會被迫進入JVM的非常暗的區域以支持我們所需的產品功能,而開銷卻足以降低生產部署的成本。
帶走
這些示例說明了一些看似無辜的設置和構建Java代理的幼稚方法如何以令人驚訝的方式讓您大吃一驚。 實際上,以上內容幾乎不涉及我們多年來發現的內容。
再加上數量眾多的不同平臺,此類代理將需要完美運行(不同的JVM供應商,不同的Java版本,不同的操作系統),并且本來就很復雜的任務變得更具挑戰性。
但是,通過盡職調查和適當的監視,構建可靠的Java代理是一項可以由一組敬業工程師解決的任務。 我們在自己的產品中自信地運行Plumbr Agent,并且不會因此而睡不著。
翻譯自: https://www.javacodegeeks.com/2017/06/shoot-foot-building-java-agent.html
java agent
總結
以上是生活随笔為你收集整理的java agent_如何脚踏实地构建Java Agent的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: gwt格式_GWT HTTP请求替代方案
- 下一篇: 创建文件夹Linux命令(创建文件夹li