java web源代码_检测Java Web应用程序而无需修改其源代码
java web源代碼
與其他系統進行交互時,大多數Java Web應用程序都使用標準Java接口。 諸如Web頁面或REST服務器之類的基于HTTP的服務是使用接口javax.servlet.Servlet來實現的。 使用JDBC接口java.sql.Statement和java.sql.Connection實現數據庫交互。 這些標準幾乎是通用的,與基礎框架(Spring或Java EE)和Servlet容器(Tomcat,Wildfly等)無關。
本文介紹如何實現Java代理,該Java代理使用Bytecode操作來掛接到這些接口,并收集有關HTTP和數據庫調用的頻率和持續時間的度量。 演示代碼可在https://github.com/fstab/promagent上找到 ,該代碼是為Prometheus監視系統檢測Java Web應用程序的代理。 但是,本文不是Prometheus特有的,它著重于Java代理,字節碼操作和類加載器等基礎技術。
1. Java代理
Java代理是可以附加到JVM以便操作Java字節碼的Java程序。 例如,Java代理可用于修改接口javax.servlet.Servlet所有實現,以獲取有關HTTP調用的數量和持續時間的統計信息。
Java代理作為JAR文件提供。 常規Java程序使用main()方法作為應用程序的入口點,而Java代理具有premain()方法,該方法將在應用程序的main()方法之前調用:
Java代理概述
public class MyAgent {public static void premain(String agentArgs, Instrumentation inst) throws Exception {// ...} }雖然可執行的JAR文件有一個MANIFEST.MF文件中指定Main-Class ,代理JAR文件有一個MANIFEST.MF文件中指定的Premain-Class 。 可以使用命令行選項-javaagent:在應用程序啟動期間附加代理:
Java代理命令行
java -javaagent:myagent.jar -jar myapp.jar然后, premain()方法可以調用inst.addTransformer()來注冊ClassFileTransformer 。 類文件轉換器實現了每當加載Java類時都會調用的transform()方法。 它可以檢查和修改任何Java類的字節碼,以添加其他功能。
2.字節碼操作
有幾個可用的庫可幫助Java開發人員實現字節碼操作。 最低級別的是ASM 。 其他庫,例如cglib和javassist提供更高級別的API。 最新,最易于使用的庫是Byte Buddy 。 它提供了易于閱讀的流暢Java API,用于創建ClassFileTransformer并將其注冊到Instrumentation :
字節伙伴代理示例
package io.promagent;import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.agent.builder.AgentBuilder.Transformer; import net.bytebuddy.matcher.ElementMatchers;import java.lang.instrument.Instrumentation;import static net.bytebuddy.matcher.ElementMatchers.hasSuperType; import static net.bytebuddy.matcher.ElementMatchers.named;public class MyAgent {public static void premain(String agentArgs, Instrumentation inst) throws Exception {new AgentBuilder.Default().type(hasSuperType(named("javax.servlet.Servlet"))).transform(new Transformer.ForAdvice().include(MyAgent.class.getClassLoader()).advice(ElementMatchers.named("service"), "io.promagent.MyAdvice")).installOn(inst);}上面的示例顯示了檢測所有javax.servlet.Servlet實現的service()方法所需的完整代碼。 每當Servlet處理Web請求時,都會調用service()方法。 MyAdvice類定義將注入到Servlet的service()方法中的代碼。 這段代碼使用@Advice.OnMethodEnter和@Advice.OnMethodExit :
字節好友建議示例
public class MyAdvice {@Advice.OnMethodEnterpublic static void before(ServletRequest request, ServletResponse response) {System.out.println("before serving the request...");}@Advice.OnMethodExitpublic static void after(ServletRequest request, ServletResponse response) {System.out.println("after serving the request...");} }Byte Buddy提供了兩種檢測方法:建議(如上所示)和攔截器。 區別是微妙的:有了建議, @Advice.OnMethodEnter和@Advice.OnMethodExit方法的字節碼被復制到被攔截方法的開始和最終塊中。 效果與將代碼復制并粘貼到要攔截的service()實現中相同。 結果,在完成檢測之后,不再使用類MyAdvice 。 截獲的service()方法不需要訪問MyAdvice類,可以在MyAdvice類不可用的類加載器上下文中執行。
另一方面,攔截器是常規方法調用,它們在被攔截方法的開始和最終塊中執行。 這意味著被攔截的方法必須在攔截器可用的上下文中執行。
在以下各節中,我們將看到可能會限制應用程序服務器環境中類的可見性,這就是Promagent使用Advices而非Interceptor的原因。
3.添加依賴項
為了使上面的示例有用,我們需要用代碼維護指標并將指標提供給監視系統來替換System.out.println()消息。 例如, Promagent使用Prometheus客戶端庫來維護和公開Prometheus指標。
JVM自動將使用-javaagent:命令行參數指定的JAR文件添加到應用程序的系統類加載器。 因此,從理論上講,應該可以創建一個包含代理及其所有依賴項的Uber JAR ,并在-javaagent:命令行參數中使用它。
但是,在應用程序服務器環境中,使所有依賴項在系統類加載器上可用都是有問題的,原因有兩個:
- 某些代理的依賴項可能與應用程序服務器內部使用的庫或WAR文件中作為已部署應用程序的一部分提供的庫發生沖突。
- 為了防止沖突,應用程序服務器限制了從系統類加載器對類的訪問。 例如,除非使用jboss.modules.system.pkgs系統屬性顯式公開了受影響的程序包,否則Wildfly模塊無法從系統類加載器訪問類。 跟蹤所有依賴關系并相應地配置模塊系統并非易事。
更好的方法是只公開幾個Java類,而無需外部依賴系統類加載器,并使用自定義類加載器加載實際的度量實現。 這樣可以最大程度地減少潛在的沖突以及運行代理所需的配置。
4.從自定義類加載器加載鉤子
在Java中實現自定義類加載器很容易,因為我們可以簡單地使用java.net.URLClassLoader并使用指向我們的類所在的JAR文件的路徑對其進行初始化。 為了使代理易于使用, Promagent隨包含其他JAR文件的JAR文件一起提供。 內部JAR文件在啟動時被復制到臨時目錄,并且使用臨時路徑配置了自定義類加載器。 這樣,用戶將獲得一個單一的代理JAR,而該代理在內部區分系統類加載器上的類(這些類直接包含在代理JAR中)和定制類加載器上的類(這些類是從JAR中加載的)臨時目錄)。
實際的工具在稱為hook的類中實現。 掛鉤是從自定義類加載器加載的。 這樣,只要自定義類加載器能夠提供這些依賴關系,鉤子就可以引用其所需的任何依賴關系。 例如, ServletHook如下所示:
自定義鉤子類示例
public class ServletHook {public void before(ServletRequest request, ServletResponse response) {// ...}public void after(ServletRequest request, ServletResponse response) {// ...} }該鉤子看起來與Byte Buddy建議類似。 區別在于Byte Buddy建議僅是幾行代碼,這些代碼具有最小的依賴性,以便從自定義類加載器加載相應的鉤子,并通過反射將其委派給鉤子的before()和after()方法。 字節好友建議對檢測庫沒有任何依賴關系,因為實際的檢測庫僅在自定義類加載器中可見。
但是,在加載鉤子時有一個細微的陷阱:參數ServletRequest和ServletResponse將從已檢測的Servlet傳遞。 這意味著,掛鉤中的ServletRequest和ServletResponse類必須使用與攔截的Servlet相同的類加載器進行加載,否則我們無法將Servlet的參數傳遞到掛鉤的before()和after()方法中。
解決方案是使用Thread.currentThread().getContextClassLoader()作為自定義類加載器的父級。 這樣,可以從上下文類加載器加載的所有類都將從上下文類加載器加載。 這包括ServletRequest和ServletResponse 。 只有當前上下文中不可用的類(例如鉤子本身及其依賴項)才會從自定義JAR文件中加載。 這意味著我們每個上下文需要一個自定義類加載器,因為每個自定義類加載器都委托給另一個上下文類加載器作為其父代。
5.實施全球指標注冊
使用到目前為止描述的實現,可以檢測單個Web應用程序。 但是,如果應用程序服務器上有多個部署,則每個工具將具有自己的類加載器。 從不同的類加載器加載指標庫時,部署無法共享在該指標庫中定義的全局靜態變量。 例如,不可能跨多個部署使用Prometheus客戶端庫隨附的全局度量標準注冊表。 缺少全局注冊表,每個部署都需要獨立維護和公開其指標。
解決此問題的一種方法是擴展自定義類加載器,并使其委托將共享度量庫加載到另一個共享的自定義類加載器。 但是,JVM還附帶有一個內置的全局注冊表,我們可以將其用作VM范圍的指標存儲:JMX平臺MBean服務器。 將度量標準注冊為MBean具有以下好處:
- 全局注冊表:JMX平臺MBean服務器提供了VM范圍的注冊表,使我們能夠維護一組全局指標,以對應用程序服務器上的所有部署進行檢測。
- 監視系統的單個導出器:易于實現一個小型Web應用程序,該應用程序從MBean服務器讀取所有度量并將其提供給監視系統。 例如, Promagent包含用于將指標導出到Prometheus服務器的WAR部署。
- JMX工具:由于所有指標都可以作為MBean使用,因此可以使用任何JMX客戶端來了解指標的狀態。
JMX平臺MBean服務器是Java SE的一部分,可以通過靜態方法ManagementFactory.getPlatformMBeanServer()進行訪問。 在MBean服務器上注冊的Java對象稱為MBean。 MBean必須在一個接口中定義其公共可訪問的API,按照慣例,該接口的名稱類似于Java類,并附加了后綴MBean 。 例如,要將Counter類注冊為MBean,該類必須實現一個名為CounterMBean的接口。 每個MBean均可通過唯一的ObjectName尋址。 可以使用MBeanServer.invoke()來調用MBean接口中定義的方法。
6.總結
本文概述了如何在不修改Java Web應用程序源代碼的情況下對其進行檢測。 它基于Promagent ,該工具使用Prometheus指標對Java應用程序進行檢測。 但是,本文重點介紹了諸如Java代理, 字節好友字節碼操作庫之類的基礎技術,在諸如Wildfly之類的應用服務器環境中的類加載以及JMX平臺MBean服務器。
最好從Promagent示例代碼中總結出一些松散的結局,例如如何避免HTTP請求經過多個Servlet鏈時重復計數。 有關更多示例,值得研究相關項目,例如inspectIT或stagemonitor 。
翻譯自: https://www.javacodegeeks.com/2017/07/instrumenting-java-web-applications-without-modifying-source-code.html
java web源代碼
總結
以上是生活随笔為你收集整理的java web源代码_检测Java Web应用程序而无需修改其源代码的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java分布式系统开发_从微服务到分布式
- 下一篇: 吐血推荐珍藏的Chrome插件