閑來無事試一下java的熱修複。主要的原理是通過動態載入class檔案的方式,參考文章為在 Java 中運用動態掛載實現 Bug 的熱修複和動態替換目標進程的Java類
兩篇文章分別在原理和實踐上給出了詳細的說明,這裡做一下執行個體的整理並對遇到的問題和兩篇文章中一些沒有提到的注意事項進行一下說明
被替換的類的準備
先寫一個要被替換掉的類libUtil,裡面只有一個靜態方法printString,列印一句話
public class libUtil { public static void printString(String s){ System.out.println("this is a bug string : "+s); }}
然後是測試類別,需要完成以下工作
1. 列印進程號,方便熱修複時建立的虛擬機器attach到這個進程上
2. 不停的調用要被替換掉的類的方法,以便我們可以清晰的看到熱修複的效果
測試類別如下:
import java.lang.management.ManagementFactory; public class testHotSwap { public static void main(String[] args) { testStatic t=(newtestHotSwap()).new testStatic(); while(true){ try{ Thread.sleep(5000); String name = ManagementFactory.getRuntimeMXBean().getName(); System.out.println(name); // get pid String pid = name.split("@")[0]; System.out.println("Pid is:" + pid); t.print(); } catch(InterruptedException e){} } } class testStatic{ public void print(){ libUtil.printString("did u miss me ?"); } }}
熱修複的準備
接下來我們需要完成三件事:
1. 寫一個進程去啟動一個虛擬機器並attach到已上線的進程上去
2. 寫一個修複類去覆蓋有問題的類
3. 寫一個代理類,通過代理類的介面去實現熱修複,其中這個代理類是要打包成jar檔案的形式
首先來寫一個修複類,這個修複類需要與原來的類在包名,類名,修飾符上完全一致,否則在classRedefine過程中會產生classname don’t match 的異常
代碼如下:
public class libUtil { public static void printString(String s){ System.out.println("this is a fixed string : "+s); }}
然後來寫代理類,其中需要特別注意的有以下幾點:
1. 必須含有static的代理main方法
2. 打成jar包後的Manifest.mf檔案中必須含有以下兩行:
l agent class: Agent-Class:testHotSwapDebug.debugJar
l Can-Redefine-Classes: true
Agent class指定了代理類的包名類名
Can redefineclass屬性使得這個包可以對已載入的class進行redefine
import java.io.ByteArrayOutputStream;import java.io.FileInputStream;import java.io.InputStream;import java.lang.instrument.ClassDefinition;import java.lang.instrument.Instrumentation; public class debugJar { public static void agentmain(String arg, Instrumentation inst) throws Exception { // only if header utility is on the classpath; otherwise, // a class can be found within any classloader by iterating // over the return value ofInstrumentation::getAllLoadedClasses // 需要打成jar包並修改manifest.mf 添加一行指定agent class : Agent-Class:testHotSwapDebug.debugJar //還需要加一行 :Can-Redefine-Classes: true Class<?> libUtil = Class.forName("processWithBug.libUtil"); // copy the contents of typo.fix into abyte array ByteArrayOutputStream output = new ByteArrayOutputStream(); InputStream input=null; try { input =new FileInputStream("C:\\Users\\Administrator\\workspace\\testHotSwapDebug\\bin\\processWithBug\\libUtil.class");//包名類名都要與原來的一致 byte[] buffer = new byte[1024]; int length; while ((length = input.read(buffer)) != -1) { output.write(buffer, 0, length); } }catch(Exception e){ if(input==null) System.out.println("class input is null"); else e.printStackTrace(); } // Apply the redefinition inst.redefineClasses( new ClassDefinition(libUtil, output.toByteArray())); } } 接下來是用來 attach 到已有進程的類,注意事項有以下幾點:
1. 需要在工程中引入tools.jar,這個jar包在jdk的lib檔案夾下,但一般ide的運行環境為jre,所以需要特別添加進環境變數裡面
2. 需要將attach.dll進行動態載入,同樣attach.dll在jdk的一個bin目錄下,需要拷貝到運行目錄下,忘記具體在哪了,可以在jdk目錄下進行搜尋,找到以後放到jre/bin下就可以了
import java.io.File;import com.sun.tools.attach.VirtualMachine;public classtestFixBug { static { System.loadLibrary("attach"); //需要將attach.dll放到java.library.path下 } public static void main(String[] args) { String processId = "17244";//需要修改為要被修複進程的pid String jarFileName ="fixBug.jar";//有agentmain修複類的jar包名 File f=new File(jarFileName); System.out.println(f.getAbsolutePath()); VirtualMachine virtualMachine; try { virtualMachine =VirtualMachine.attach(processId); virtualMachine.loadAgent(jarFileName, "World!"); virtualMachine.detach(); } catch(Exception e){ e.printStackTrace(); } }}
運行結果
先運行要被替換的類的測試進程
11368@SKY-20150127GHWPid is:11368this is a bugstring : did u miss me ?
運行attach的進程:
11368@SKY-20150127GHWPid is:11368thisis a bug string : did u miss me ?11368@SKY-20150127GHWPid is:11368thisis a bug string : did u miss me ?11368@SKY-20150127GHWPid is:11368thisis a fixed string : did u miss me ?11368@SKY-20150127GHWPid is:11368this is a fixedstring : did u miss me ?
一些奇怪的小現象
當static方法直接在main中調用時不能被熱修複,準確的說redefine成功了卻沒有效果。猜測是static塊中調用static方法在編譯時間就已經固定了無法用動態重定義的方法產生影響