8.3.2 增强代理
ASM工具的一种典型用法是在字节代码被使用之前进行处理。一般的做法是在程序的构建过程中,在源代码被编译之后,再运行相关的程序来对字节代码进行处理。如果需要在运行时进行处理,可以使用类加载器来完成。不过类加载器的方式相对比较复杂。一种更简单的做法是使用J2SE 5.0中引入的java.lang.instrument包提供的API。java.lang.instrument.Instrumentation接口中实现了一种在运行时对类的字节代码进行转换的能力。
1.增强代理的基本用法
对字节代码的转换操作由特殊的代理程序来完成。代理程序是一个jar包。在该jar包的清单文件中定义了启动代理的Java类名称。不同的虚拟机实现提供的启动代理的方式不尽相同,一般有两种实现方法。第一种做法是通过虚拟机的启动参数指定代理程序jar包的路径。所用的参数是“-javaagent”,如“-javaagent:myAgent.jar”。对于这种方式,代理程序的jar包的清单文件中要包含Premain-Class的属性,用来指定一个Java类。该类中必须包含一个premain方法。在虚拟机启动之后,代理程序类中的premain方法会被调用,然后才是程序的主Java类的main方法被调用。在premain方法的参数中可以得到Instrumentation接口的实现对象。第二种做法是在虚拟机运行主程序之后,再启动代理程序。这类代理程序的jar包的清单文件中要由Agent-Class属性指明代理类的名称。当代理类被加载之后,虚拟机会尝试调用类中的agentmain方法。该方法的参数类型与premain相同。下面的介绍都是针对第一种方式进行的。
在代理类的premain或agentmain方法中可以获取作为参数传递的Instrumentation接口的实现。该接口中的重要方法是addTransformer,用来添加java.lang.instrument.ClassFileTransformer接口的实现对象。ClassFileTransformer接口只有一个方法transform,用来对字节代码进行处理,返回处理的结果。代码清单8-18中的TraceTransformer类的作用是对字节代码中的方法进行处理,在方法的开始处插入额外的指令,用System.out方法输出当前方法的名称。经过TraceTransformer类的转换之后,类中的方法在被调用时都会先输出方法的名称。对字节代码修改时使用了ASM工具。
代码清单8-18 追踪方法调用的字节代码转换代理
public class TraceTransformer implements ClassFileTransformer{
public byte[]transform(ClassLoader loader, String className, Class<?>classBeingRedefined, ProtectionDomain protectionDomain, byte[]classfileBuffer)throws IllegalClassFormatException{
ClassReader reader=new ClassReader(classfileBuffer);
ClassWriter writer=new ClassWriter(0);
ClassAdapter adapter=new ClassAdapter(writer){
public MethodVisitor visitMethod(int access, final String name, String desc, String signature, String[]exceptions){
MethodVisitor mv=cv.visitMethod(access, name, desc, signature, exceptions);
return new MethodAdapter(mv){
public void visitCode(){
mv.visitCode();
mv.visitFieldInsn(Opcodes.GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
mv.visitLdcInsn("进入方法:"+name);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V");
}
public void visitMaxs(int maxStack, int maxLocals){
mv.visitMaxs(maxStack+2,maxLocals);
}
};
}
};
reader.accept(adapter,0);
return writer.toByteArray();
}
}
接下来需要把TraceTransformer类添加到代理程序中。代理程序可以是任何Java类。对于在虚拟机启动时运行的代理程序,只要包含premain方法即可。代码清单8-19给出了代理程序的实现。在premain方法中调用addTransformer方法添加了代码清单8-18中的TraceTransformer类的对象,通过该对象来进行相应的字节代码转换操作。把TraceAgent类和相应的清单文件打包在一个jar文件中。在虚拟机启动时添加相关的启动参数来使用此jar文件作为代理程序。
代码清单8-19 追踪方法调用的代理程序
public class TraceAgent{
public static void premain(String args, Instrumentation inst){
inst.addTransformer(new TraceTransformer());
}
}
2.字节代码的重新转换
有两种类型的字节代码转换器,一种是允许重新进行转换的,另外一种是不允许重新进行转换的。这两种类型通过Instrumentation接口的addTransformer方法的调用方式来区分。在调用addTransformer方法时可以指定一个额外的参数来声明是否允许进行重新转换。对于使用addTransformer方法添加的转换器,在通过类加载器的defineClass方法进行定义或使用Instrumentation接口的redefineClasses方法重新进行定义时,转换器的transform方法都会被调用。当类已经被加载后,可以通过Instrumentation接口的retransformClasses方法来重新进行转换。在重新转换的过程中,只有允许进行重新转换的转换器类的transform方法才会被调用。通过addTransformer方法可以添加多个转换器。在转换过程中,这些转换器按照添加时的顺序级联起来。前一个转换器的输出是下一个转换器的输入。如果转换器不需要对某个类的字节代码进行处理,那么transform方法直接返回null即可。
不是所有虚拟机的实现都支持对类的字节代码进行重定义和重新转换操作,也就是说Instrumentation接口的redefineClasses和retransformClasses方法不一定起作用。需要通过Instrumentation接口的isRedefineClassesSupported和isRetransform-ClassesSupported方法来分别检查虚拟机对于重定义和重新转换的支持能力。除了虚拟机支持之外,代理程序也需要在其jar包的清单文件中声明是否需要启用重定义和重新转换类的功能。这是通过清单文件中的Can-Redefine-Classes和Can-Retransform-Classes属性来表示的。这两个属性的默认值都为false,需要显式地启用。这两个条件需要同时满足才能完成重定义和重新转换的操作。
由于重定义和重新转换的支持,可以在运行时动态改变类的行为。代码清单8-19中的TraceAgent在所有类被加载之前都会进行转换操作。如果希望在运行时进行转换操作,可以使用代码清单8-20中的代理程序。在RetransformClassesAgent的premain方法中把Instrumentation接口的实现对象保存下来。当程序调用enableTrace方法时,会添加代码清单8-18中的TraceTransformer转换器,再对ToBeTraced类进行转换操作。转换操作完成之后,ToBeTraced类的行为会即时发生改变。在disableTrace方法的实现中,先把TraceTransformer转换器从列表中删除,再重新进行转换。其结果是ToBeTraced类恢复到了最初的状态。重新转换是必须进行的,否则ToBeTraced类的定义不会更新。
代码清单8-20 使用重新转换操作的代理程序
public class RetransformClassesAgent{
static Instrumentation instrumentation;
static TraceTransformer traceTransformer=new TraceTransformer();
public static void premain(String args, Instrumentation inst){
instrumentation=inst;
}
public static void enableTrace(){
if(instrumentation.isRetransformClassesSupported()){
instrumentation.addTransformer(traceTransformer, true);
try{
instrumentation.retransformClasses(ToBeTraced.class);
}catch(UnmodifiableClassException e){
e.printStackTrace();
}
}
}
public static void disableTrace(){
if(instrumentation.isRetransformClassesSupported()){
instrumentation.removeTransformer(traceTransformer);
try{
instrumentation.retransformClasses(ToBeTraced.class);
}catch(UnmodifiableClassException e){
e.printStackTrace();
}
}
}
}
通过代码清单8-21中给出的方式来使用ToBeTraced类。在method1方法被调用时,ToBeTraced类还没有被转换,不会有调试信息输出。随后调用RetransformClassesAgent类的enableTrace方法进行转换。接下来调用method2方法时会输出相关的调试信息。在disableTrace方法被调用之后,再次对ToBeTraced类进行转换。再次转换完成后,调用method3方法时不再输出调试信息。
代码清单8-21 使用重新转换的代理程序的测试示例
ToBeTraced traced=new ToBeTraced();
traced.method1();
RetransformClassesAgent.enableTrace();
traced.method2();
RetransformClassesAgent.disableTrace();
traced.method3();
每次转换操作都从类最初的字节代码开始,也就是说,在没有改变转换器列表的情况下,多次调用retransformClasses方法的结果是一样的,并不会出现同一转换操作被应用多次的情况。这也是disableTrace方法实现的基础。