轉自 http://linmingren.me/blog/2013/02/%E5%8A%A8%E6%80%81%E6%9B%BF%E6%8D%A2%E7%9B%AE%E6%A0%87%E8%BF%9B%E7%A8%8B%E7%9A%84java%E7%B1%BB/
我們都知道在Eclipse中調試代碼時,可以直接修改代碼,然后繼續調試,不需要重新啟動它,我一直很好奇這是怎么實現的。找了一段時間后,發現做起來很簡單,原理如下:
你可以把目標進程想象成你的被調試程序,而客戶進程想象成Eclipse本身。當某些類有變化時,客戶進程能探測到這些類的變化,然后動態換掉它們,這樣目標進程就可以用上新的類了。
對應的Eclipse工程如下:
其中RunAlways工程對應的就是目標進程,它的main函數里會不停new一個新的User類,然后打印它的名字
package test; public class Main { public static void main(String[] args) throws InterruptedException { while (true) { //替換前,打印出 firstName.lastName //被替換后,打印lastName.firstName System.out.println(new User("firstName","lastName").getName()); Thread.sleep(5000); } } }
User類的代碼如下:
package test; public class User { private String firstName; private String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getName() { return firstName + "." + lastName; } }
先啟動它,假設對應的進程號是1234。
接下來是代理jar的編寫,這個jar就包含一個類和一個manifest.mf文件。類的內容是
package agent; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.lang.instrument.ClassDefinition; import java.lang.instrument.Instrumentation; public class MyAgent { //agentArgs就是VirtualMachine.loadAgent()的第二個參數 public static void agentmain(String agentArgs, Instrumentation inst) { try { System.out.println("args: " + agentArgs); System.out.println("重新定義 test.User -- 開始"); //把新的User類文件的內容讀出來 File f = new File(agentArgs); byte[] reporterClassFile = new byte[(int) f.length()]; DataInputStream in = new DataInputStream(new FileInputStream(f)); in.readFully(reporterClassFile); in.close(); //把User類的定義與新的類文件關聯起來 ClassDefinition reporterDef = new ClassDefinition(Class.forName("test.User"), reporterClassFile); //重新定義User類, 媽呀, 太簡單了 inst.redefineClasses(reporterDef); System.out.println("重新定義 test.User -- 完成"); } catch(Exception e) { System.out.println(e); e.printStackTrace(); } } }
就幾句話,看注釋就明白了。manifest.mf是放在src/META-INF目錄下,內容是
Manifest-Version: 1.0
Agent-Class: agent.MyAgent
Can-Redefine-Classes: true
最后一句是必須的,否則運行起來目標程序會有異常:
java.lang.UnsupportedOperationException: redefineClasses is not supported in this environment at sun.instrument.InstrumentationImpl.redefineClasses(Unknown Source) at agent.MyAgent.agentmain(MyAgent.java:24) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(Unknown Source) at sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain(Unknown Source)
從Eclipse中把這個工程導出為一個jar文件,記得要包含我們的manifest.mf文件。保存在e:/agent.jar(位置隨便).
Client工程的Client類是這樣:
package client; import java.lang.reflect.Field; import com.sun.tools.attach.VirtualMachine; public class Client { /** * @param args * @throws Exception */ public static void main(String[] args) throws Exception { //注意,是jre的bin目錄,不是jdk的bin目錄
//VirtualMachine need the attach.dll in the jre of the JDK. System.setProperty("java.library.path", "C:\\Program Files\\Java\\jdk1.7.0_13\\jre\\bin"); Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths"); fieldSysPath.setAccessible(true); fieldSysPath.set(null, null); //目標進程的進程id -- 記得改成正確的數字 VirtualMachine vm = VirtualMachine.attach("1234"); //參數1:代理jar的位置 //參數2, 傳遞給代理的參數 vm.loadAgent("e:/agent.jar", "e:/User.class"); } }
java.util.ServiceConfigurationError: com.sun.tools.attach.spi.AttachProvider: Provider sun.tools.attach.WindowsAttachProvider could not be instantiated: java.lang.UnsatisfiedLinkError: no attach in java.library.path Exception in thread "main" com.sun.tools.attach.AttachNotSupportedException: no providers installed at com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:208) at client.Client.main(Client.java:29)
現在就差最后一步了,就是要提供新的User類,它跟舊的User類的差別就是把lastName和firstName調換了位置:
package test; public class User { private String firstName; private String lastName; public User(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } //打印出來的位置變了 public String getName() { return lastName + "." + firstName; } }
直接把這個類對應的class文件復制到e:/User.class (簡單起見,你可以隨便放在什么地方)。寫了半天,終於可以看看成果了,直接運行Client類。然后看看目標進程的輸出:
firstName.lastName
args: e:/User.class
重新定義 test.User — 開始
重新定義 test.User — 完成
lastName.firstName
lastName.firstName
媽呀,User類真的被我們改了。有點美中不足的是這個類只能被改一次,能不能像Eclise那樣每次e:/User.class有變化都重新加載呢,這對整天寫Java的我們來說,簡直難度為0,直接啟動一個線程,不停看那個文件,只要修改時間有變化,就重新加載它,代理類改成這樣,里面包含一個自動偵測的線程。
package agent; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.lang.instrument.ClassDefinition; import java.lang.instrument.Instrumentation; public class MyAgent { public static void agentmain(String agentArgs, Instrumentation inst) { try { System.out.println("args: " + agentArgs); File f = new File(agentArgs); ClassFileWatcher watcher = new ClassFileWatcher(inst, f); watcher.start(); System.out.println("agentmain done "); } catch (Exception e) { System.out.println(e); e.printStackTrace(); } } private static void reDefineClass(Instrumentation inst, File classFile) { byte[] reporterClassFile = new byte[(int) classFile.length()]; DataInputStream in; try { System.out.println("redefiniton test.User -- begin"); in = new DataInputStream(new FileInputStream(classFile)); in.readFully(reporterClassFile); in.close(); ClassDefinition reporterDef = new ClassDefinition(Class.forName("test.User"), reporterClassFile); inst.redefineClasses(reporterDef); System.out.println("redefiniton test.User -- done"); } catch (Exception e) { e.printStackTrace(); } } private static class ClassFileWatcher extends Thread { private File classFile; private long lastModified = 0; private Instrumentation inst; private boolean firstRun = true; public ClassFileWatcher(Instrumentation inst, File classFile) { this.classFile = classFile; this.inst = inst; lastModified = classFile.lastModified(); } @Override public void run() { while (true) { if (firstRun || (lastModified != classFile.lastModified()) ) { firstRun = false; lastModified = classFile.lastModified(); reDefineClass(inst,classFile); } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
現在重新把整個過程再跑一遍,然后用新的User.class覆蓋e:/User.class,每次覆蓋后,你就會發現目標進程已經用上了新的版本,酷啊。
備注:下面這段代碼是因為Java的關於動態修改java.library.path而引入的.
具體原因是因為在代碼中設置java.library.path時,不會生效,因為JVM在啟動時讀取值后會一直緩存起來用。
下面的代碼就是一個workaroud使修改java.library.path生效。
//注意,是jre的bin目錄,不是jdk的bin目錄 //VirtualMachine need the attach.dll in the jre of the JDK. System.setProperty("java.library.path","C:\\Program Files\\Java\\jdk1.7.0_13\\jre\\bin"); Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths"); fieldSysPath.setAccessible(true); fieldSysPath.set(null, null);
原因和ClassLoader的實現有關系,使用反射,讓sys_paths==null,從而從新加載java.library.path. ClassLoader.loadLibrary() method: if (sys_paths == null) { usr_paths = initializePath("java.library.path"); sys_paths = initializePath("sun.boot.library.path"); }
設置java.library.path
java程序中:
System.setProperty("java.library.path", System.getProperty("java.library.path") + ":/home/terje/offline_devl/hadoop-2.6.0/lib/native/"); Field fieldSysPath = ClassLoader.class.getDeclaredField("sys_paths"); fieldSysPath.setAccessible(true); fieldSysPath.set(null, null);
可以參考下面的這個bug:
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4280189 但是從2002年到現在Sun一直都沒有改,不知道出於什么原因考慮的。
有問題,就會有人解決問題,antony_miguel在一篇文章中,使用java的反射機制,完成了對於ClassLoader類中的usr_paths變量的動態修改,
public static void addDir(String s) throws IOException { try { Field field = ClassLoader.class.getDeclaredField("usr_paths"); field.setAccessible(true); String[] paths = (String[]) field.get(null); for (int i = 0; i < paths.length; i++) { if (s.equals(paths[i])) { return; } } String[] tmp = new String[paths.length + 1]; System.arraycopy(paths, 0, tmp, 0, paths.length); tmp[paths.length] = s; field.set(null, tmp); } catch (IllegalAccessException e) { throw new IOException("Failed to get permissions to set library path"); } catch (NoSuchFieldException e) { throw new IOException("Failed to get field handle to set library path"); } }
但是這種方法和jvm的實現強關聯,只要jvm實現不是用的變量usr_paths來保存java.library.path的值,這個方法就不能用了。 但是只要知道源代碼,小小的改動就應該可以實現了。