Java类动态加载(二)——动态加载class文件
想要在jvm启动后,动态的加载class类文件,我们首先需要了解Instrumentation、Attach、Agent、VirtualMachine、ClassFileTransformer这几个类的用法和他们之间的关系。
Java的com.sun.tools.attach包中的VirtualMachine类,该类允许我们通过给attach方法传入一个jvm的pid(进程id),远程连接到jvm上。然后我们可以通过loadAgent方法向jvm注册一个代理程序agent,在该agent的代理程序中会得到一个Instrumentation实例,该实例可以在class加载前改变class的字节码,可以在class加载后重新加载。在调用Instrumentation实例的方法时,这些方法会使用ClassFileTransformer接口中提供的方法进行处理。
下面先详细介绍下VirtualMachine、Attach、Agent、Instrumentation、ClassFileTransformer这几个类的用法。
一、VirtualMachine
VirtualMachine 详细API可以在这里查看:
http://docs.oracle.com/javase/6/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html
VirtualMachine中的attach(String id)方法允许我们通过jvm的pid,远程连接到jvm。当通过Attach API连接到JVM的进程上后,系统会加载management-agent.jar,然后在JVM中启动一个Jmx代理,最后通过Jmx连接到虚拟机。
下面展示通过attach到目标jvm,然后通过loadAgent注册management-agent.jar代理程序,启动jmx代理服务。
位于jre\lib目录中的management-agent.jar是没有任何class类文件的,整个jar包中只有MANIFEST.MF文件,文件内容如下:
代理类的方法中的参数中的Instrumentation:
通过参数中的Instrumentation inst,添加自己定义的ClassFileTransformer,来改变class文件。这里自定义的Transformer实现了transform方法,在该方法中提供了对实际要执行的类的字节码的修改,甚至可以达到执行另外的类方法的地步
关于更多的Agent代理类的使用方法请参考下面的URI:
http://blog.sina.com.cn/s/blog_605f5b4f0100qfvc.html
http://mgoann.iteye.com/blog/1422680
三、Instrumentation
java.lang.Instrument包是在JDK5引入的,程序员通过修改方法的字节码实现动态修改类代码。在代理类的方法中的参数中,就有Instrumentation inst实例。通过该实例,我们可以调用Instrumentation提供的各种接口。比如调用inst.getAllLoadedClasses()得到所有已经加载过的类。调用inst.addTransformer(new SdlTransformer(), true)增加一个可重转换转换器。调用inst.retransformClasses(Class cls),向jvm发起重转换请求。
Java Instrutment只提供了JVM TI中非常小的一个功能子集,一个是允许在类加载之前,修改类字节(ClassFileTransformer)(JDK5中开始提供,即使随JVM启动的Agent),另外一个是在类加载之后,触发JVM重新进行类加载(JDK6中开始提供,用于JVM启动之后通过Attach去加载Agent)。这两个功能表面看起来微不足道,但实际非常强大,AspectJ AOP的动态Weaving、Visual VM的性能剖析、JConsole支持Attach到进程上进行监控,都是通过这种方式来做的。除了这两个功能外,JDK 6中还提供了动态增加BootstrapClassLoader/SystemClassLoader的搜索路径、对Native方法进行instrutment(还记得JVM TI的Native Method Bind吗?)。
1.主要API(java.lang.instrutment)
1)ClassFileTransformer:定义了类加载前的预处理类,可以在这个类中对要加载的类的字节码做一些处理,譬如进行字节码增强
2)Instrutmentation:增强器,由JVM在入口参数中传递给我们,提供了如下的功能
addTransformer/ removeTransformer:注册/删除ClassFileTransformerretransformClasses:对于已经加载的类重新进行转换处理,即会触发重新加载类定义,需要注意的是,新加载的类不能修改旧有的类声明,譬如不能增加属性、不能修改方法声明redefineClasses:与如上类似,但不是重新进行转换处理,而是直接把处理结果(bytecode)直接给JVMgetAllLoadedClasses:获得当前已经加载的Class,可配合retransformClasses使用getInitiatedClasses:获得由某个特定的ClassLoader加载的类定义getObjectSize:获得一个对象占用的空间,包括其引用的对象appendToBootstrapClassLoaderSearch/appendToSystemClassLoaderSearch:增加BootstrapClassLoader/SystemClassLoader的搜索路径isNativeMethodPrefixSupported/setNativeMethodPrefix:支持拦截Native Method
关于更多的Agent代理类的使用方法请参考下面的URI:
http://ayufox.iteye.com/blog/655619
http://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html
四、ClassFileTransformerbyte[] transform(ClassLoader loader,String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer)throws IllegalClassFormatException
该接口只定义个一个方法transform,该方法会在加载新class类或者重新加载class类时,调用。例如,inst.addTransformer(new SdlTransformer(), true)当代码中增加了一个可重转换转换器后,每次类加载之前,就会调用transform方法。若该方法返回null,则不改变加载的class字节码,若返回一个byte[]数组,则jvm将会用返回的byte[]数组替换掉原先应该加载的字节码。
下面将transform的官方说明贴出来:
byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException此方法的实现可以转换提供的类文件,并返回一个新的替换类文件。
有两种装换器,由 Instrumentation.addTransformer(ClassFileTransformer,boolean) 的 canRetransform 参数确定:
可重转换 转换器,将 canRetransform 设为 true 可添加这种转换器
不可重转换 转换器,将 canRetransform 设为 false 或者使用 Instrumentation.addTransformer(ClassFileTransformer) 可添加这种转换器
在转换器使用 addTransformer 注册之后,每次定义新类和重定义类时都将调用该转换器。每次重转换类时还将调用可重转换转换器。对新类定义的请求通过 ClassLoader.defineClass 或其本机等价方法进行。对类重定义的请求通过 Instrumentation.redefineClasses 或其本机等价方法进行。对类重转换的请求将通过 Instrumentation.retransformClasses 或其本机等价方法进行。转换器是在验证或应用类文件字节之前的请求处理过程中调用的。 当存在多个转换器时,转换将由 transform 调用链组成。也就是说,一个 transform 调用返回的 byte 数组将成为下一个调用的输入(通过 classfileBuffer 参数)。
转换将按以下顺序应用:
不可重转换转换器
不可重转换本机转换器
可重转换转换器
可重转换本机转换器
对于重转换,不会调用不可重转换转换器,而是重用前一个转换的结果。对于所有其他情况,调用此方法。在每个这种调用组中,转换器将按照注册的顺序调用。本机转换器由 Java 虚拟机 Tool 接口中的 ClassFileLoadHook 事件提供。
第一个转换器的输入(通过 classfileBuffer 参数)如下:
对于新的类定义,是传递给 ClassLoader.defineClass 的 byte
对于类重定义,是 definitions.getDefinitionClassFile(),其中 definitions 是 Instrumentation.redefineClasses 的参数
对于类重转换,是传递给新类定义的 byte,或者是最后一个重定义(如果有重定义),所有不可转换转换器进行的转换都将自动重新应用并保持不变;有关细节,请参阅 Instrumentation.retransformClasses
如果实现方法确定不需要进行转换,则应返回 null。否则,它将创建一个新的 byte[] 数组,将输入 classfileBuffer 连同所有需要的转换复制到其中,并返回这个新数组。不得修改输入 classfileBuffer。
在重转换和重定义中,转换器必须支持重定义语义:如果转换器在初始定义期间更改的类在以后要重转换或重定义,那么转换器必须确保第二个输出类文件是第一个输出类文件的合法重定义文件。
如果转换器抛出异常(未捕获的异常),后续转换器仍然将被调用并加载,仍然将尝试重定义或重转换。因此,抛出异常与返回 null 的效果相同。若要使用转换器代码在生成未检验异常时防止不希望发生的行为,可以让转换器捕获 Throwable。如果转换器认为 classFileBuffer 不表示一个有效格式的类文件,则将抛出 IllegalClassFormatException;尽管这与返回 null 的效果相同,但它便于对格式毁坏进行记录或调试。
参数:
loader - 定义要转换的类加载器;如果是引导加载器,则为 null
className - 完全限定类内部形式的类名称和 The Java Virtual Machine Specification 中定义的接口名称。例如,"java/util/List"。
classBeingRedefined - 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null
protectionDomain - 要定义或重定义的类的保护域
classfileBuffer - 类文件格式的输入字节缓冲区(不得修改)
返回:
一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。
抛出:
IllegalClassFormatException - 如果输入不表示一个格式良好的类文件
另请参见:
Instrumentation.redefineClasses(java.lang.instrument.ClassDefinition...)
参考文档:
http://ayufox.iteye.com/blog/653214
http://ayufox.iteye.com/blog/655619
http://blog.sina.com.cn/s/blog_605f5b4f0100qfvc.html
http://mgoann.iteye.com/blog/1422680
http://www.ibm.com/developerworks/cn/java/j-lo-jse61/index.html