Java是一種編程語言,相信很多熟知Java的同學都會碰到一種常見的問題那就是在Java中運用動態(tài)掛載的過程中經(jīng)常會出現(xiàn)很多bug,那么針對性這些我們常見的bug我們應該如何解決呢?以下是小編整理的遇到bug時候要怎么解決的資料,一起來看看吧!
大多數(shù)JVM具備Java的HotSwap特性,大部分開發(fā)者認為它僅僅是一個調試工具。利用這一特性,有可能在不重啟Java進程條件下,改變Java方法的實現(xiàn)。典型的例子是使用IDE來編碼。然而HotSwap可以在生產(chǎn)環(huán)境中實現(xiàn)這一功能。通過這種方式,不用停止運行程序,就可以擴展在線的應用程序,或者在運行的項目上修復小的錯誤。這篇文章中,我將演示動態(tài)綁定、應用運行期代碼變化進行綁定、介紹一些工具API以及ByteBuddy庫,這個庫提供了一些API代碼改變更方便。
假設有一個正在運行的應用程序,通過校驗HTTP請求中的X-Priority頭部,來執(zhí)行服務器的特殊處理。該校驗使用下面的工具類來實現(xiàn):
classHeaderUtility{staticbooleanisPriorityCall(HttpServletRequestrequest){returnrequest.getHeader("X-Pirority")!=null;}}
你發(fā)現(xiàn)錯誤了嗎?這樣的錯誤很常見,尤其是在測試代碼中常量值分解為靜態(tài)字段重用。在不太理想的情況下,這個錯誤只會在產(chǎn)品被安裝的時候才被發(fā)現(xiàn),其中頭通過另外一個應用生成并沒有拼寫錯誤。
修復這樣的錯誤并不難。在持續(xù)交付的時代,重新部署一個新的版本只需要點擊一下按鈕。但在其他情況下,變更可能就不是那么簡單了,重新部署過程可能比較復雜,其中停機是不允許的,帶著錯誤運行可能會比較好。但HotSwap給我們提供了另外一種選擇:在不重啟應用的前提下進行小幅改動。
為了修改一個運行中的Java程序,我們首先需要一種可以同處在運行狀態(tài)的JVM進行通信的方式。因為Java的虛擬機實現(xiàn)是一個受到管理的系統(tǒng),因此擁有進行這些操作的標準API。提問中涉及到的API被稱作attachmentAPI,它是官方Java工具的一部分。使用這個由運行之中的JVM所暴露的API,能讓第二個Java進程來同其進行通信。
事實上,我們已經(jīng)用到了該API:它已經(jīng)由諸如VisualVM或者JavaMissionControl這樣的調試和模擬工具進行了應用。應用這些附件的API并沒有同日常使用的標準JavaAPI打在一起,而是被打到了一個特殊的文件之中,叫做tools.jar,它只含了一個虛擬機的JDK打發(fā)布版本。更糟糕的是,這個JAR文件的位置并沒有進行設置,它在Windows、Linux,特別是在Macintosh上的VM都存在差別,不光文件的位置,連文件名也各異,有些發(fā)行版上就被叫做classes.jar。最后,IBM甚至決定對這個JAR中含的一些類的名稱進行修改,將所有com.sun類挪到com.ibm命名空間之中,又添了一個亂子。在Java9中,亂糟糟的狀態(tài)才最終得以清理,tools.jar被Jigsaw的模塊jdk.attach所替代。
在對API的JAR(或者模塊)進行了定位之后,我們就該讓其對附件進程可用。在OpenJDK上,被用來連接到另外一個JVM的類叫做VirtualMachine,它向任何由位于同一臺物理機器上的JDK或者是一個普通的HtpSpotJVM所運行的VM提供了一個入口點。在通過進程id附加到另外一臺虛擬機上之后,我們就能夠在目標VM指定的一個線程中運行一個JAR文件:
//thefollowingstringsmustbeprovidedbyusStringprocessId=processId();StringjarFileName=jarFileName();VirtualMachinevirtualMachine=VirtualMachine.attach(processId);try{virtualMachine.loadAgent(jarFileName,"World!");}finally{virtualMachine.detach();}
在收到一個JAR文件之后,目標虛擬機會查看該JAR的程序清單描述文件(manifest),并定位處在Premain-Class屬性之下的類。這非常類似于VM執(zhí)行一個主方法的方式。有了一個Java代理,VM和指定的進程id就可以查找到一個名為agentmain的方法,該方法可以由指定線程中的遠程進程來執(zhí)行:
publicclassHelloWorldAgent{publicstaticvoidagentmain(Stringarg){System.out.println("Hello,"+arg);}}
使用該API,只要我們知道一個JVM的進程id,就可以來在其上運行代碼,打印出一條Hello,World!消息。甚至有可能同并不熟JDK發(fā)行版一部分的JVM進行通信,只要附加的VM是一個用來訪問tools.jar的JDK安裝程序。
到目前來看一切順利。但是除了成功地同目標VM建立起了通信之外,我們還不能夠修改目標VM上的代碼以及BUG。后續(xù)的修改,Java代理可以定義第二參數(shù)來接收一個Instrumentation的實例。稍后要實現(xiàn)的接口提供了向幾個底層方法的訪問途徑,它們中的一個就能夠對已經(jīng)加載的代碼進行修改。
為了修正“X-Pirority”錯字,我們首先來假設為HeaderUtility引入了一個修復類,叫做typo.fix,就在我們下面所開發(fā)的BugFixAgent后面的代理的JAR文件中。此外,我們需要給予代理通過向manifest文件添加Can-Redefine-Classes:true來替換現(xiàn)有類的能力。有了現(xiàn)在這些東西,我們就可以使用instrumentation的API來對類進行重新定義,該API會接受一對已經(jīng)加載的類以及用來執(zhí)行類重定義的字節(jié)數(shù)組:
publicclassBugFixAgent{publicstaticvoidagentmain(Stringarg,Instrumentationinst)throwsException{//onlyifheaderutilityisontheclasspath;otherwise,//aclasscanbefoundwithinanyclassloaderbyiterating//overthereturnvalueofInstrumentation::getAllLoadedClassesClass<?>headerUtility=Class.forName("HeaderUtility");//copythecontentsoftypo.fixintoabytearrayByteArrayOutputStreamoutput=newByteArrayOutputStream();try(InputStreaminput=BugFixAgent.class.getResourceAsStream("/typo.fix")){byte[]buffer=newbyte[1024];intlength;while((length=input.read(buffer))!=-1){output.write(buffer,0,length);}}//Applytheredefinitioninstrumentation.redefineClasses(newClassDefinition(headerUtility,output.toByteArray()));}}
運行上述代碼后,HeaderUtility類會被重定義以對應其修補的版本。對isPrivileged的任何后續(xù)調用現(xiàn)在將讀取正確的頭信息。作為一個小的附加說明,JVM可能會在應用類重定義時執(zhí)行完全的垃圾回收,并且會對受影響的代碼進行重新優(yōu)化。總之,這會導致應用程序性能的短時下降。然而,在大多數(shù)情況下,這是較之完全重啟進程更好的方式。
當應用代碼更改時,要確保新類定義了與它替換的類完全相同的字段、方法和修飾符。嘗試修改任何此類屬性的類重定義行為都會導致UnsupportedOperationException。現(xiàn)在HotSpot團隊正試圖去掉這個限制。此外,基于OpenJDK的動態(tài)代碼演變虛擬機支持預覽此功能。
一個如上述示例的簡單的BUG修復代理在你熟悉了instrumentation的API的時候是比較容易實現(xiàn)的。只要更加深入一點,也可以在運行代理的時候,無需手動創(chuàng)建附加的class文件,而是通過重寫現(xiàn)有的class來應用更多通用的代碼修改。
編譯好的Java代碼所呈現(xiàn)的是一系列字節(jié)碼指令。從這個角度來看,一個Java方法無非就是一個字節(jié)數(shù)組,其每一個字節(jié)都是在表示一個向運行時發(fā)出的指令,或者是最近一個指令的參數(shù)。每個字節(jié)對應其意義的映射在《Java虛擬機規(guī)范》中進行了定義,例如字節(jié)0xB1就是在指示VM從一個帶有void返回類型的方法返回。因此,對字節(jié)碼進行增強就是對一個方法的字節(jié)數(shù)字進行擴展,將我們想要應用的表示額外的業(yè)務邏輯指令含進去。
當然,逐個字節(jié)的操作會特別麻煩,而且容易出錯。為了避免手工的處理,許多的庫都提供了更高級一點的API,使用它們不需要我們直接同Java字節(jié)碼打交道。這樣的庫其中就有一個叫做ByteBuddy(當然我就是該庫的作者)。它的功能之一就是能夠定義可以在方法原來的代碼之前和之后被執(zhí)行的模板方法。