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函数运行前执行,实现步骤:
编写一个 Java 类,包含如下任意一个方法
1 2 |
|
其中[1] 的优先级比 [2] 高,将会被优先执行([1] 和 [2] 同时存在时,[2] 被忽略)。
在premain函数中,开发者可以进行对类的各种操作。
agentArgs是premain函数得到的程序参数,随同 -javaagent
一起传入。与main函数不同的是,该参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。
Inst 是一个java.lang.instrument.Instrumentation的实例,由jvm自动传入。java.lang.instrument.Instrumentation是 instrument包中定义的一个接口,也是这个包的核心部分,集中了几乎所有功能方法,如类定义的转换和操作......。
将java类打包成一个jar文件,并在其中的manifest属性当中加入Premain-Class来指定步骤1当中编写的那个带有 premain 的java类。(可能还需要指定其他属性以开启更多功能)。
用如下方式运行带有Instrumentation的java程序:
1 |
|
对 Java 类文件的操作可以理解为对一个 byte 数组的操作(将类文件的二进制字节流读入一个 byte 数组),开发者可以在ClassFileTransformer的transform方法当中得到,操作并最终返回一个类的定义(一个 byte 数组)。
下面通过简单的举例来说明Instrumentation的基本使用方法。
1.新建TransClass类
1 2 3 4 5 |
|
运行如下类输出1。
1 2 3 4 5 |
|
将TransClass的getNumber方法改成如下 :
1 2 3 |
|
将修改后的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 |
|
该类实现了ClassFileTransformer接口。其中getBytesFromFile方法根据文件名读入二进制字符流,而ClassFileTransformer当中规定的transform方法则完成了类定义的替换转换。
3.新建Premain类,写入Instrumentation的代理方法premain:
1 2 3 4 5 6 |
|
可以看出addTransformer方法并没有指明要转换哪个类。转换发生在premain函数执行之后,main函数执行之前,这时每装载一个类,transform方法就会执行一次,看看是否需要转换,所以在transform方法中,程序用className.equals("TransClass") 来判断当前类是否需要转换。
4.打包为TestInstrument1.jar。返回1的那个TransClass的类文件保留在jar包中,而返回2的那个TransClass.class.2则放到jar的外面。在manifest里面加入如下属性来指定premain所在的类:
1 2 |
|
在运行这个程序的时候,如果我们用普通方式运行这个jar中的main函数,可以得到输出1。如果用下列方式运行 :
1 |
|
则会得到输出2。
当然程序运行的main函数不一定要放在premain所在的这个 jar 文件里面,这里只是为了例子程序打包的方便而放在一起的。
除了用addTransformer的方式,Instrumentation当中还有另外一个方法redefineClasses来实现 premain 当中指定的转换。
用法类似如下:
1 2 3 4 5 6 7 8 9 |
|
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 |
|
同样[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 |
|
含有agentmain的AgentMain类的代码为:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
其中retransformClasses是Java SE 6新方法,跟redefineClasses一样,可以批量转换类定义,多用于agentmain场合。
Jar文件跟Premain那个例子里面的Jar文件差不多,也是把main和agentmain的类,TransClass,Transformer等类放在一起,打包为TestInstrument1.jar,而Jar文件当中的 Manifest 文件为 :
1 2 |
|
为了运行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 |
|
运行时,可以首先运行上面这个启动新线程的main函数,然后在5秒钟内(仅仅简单模拟JVM的监控过程)运行如下命令启动测试Jar文件 :
1 |
|
如果时间掌握得不太差的话,程序首先会在屏幕上打出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