動態替換目標進程的Java類


轉自 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"); } }
main函數的前四句是必須的,否則會有這樣的異常:
 
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
-Djava.library.path=xxx
 

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的值,這個方法就不能用了。 但是只要知道源代碼,小小的改動就應該可以實現了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM