java之Instrumentation

java.lang.instrument

java.lang.instrument提供了允许java通过代理服务来检测运行在jvm上的程序,检测机制是对方法的字节码进行修改。这一机制实现了虚拟机级别的aop。

通常代理服务被部署为jar文件,jar文件清单中的属性指定将被加载以启动代理的代理类。对于支持命令行接口的实现,可以在命令行指定一个选项来启动代理。实现也支持在vm启动后某一时刻启动代理的机制。

Java SE 6中instrumentation包被赋予了更强大的功能:启动后的 instrument、本地native的instrument(添加prefix方式)以及动态改变classpath等等,Java SE 6的最大的改变使运行时Instrumentation成为可能。

Java SE 5中Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。

java.lang.instrument包的具体实现依赖于JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由Java 虚拟机提供的,为JVM相关的工具提供的本地编程接口集合。JVMTI是从Java SE 5开始引入,整合和取代了以前使用的Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在Java SE 6中,JVMPI和JVMDI已经消失了。JVMTI提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问JVM,并利用JVMTI提供的丰富的编程接口,完成很多跟JVM相关的功能。

Java SE 5中开发者可以让Instrumentation代理在main函数运行前执行,实现步骤:

  1. 编写 premain 函数

    编写一个 Java 类,包含如下任意一个方法

    1

    2

    public static void premain(String agentArgs, Instrumentation inst);[1]

    public static void premain(String agentArgs);[2]

    其中[1] 的优先级比 [2] 高,将会被优先执行([1] 和 [2] 同时存在时,[2] 被忽略)。

    在premain函数中,开发者可以进行对类的各种操作。

    agentArgs是premain函数得到的程序参数,随同 -javaagent一起传入。与main函数不同的是,该参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。

    Inst 是一个java.lang.instrument.Instrumentation的实例,由jvm自动传入。java.lang.instrument.Instrumentation是 instrument包中定义的一个接口,也是这个包的核心部分,集中了几乎所有功能方法,如类定义的转换和操作......。

  2. jar 文件打包

    将java类打包成一个jar文件,并在其中的manifest属性当中加入Premain-Class来指定步骤1当中编写的那个带有 premain 的java类。(可能还需要指定其他属性以开启更多功能)。

  3. 运行

    用如下方式运行带有Instrumentation的java程序:

    1

    java -javaagent:jar 文件的位置 [= 传入 premain 的参数 ]

对 Java 类文件的操作可以理解为对一个 byte 数组的操作(将类文件的二进制字节流读入一个 byte 数组),开发者可以在ClassFileTransformer的transform方法当中得到,操作并最终返回一个类的定义(一个 byte 数组)。

下面通过简单的举例来说明Instrumentation的基本使用方法。
1.新建TransClass类

1

2

3

4

5

public class TransClass {

    public int getNumber() {

    return 1;

   }

}

运行如下类输出1。

1

2

3

4

5

public class TestMainInJar {

   public static void main(String[] args) {

       System.out.println(new TransClass().getNumber());

   }

}

将TransClass的getNumber方法改成如下 :

1

2

3

public int getNumber() {

       return 2;

}

将修改后的java文件编译成类文件,为了区分原类,以TransClass2.class.2命名。

2.新建Transformer 类:

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

import java.io.File;

import java.io.FileInputStream;

import java.io.IOException;

import java.io.InputStream;

import java.lang.instrument.ClassFileTransformer;

import java.lang.instrument.IllegalClassFormatException;

import java.security.ProtectionDomain;

 

class Transformer implements ClassFileTransformer {

 

   public static final String classNumberReturns2 = "TransClass.class.2";

 

   public static byte[] getBytesFromFile(String fileName) {

       try {

           // precondition

           File file = new File(fileName);

           InputStream is = new FileInputStream(file);

           long length = file.length();

           byte[] bytes = new byte[(int) length];

 

           // Read in the bytes

           int offset = 0;

           int numRead = 0;

           while (offset <bytes.length

                   && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {

               offset += numRead;

           }

 

           if (offset < bytes.length) {

               throw new IOException("Could not completely read file "

                       + file.getName());

           }

           is.close();

           return bytes;

       } catch (Exception e) {

           System.out.println("error occurs in _ClassTransformer!"

                   + e.getClass().getName());

           return null;

       }

   }

 

   public byte[] transform(ClassLoader l, String className, Class c,

           ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {

       if (!className.equals("TransClass")) {

           return null;

       }

       return getBytesFromFile(classNumberReturns2);

 

   }

}

该类实现了ClassFileTransformer接口。其中getBytesFromFile方法根据文件名读入二进制字符流,而ClassFileTransformer当中规定的transform方法则完成了类定义的替换转换。

3.新建Premain类,写入Instrumentation的代理方法premain:

1

2

3

4

5

6

public class Premain {

   public static void premain(String agentArgs, Instrumentation inst)

           throws ClassNotFoundException, UnmodifiableClassException {

       inst.addTransformer(new Transformer());

   }

}

可以看出addTransformer方法并没有指明要转换哪个类。转换发生在premain函数执行之后,main函数执行之前,这时每装载一个类,transform方法就会执行一次,看看是否需要转换,所以在transform方法中,程序用className.equals("TransClass") 来判断当前类是否需要转换。

4.打包为TestInstrument1.jar。返回1的那个TransClass的类文件保留在jar包中,而返回2的那个TransClass.class.2则放到jar的外面。在manifest里面加入如下属性来指定premain所在的类:

1

2

Manifest-Version: 1.0

Premain-Class: Premain

在运行这个程序的时候,如果我们用普通方式运行这个jar中的main函数,可以得到输出1。如果用下列方式运行 :

1

java – javaagent:TestInstrument1.jar – cp TestInstrument1.jar TestMainInJar

则会得到输出2。
当然程序运行的main函数不一定要放在premain所在的这个 jar 文件里面,这里只是为了例子程序打包的方便而放在一起的。

除了用addTransformer的方式,Instrumentation当中还有另外一个方法redefineClasses来实现 premain 当中指定的转换。
用法类似如下:

1

2

3

4

5

6

7

8

9

public class Premain {

   public static void premain(String agentArgs, Instrumentation inst)

           throws ClassNotFoundException, UnmodifiableClassException {

       ClassDefinition def = new ClassDefinition(TransClass.class, Transformer

               .getBytesFromFile(Transformer.classNumberReturns2));
       //
redefineClasses 的功能比较强大,可以批量转换很多类。

       inst.redefineClasses(new ClassDefinition[] { def });

       System.out.println("success");

   }

}


Java SE 6的新特性:虚拟机启动后的动态instrument

Java SE 5中开发者只能在premain当中施展想象力,所作的Instrumentation也仅限与main函数执行前,这样的方式存在一定的局限性。在Java SE 5的基础上,Java SE 6针对这种状况做出了改进,开发者可以在main函数开始执行以后,再启动自己的 Instrumentation程序在Java SE 6的Instrumentation当中,有一个跟premain并驾齐驱的agentmain方法,可以在main函数开始运行之后再运行。

跟premain函数一样, 开发者可以编写一个含有agentmain函数的java类:

1

2

public static void agentmain (String agentArgs, Instrumentation inst);[1]

public static void agentmain (String agentArgs);[2]

同样[1]的优先级比[2]高,将会被优先执行。

类似premain函数,开发者可以在 agentmain 中进行对类的各种操作。其中的agentArgs和Inst的用法跟premain相同。与Premain-Class类似,开发者必须在manifest文件里设置Agent-Class来指定包含agentmain函数的类。可是跟 premain不同的是,agentmain 需要在main函数开始运行后才启动,这样的时机应该如何确定呢,这样的功能又如何实现呢?引入Attach API


Java SE 6当中提供的Attach API

Attach API 不是java的标准API,而是Sun公司提供的一套扩展API,用来向目标JVM附着(Attach)代理工具程序的。有了它,开发者可以方便的监控一个JVM,运行一个外加的代理程序。

Attach API很简单,只有2个主要的类,都在com.sun.tools.attach包里面: VirtualMachine代表一个Java虚拟机,也就是程序需要监控的目标虚拟机,提供了JVM枚举,Attach动作和Detach动作(Attach动作的相反行为,从JVM上面解除一个代理)等等 ; VirtualMachineDescriptor则是一个描述虚拟机的容器类,配合VirtualMachine类完成各种功能。

为了简单起见,我们举例简化如下:依然用类文件替换的方式,将一个返回1的函数替换成返回2的函数,Attach API写在一个线程里面,用睡眠等待的方式,每隔半秒时间检查一次所有的Java虚拟机,当发现有新的虚拟机出现的时候,就调用attach函数,随后再按照Attach API文档里面所说的方式装载jar文件。等到5秒钟的时候,attach程序自动结束。而在main函数里面,程序每隔半秒钟输出一次返回值(显示出返回值从1变成2)。

TransClass类和Transformer类的代码不变,参看上一节介绍。 含有main函数的TestMainInJar代码为:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

public class TestMainInJar {

   public static void main(String[] args) throws InterruptedException {

       System.out.println(new TransClass().getNumber());

       int count = 0;

       while (true) {

           Thread.sleep(500);

           count++;

           int number = new TransClass().getNumber();

           System.out.println(number);

           if (3 == number || count >= 10) {

               break;

           }

       }

   }

}

含有agentmain的AgentMain类的代码为:

1

2

3

4

5

6

7

8

9

10

11

12

13

import java.lang.instrument.ClassDefinition;

import java.lang.instrument.Instrumentation;

import java.lang.instrument.UnmodifiableClassException;

 

public class AgentMain {

   public static void agentmain(String agentArgs, Instrumentation inst)

           throws ClassNotFoundException, UnmodifiableClassException,

           InterruptedException {

       inst.addTransformer(new Transformer (), true);

       inst.retransformClasses(TransClass.class);

       System.out.println("Agent Main Done");

   }

}

其中retransformClasses是Java SE 6新方法,跟redefineClasses一样,可以批量转换类定义,多用于agentmain场合。

Jar文件跟Premain那个例子里面的Jar文件差不多,也是把main和agentmain的类,TransClass,Transformer等类放在一起,打包为TestInstrument1.jar,而Jar文件当中的 Manifest 文件为 :

1

2

Manifest-Version: 1.0

Agent-Class: AgentMain

为了运行Attach API,我们可以再写一个控制程序来模拟监控过程:(代码片段)

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

import com.sun.tools.attach.VirtualMachine;

import com.sun.tools.attach.VirtualMachineDescriptor;

……

 // 一个运行 Attach API 的线程子类

 static class AttachThread extends Thread {        

 private final List<VirtualMachineDescriptor> listBefore;

        private final String jar;

        AttachThread(String attachJar, List<VirtualMachineDescriptor> vms) {

            listBefore = vms;  // 记录程序启动时的 VM 集合

            jar = attachJar;

        

        public void run() {

            VirtualMachine vm = null;

            List<VirtualMachineDescriptor> listAfter = null;

            try {

                int count = 0;

                while (true) {

                    listAfter = VirtualMachine.list();

                    for (VirtualMachineDescriptor vmd : listAfter) {

                        if (!listBefore.contains(vmd)) {

                            // 开始监控新增VM

                            vm = VirtualMachine.attach(vmd);

                            break;

                        }

                    }

                    Thread.sleep(500);

                    count++;

                    if (null != vm || count >= 10) {

                        break;

                    }

                }

                vm.loadAgent(jar);

                vm.detach();

            } catch (Exception e) {

                 ignore

            }

        }

    }

 ……

 public static void main(String[] args) throws InterruptedException {   

     new AttachThread("TestInstrument1.jar", VirtualMachine.list()).start();

 }

运行时,可以首先运行上面这个启动新线程的main函数,然后在5秒钟内(仅仅简单模拟JVM的监控过程)运行如下命令启动测试Jar文件 :

1

java – javaagent:TestInstrument2.jar – cp TestInstrument2.jar TestMainInJar

如果时间掌握得不太差的话,程序首先会在屏幕上打出1,这是改动前的类的输出,然后会打出一些2,这个表示agentmain已经被 Attach API 成功附着到JVM上,代理程序生效了,当然还可以看到Agent Main Done字样的输出。

Java SE 6 新特性:本地方法的 Instrumentation(有待研究)
 

Java SE 6 新特性:BootClassPath / SystemClassPath 的动态增补

虚拟机启动之后来加载某些jar进入bootclasspath?(确认虚拟机已经支持这个功能)
解决办法:
Transformer中使用appendToBootstrapClassLoaderSearch、appendToSystemClassLoaderSearch 来完成这个任务,不过在agent的manifest里加入Boot-Class-Path也一样可以在动态载入agent同时加入自己的boot class路径。只不过没有java code来得灵活,java code可以灵活地选择和判断加载jar。

参考:https://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html

你可能感兴趣的:(Java基础)