动态替换目标进程的Java类

动态替换目标进程的Java类

我们都知道在Eclipse中调试代码时,可以直接修改代码,然后继续调试,不需要重新启动它,我一直很好奇这是怎么实现的。找了一段时间后,发现做起来很简单,原理如下:

你可以把目标进程想象成你的被调试程序,而客户进程想象成Eclipse本身。当某些类有变化时,客户进程能探测到这些类的变化,然后动态换掉它们,这样目标进程就可以用上新的类了。

对应的Eclipse工程如下:

其中RunAlways工程对应的就是目标进程,它的main函数里会不停new一个新的User类,然后打印它的名字

1
2
3
4
5
6
7
8
9
10
11
12
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类的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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文件。类的内容是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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

最后一句是必须的,否则运行起来目标程序会有异常:

1
2
3
4
5
6
7
8
9
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类是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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目录
         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函数的前四句是必须的,否则会有这样的异常:

1
2
3
4
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调换了位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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,直接启动一个线程,不停看那个文件,只要修改时间有变化,就重新加载它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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 ClassFileWatcher extends Thread {
     private File classFile ;
     private long lastModified = 0 ;
     private Instrumentation inst ;
     public ClassFileWatcher ( Instrumentation inst , File classFile ) {
         this . classFile = classFile ;
         this . inst = inst ;
         lastModified = classFile . lastModified ( ) ;
     }
    
     @Override
     public void run ( ) {
         while ( true ) {
             //如果class文件有变化,则重新加载
             if ( lastModified != classFile . lastModified ( ) ) {
                 lastModified = classFile . lastModified ( ) ;
                
               System . out . println ( "重新定义 test.User -- 开始" ) ;
                 byte [ ] reporterClassFile = new byte [ ( int ) classFile . length ( ) ] ;
                 DataInputStream in ;
                 try {
                     in = new DataInputStream ( new FileInputStream ( classFile ) ) ;
                     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 ) {
                     e . printStackTrace ( ) ;
                 }
             }
            
             try {
                 Thread . sleep ( 3000 ) ;
             } catch ( InterruptedException e ) {
                 e . printStackTrace ( ) ;
             }
         }
     }
   
}

代理类改成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package agent ;
import java . io . File ;
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 ) ;
            //把新的User类文件的内容读出来
              File f = new File ( agentArgs ) ;
              ClassFileWatcher watcher = new ClassFileWatcher ( inst , f ) ;
              watcher . start ( ) ;
             
         }
         catch ( Exception e )
         {
             System . out . println ( e ) ;
             e . printStackTrace ( ) ;
         }
     }
}

现在重新把整个过程再跑一遍,然后用新的User.class覆盖e:/User.class,每次覆盖后,你就会发现目标进程已经用上了新的版本,酷啊。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值