java 字节码增强之ASM

ASM系列之一:初探ASM

一、什么是ASM

    ASM是一个JAVA字节码分析、创建和修改的开源应用框架。在ASM中提供了诸多的API用于对类的内容进行字节码操作的方法。与传统的BCEL和SERL不同,在ASM中提供了更为优雅和灵活的操作字节码的方式。目前ASM已被广泛的开源应用架构所使用,例如:Spring、Hibernate等。

二、ASM能干什么

    分析一个类、从字节码角度创建一个类、修改一个已经被编译过的类文件

三、ASM初探例子

    这里我们使用ASM的CoreAPI(ASM提供了两组API:Core和Tree,Core是基于访问者模式来操作类的,而Tree是基于树节点来操作类的)创建一个MyClass类,目标类如下:

[plain] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class MyClass {    
  2.     private String name;    
  3.         
  4.     public MyClass(){    
  5.         this.name = "zhangzhuo";    
  6.     }    
  7.     
  8.     public String getName() {    
  9.         return name;    
  10.     }    
  11.     
  12.     public void setName(String name) {    
  13.         this.name = name;    
  14.     }    
  15. }   

 这个类在构造方法中初始化了属性name,并提供了两个public方法来修改和访问name属性。

 

 接下来就要书写创建这个类的代码了,现将代码给出,然后逐步解释,代码如下:

   代码1:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class GenerateClass {    
  2.     public void generateClass() {    
  3.     
  4.                 //方法的栈长度和本地变量表长度用户自己计算    
  5.         ClassWriter classWriter = new ClassWriter(0);       
  6.         
  7.                 //Opcodes.V1_6指定类的版本    
  8.                 //Opcodes.ACC_PUBLIC表示这个类是public,    
  9.                 //“org/victorzhzh/core/classes/MyClass”类的全限定名称    
  10.                 //第一个null位置变量定义的是泛型签名,    
  11.                 //“java/lang/Object”这个类的父类    
  12.                 //第二个null位子的变量定义的是这个类实现的接口    
  13. classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC,    
  14.                 "org/victorzhzh/core/classes/MyClass"null,    
  15.                 "java/lang/Object"null);    
  16.                    
  17.         ClassAdapter classAdapter = new MyClassAdapter(classWriter);    
  18.     
  19.         classAdapter.visitField(Opcodes.ACC_PRIVATE, "name",    
  20.                 Type.getDescriptor(String.class), nullnull);//定义name属性    
  21.     
  22.         classAdapter.visitMethod(Opcodes.ACC_PUBLIC, "<init>""()V"null,    
  23.                 null).visitCode();//定义构造方法    
  24.     
  25.         String setMethodDesc = "(" + Type.getDescriptor(String.class) + ")V";    
  26.         classAdapter.visitMethod(Opcodes.ACC_PUBLIC, "setName", setMethodDesc,    
  27.                 nullnull).visitCode();//定义setName方法    
  28.     
  29.         String getMethodDesc = "()" + Type.getDescriptor(String.class);    
  30.         classAdapter.visitMethod(Opcodes.ACC_PUBLIC, "getName", getMethodDesc,    
  31.                 nullnull).visitCode();//定义getName方法    
  32.     
  33.         byte[] classFile = classWriter.toByteArray();//生成字节码    
  34.     
  35.         MyClassLoader classLoader = new MyClassLoader();//定义一个类加载器    
  36.         Class clazz = classLoader.defineClassFromClassFile(    
  37.                 "org.victorzhzh.core.classes.MyClass", classFile);    
  38.         try {//利用反射方式,访问getName    
  39.             Object obj = clazz.newInstance();    
  40.             Method method = clazz.getMethod("getName");    
  41.                         System.out.println(obj.toString());    
  42.             System.out.println(method.invoke(obj, null));    
  43.         } catch (Exception e) {    
  44.             e.printStackTrace();    
  45.         }    
  46.     }    
  47.     
  48.     class MyClassLoader extends ClassLoader {    
  49.         public Class defineClassFromClassFile(String className, byte[] classFile)    
  50.                 throws ClassFormatError {    
  51.             return defineClass(className, classFile, 0, classFile.length);    
  52.         }    
  53.     }    
  54.     
  55.     public static void main(String[] args) {    
  56.         GenerateClass generateClass = new GenerateClass();    
  57.         generateClass.generateClass();    
  58.     }    
  59. }   

   代码2:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class MyClassAdapter extends ClassAdapter {    
  2.     
  3.     public MyClassAdapter(ClassVisitor cv) {    
  4.         super(cv);    
  5.     }    
  6.     
  7.     @Override    
  8.     public MethodVisitor visitMethod(int access, String name, String desc,    
  9.             String signature, String[] exceptions) {    
  10.         MethodVisitor methodVisitor = cv.visitMethod(access, name, desc,    
  11.                 signature, exceptions);    
  12.         if (name.equals("<init>")) {    
  13.             return new InitMethodAdapter(methodVisitor);    
  14.         } else if (name.equals("setName")) {    
  15.             return new SetMethodAdapter(methodVisitor);    
  16.         } else if (name.equals("getName")) {    
  17.             return new GetMethodAdapter(methodVisitor);    
  18.         } else {    
  19.             return super.visitMethod(access, name, desc, signature, exceptions);    
  20.         }    
  21.     }    
  22.     
  23.         //这个类生成具体的构造方法字节码    
  24.     class InitMethodAdapter extends MethodAdapter {    
  25.         public InitMethodAdapter(MethodVisitor mv) {    
  26.             super(mv);    
  27.         }    
  28.     
  29.         @Override    
  30.         public void visitCode() {    
  31.             mv.visitVarInsn(Opcodes.ALOAD, 0);    
  32.             mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",    
  33.                     "<init>""()V");//调用父类的构造方法    
  34.             mv.visitVarInsn(Opcodes.ALOAD, 0);    
  35.             mv.visitLdcInsn("zhangzhuo");//将常量池中的字符串常量加载刀栈顶    
  36.             mv.visitFieldInsn(Opcodes.PUTFIELD,    
  37.                     "org/victorzhzh/core/classes/MyClass""name",    
  38.                     Type.getDescriptor(String.class));//对name属性赋值    
  39.             mv.visitInsn(Opcodes.RETURN);//设置返回值    
  40.             mv.visitMaxs(21);//设置方法的栈和本地变量表的大小    
  41.         }    
  42.     };    
  43.     
  44.         //这个类生成具体的setName方法字节码      
  45.         class SetMethodAdapter extends MethodAdapter {    
  46.         public SetMethodAdapter(MethodVisitor mv) {    
  47.             super(mv);    
  48.         }    
  49.     
  50.         @Override    
  51.         public void visitCode() {    
  52.             mv.visitVarInsn(Opcodes.ALOAD, 0);    
  53.             mv.visitVarInsn(Opcodes.ALOAD, 1);    
  54.             mv.visitFieldInsn(Opcodes.PUTFIELD,    
  55.                     "org/victorzhzh/core/classes/MyClass""name",    
  56.                     Type.getDescriptor(String.class));    
  57.             mv.visitInsn(Opcodes.RETURN);    
  58.             mv.visitMaxs(22);    
  59.         }    
  60.     
  61.     }    
  62.     
  63.            
  64.          //这个类生成具体的getName方法字节    
  65.         class GetMethodAdapter extends MethodAdapter {    
  66.     
  67.         public GetMethodAdapter(MethodVisitor mv) {    
  68.             super(mv);    
  69.         }    
  70.     
  71.         @Override    
  72.         public void visitCode() {    
  73.             mv.visitVarInsn(Opcodes.ALOAD, 0);    
  74.             mv.visitFieldInsn(Opcodes.GETFIELD,    
  75.                     "org/victorzhzh/core/classes/MyClass""name",    
  76.                     Type.getDescriptor(String.class));//获取name属性的值    
  77.             mv.visitInsn(Opcodes.ARETURN);//返回一个引用,这里是String的引用即name    
  78.             mv.visitMaxs(11);    
  79.         }    
  80.     }    
  81. }   

   运行结果:

[plain] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. org.victorzhzh.core.classes.MyClass@1270b73    
  2. zhangzhuo   

   这个例子只是简单地介绍了一下ASM如何创建一个类,接下来的几个章节,将详细介绍ASM的CoreAPI和TreeAPI中如何操作类。


ASM系列之二:Java类的基本表述

    上一篇文章中我们看到了如何使用ASM生成一个简单的JAVA类,里面使用到了很多的基本概念,比如:方法描述、引用描述等,下面将一一介绍。

一、类版本:

    一个Java二进制的类文件,都有一个版本,因此ASM中提供了几个常量来指定一个类的版,这些常量定义在org.objectweb.asm.Opcodes接口中,如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. int V1_1 = 3 << 16 | 45;    
  2. int V1_2 = 0 << 16 | 46;    
  3. int V1_3 = 0 << 16 | 47;    
  4. int V1_4 = 0 << 16 | 48;    
  5. int V1_5 = 0 << 16 | 49;    
  6. int V1_6 = 0 << 16 | 50;    
  7. int V1_7 = 0 << 16 | 51;   

 二、内部名字:

     在Java二进制文件中使用的是JVM的内部名字,而不是我们所熟悉的以“.”分割的全限定名,内部名字是以“/”替代“.”的全名,例如:java.lang.String在JVM中的内部名字是java/lang/String。在ASM中可以使用org.objectweb.asm.Type类中的静态方法getInternalName(final Class c) 来获得,如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class InternalNameTransform {    
  2.     
  3.     public static void main(String[] args) {    
  4.         System.out.println(Type.getInternalName(String.class));    
  5.         System.out.println(Type.getInternalName(Integer.class));    
  6.         System.out.println(Type.getInternalName(InternalNameTransform.class));    
  7.     }    
  8. }   

    运行结果:

[plain] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. java/lang/String    
  2. java/lang/Integer    
  3. org/victorzhzh/core/structure/InternalNameTransform   

三、类型描述:

    我们知道JAVA类型分为基本类型和引用类型,在JVM中对每一种类型都有与之相对应的类型描述,如下表:

Java类型 JVM中的描述
boolean Z
char C
byte B
short S
int I
float F
long J
double D
Object Ljava/lang/Object;
int [I
Object [[Ljava/lang/Object;

    在ASM中要获得一个类的JVM内部描述,可以使用org.objectweb.asm.Type类中的getDescriptor(final Class c)方法,如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class TypeDescriptors {    
  2.     public static void main(String[] args) {    
  3.         System.out.println(Type.getDescriptor(TypeDescriptors.class));    
  4.         System.out.println(Type.getDescriptor(String.class));    
  5.     }    
  6.     
  7. }    

    运行结果:

[plain] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. Lorg/victorzhzh/core/structure/TypeDescriptors;    
  2. Ljava/lang/String;    

 四、方法描述:

    在Java的二进制文件中,方法的方法名和方法的描述都是存储在Constant pool中的,且在两个不同的单元里。因此,方法描述中不含有方法名,只含有参数类型和返回类型,如下:

方法描述,在类中的 方法描述,在二进制文件中的
void a(int i,float f) (IF)V
void a(Object o) (Ljava/lang/Object;)V
int a(int i,String s) (ILjava/lang/String;)I
int[] a(int[] i) ([I)[I
String a() ()Ljava/lang/String;

    获取一个方法的描述可以使用org.objectweb.asm.Type.getMethodDescriptor方法,如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class MethodDescriptors {    
  2.     public static void main(String[] args) throws Exception {    
  3.         Method m = String.class.getMethod("substring"int.class);    
  4.         System.out.println(Type.getMethodDescriptor(m));    
  5.     }    
  6.     
  7. }   

    运行结果:

[plain] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. (I)Ljava/lang/String;    

 其实在org.objectweb.asm.Type类中提供了很多方法让我们去了解一个类,有兴趣的可以看一下它的源码,这对我们了解一个类和操作一个类还是有大帮助的。


ASM系列之三:ASM中的访问者模式

    在ASM的Core API中使用的是访问者模式来实现对类的操作,主要包含如下类:

一、ClassVisitor接口:

    在这个接口中主要提供了和类结构同名的一些方法,这些方法可以对相应的类结构进行操作。如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public interface ClassVisitor {    
  2.     void visit(int version,int access,String name,String signature,String superName,String[] interfaces);    
  3.     void visitSource(String source, String debug);    
  4.     void visitOuterClass(String owner, String name, String desc);    
  5.     AnnotationVisitor visitAnnotation(String desc, boolean visible);    
  6.     void visitAttribute(Attribute attr);    
  7.     void visitInnerClass(String name,String outerName,String innerName,int access);    
  8.     FieldVisitor visitField(int access,String name,String desc,String signature,Object value);    
  9.     MethodVisitor visitMethod(int access,String name,String desc,String signature,String[] exceptions);    
  10.     void visitEnd();    
  11. }   

 这里定义的方法调用是有顺序的,在ClassVisitor中定义了调用的顺序和每个方法在可以出现的次数,如下:

visit [ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod )* visitEnd。

二、ClassReader类:

    这个类会提供你要转变的类的字节数组,它的accept方法,接受一个具体的ClassVisitor,并调用实现中具体的visit,

visitSource, visitOuterClass, visitAnnotation, visitAttribute, visitInnerClass,visitField, visitMethod和 visitEnd方法。

三、ClassWriter类:

    这个类是ClassVisitor的一个实现类,这个类中的toByteArray方法会将最终修改的字节码以byte数组形式返回,在这个类的构造时可以指定让系统自动为我们计算栈和本地变量的大小(COMPUTE_MAXS),也可以指定系统自动为我们计算栈帧的大小(COMPUTE_FRAMES)。

四、ClassAdapter类:

   这个类也是ClassVisitor的一个实现类,这个类可以看成是一个事件过滤器,在这个类里,它对ClassVisitor的实现都是委派给一个具体的ClassVisitor实现类,即调用那个实现类实现的方法。

五、AnnotationVisitor接口:

   这个接口中定义了和Annotation结构想对应的方法,这些方法可以操作Annotation中的定义,如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public interface AnnotationVisitor {    
  2.     void visit(String name, Object value);    
  3.     void visitEnum(String name, String desc, String value);    
  4.     AnnotationVisitor visitAnnotation(String name, String desc);    
  5.     AnnotationVisitor visitArray(String name);    
  6.     void visitEnd();    
  7. }   

 调用顺序如下:

(visit | visitEnum | visitAnnotation | visitArray)* visitEnd

六、FieldVisitor接口:

   这个接口定义了和属性结构相对应的方法,这些方法可以操作属性,如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public interface FieldVisitor {    
  2.     AnnotationVisitor visitAnnotation(String desc, boolean visible);    
  3.     void visitAttribute(Attribute attr);    
  4.     void visitEnd();    
  5. }    

 调用顺序:

visitAnnotation | visitAttribute )* visitEnd .

七、MethodVisitor接口:

    这个接口定义了和方法结构相对应的方法,这些方法可以去操作源方法,具体的可以查看一下源码。

八、操作流程:

   一般情况下,我们需要操作一个类时,首先是获得其二进制的字节码,即用ClassReader来读取一个类,然后需要一个能将二进制字节码写回的类,即用ClassWriter类,最后就是一个事件过滤器,即ClassAdapter。事件过滤器中的某些方法可以产生一个新的XXXVisitor对象,当我们需要修改对应的内容时只要实现自己的XXXVisitor并返回就可以了。

九、例子:

    在这个例子中,我们将对Person类的sayName方法做出一些修改,源类:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class Person {    
  2.     private String name;    
  3.     
  4.     public void sayName() {    
  5.         System.out.println(name);    
  6.     }    
  7. }  

  如果我们定义一个Person类然后调用其sayName()方法将会得到的是一个null,行成的二进制字节码如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public void sayName();    
  2.   Code:    
  3.    Stack=2, Locals=1, Args_size=1    
  4.    0: getstatic   #17//Field java/lang/System.out:Ljava/io/PrintStream;    
  5.    3:   aload_0    
  6.    4:   getfield    #23//Field name:Ljava/lang/String;    
  7.    7:   invokevirtual   #25//Method java/io/PrintStream.println:(Ljava/lang/String;)V    
  8.    10:  return   
  9. }  

 我们修改一下这个方法,让它输出"zhangzhuo",代码如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class GenerateNewPerson {    
  2.     public static void main(String[] args) throws Exception {    
  3.         // 使用全限定名,创建一个ClassReader对象    
  4.         ClassReader classReader = new ClassReader(    
  5.                 "org.victorzhzh.core.ic.Person");    
  6.         // 构建一个ClassWriter对象,并设置让系统自动计算栈和本地变量大小    
  7.         ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);    
  8.     
  9.         ClassAdapter classAdapter = new GeneralClassAdapter(classWriter);    
  10.     
  11.         classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);    
  12.     
  13.         byte[] classFile = classWriter.toByteArray();    
  14.     
  15.         // 将这个类输出到原先的类文件目录下,这是原先的类文件已经被修改    
  16.         File file = new File(    
  17.                 "target/classes/org/victorzhzh/core/ic/Person.class");    
  18.         FileOutputStream stream = new FileOutputStream(file);    
  19.         stream.write(classFile);    
  20.         stream.close();    
  21.     }    
  22. }    
  23.     
  24. public class GeneralClassAdapter extends ClassAdapter {    
  25.     
  26.     public GeneralClassAdapter(ClassVisitor cv) {    
  27.         super(cv);    
  28.     }    
  29.     
  30.     @Override    
  31.     public MethodVisitor visitMethod(int access, String name, String desc,    
  32.             String signature, String[] exceptions) {    
  33.         MethodVisitor mv = cv.visitMethod(access, name, desc, signature,    
  34.                 exceptions);    
  35.         // 当是sayName方法是做对应的修改    
  36.         if (name.equals("sayName")) {    
  37.             MethodVisitor newMv = new SayNameMethodAdapter(mv);    
  38.             return newMv;    
  39.         } else {    
  40.             return mv;    
  41.         }    
  42.     }    
  43.     
  44.     // 定义一个自己的方法访问类    
  45.     class SayNameMethodAdapter extends MethodAdapter {    
  46.         public SayNameMethodAdapter(MethodVisitor mv) {    
  47.             super(mv);    
  48.         }    
  49.     
  50.         // 在源方法前去修改方法内容,这部分的修改将加载源方法的字节码之前    
  51.         @Override    
  52.         public void visitCode() {    
  53.             // 记载隐含的this对象,这是每个JAVA方法都有的    
  54.             mv.visitVarInsn(Opcodes.ALOAD, 0);    
  55.             // 从常量池中加载“zhangzhuo”字符到栈顶    
  56.             mv.visitLdcInsn("zhangzhuo");    
  57.             // 将栈顶的"zhangzhuo"赋值给name属性    
  58.             mv.visitFieldInsn(Opcodes.PUTFIELD,    
  59.                     Type.getInternalName(Person.class), "name",    
  60.                     Type.getDescriptor(String.class));    
  61.         }    
  62.     
  63.     }    
  64.     
  65. }   

 这时,我们在查看一下Person的字节码:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public void sayName();    
  2.   Code:    
  3.    Stack=2, Locals=1, Args_size=1    
  4.    0:   aload_0    
  5.    1:   ldc #13//String zhangzhuo    
  6.    3:   putfield    #15//Field name:Ljava/lang/String;    
  7. =============以上是我们新增加的内容================================    
  8.    6:   getstatic   #21//Field java/lang/System.out:Ljava/io/PrintStream;    
  9.    9:   aload_0    
  10.    10:  getfield    #15//Field name:Ljava/lang/String;    
  11.    13:  invokevirtual   #27//Method java/io/PrintStream.println:(Ljava/lang/String;)V    
  12.    16:  return    
  13.     
  14. }   

再次调用Person对象,输出结果为:zhangzhuo


ASM系列之四:操作类属性

    在上一篇文章中,我们看到了ASM中的Core API中使用的是XXXVisitor操作类中的对应部分。本文将展示如何使用ASM中的Core API对类的属性的操作。

首先,我们定义一个原类Person,如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class Person {    
  2.     public String name = "zhangzhuo";    
  3.     public String address = "xxxxx" ;    
  4. }   

 这里,我们将属性定义为public类型,目的是为了我们使用反射去调用这个属性,接下来我们要为这个类添加一个int类型的属性,名字叫age。

    第一个问题,ASM的Core API允许我们在那些方法中来添加属性?

    在ASM的Core API中你要为类添加属性就必须要自己去实现ClassVisitor这个接口,这个接口中的visitInnerClass、visitField、visitMethod和visitEnd方法允许我们进行添加一个类属性操作,其余的方法是不允许的。这里我们依然使用Core API中的ClassAdapter类,我们继承这个类,定义一个去添加属性的类,ClassAdapter实现了ClassVisitor。

    第二个问题,我们要在这些方法中写什么样的代码才能添加一个属性?

    在使用ASM的Core API添加一个属性时只需要调用一句语句就可以,如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. classVisitor.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class),    
  2.                 nullnull);   

 第一个参数指定的是这个属性的操作权限,第二个参数是属性名,第三个参数是类型描述,第四个参数是泛型类型,第五个参数是初始化的值,这里需要注意一下的是第五个参数,这个参数只有属性为static时才有效,也就是数只有为static时,这个值才真正会赋值给我们添加的属性上,对于非static属性,它将被忽略。

好了,让我们看一段代码,在visitEnd去添加一个名字为age的属性:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class Transform extends ClassAdapter {    
  2.     
  3.     public Transform(ClassVisitor cv) {    
  4.         super(cv);    
  5.     }    
  6.     
  7.     @Override    
  8.     public void visitEnd() {    
  9.         cv.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class),    
  10.                 nullnull);    
  11.     }    
  12.     
  13. }   

 非常简单吧,只要一句话就可以添加一个属性到我们的类中,看一下我们的测试类:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class TransformTest {    
  2.     @Test    
  3.     public void addAge() throws Exception {    
  4.         ClassReader classReader = new ClassReader(    
  5.                 "org.victorzhzh.core.field.Person");    
  6.         ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);    
  7.         ClassAdapter classAdapter = new Transform(classWriter);    
  8.     
  9.         classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);    
  10.     
  11.         byte[] classFile = classWriter.toByteArray();    
  12.     
  13.         GeneratorClassLoader classLoader = new GeneratorClassLoader();    
  14.         Class clazz = classLoader.defineClassFromClassFile(    
  15.                 "org.victorzhzh.core.field.Person", classFile);    
  16.         Object obj = clazz.newInstance();    
  17.     
  18.         System.out.println(clazz.getDeclaredField("name").get(obj));//----(1)    
  19.         System.out.println(clazz.getDeclaredField("age").get(obj));//----(2)    
  20.     }    
  21. }    

 在这里,如果我们的age没有被添加进去那么(2)这个地方将会报错,看一下结果:

[plain] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. zhangzhuo    
  2. 0   

 int类型在没有被初始化时,默认值为0,而第二行输出0,说明我们添加了一个属性age

 

接下来,我们在visitField方法中在次添加age属性,如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class Transform extends ClassAdapter {    
  2.     
  3.     public Transform(ClassVisitor cv) {    
  4.         super(cv);    
  5.     }    
  6.     
  7.     @Override    
  8.     public FieldVisitor visitField(int access, String name, String desc,    
  9.             String signature, Object value) {    
  10.         cv.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class),    
  11.                 nullnull);    
  12.         return super.visitField(access, name, desc, signature, value);    
  13.     }    
  14.     
  15. }    

 这时,我们再次运行测试类,结果如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. java.lang.ClassFormatError: Duplicate field name&signature in class file org/victorzhzh/core/field/Person    
  2.     at java.lang.ClassLoader.defineClass1(Native Method)    
  3.     at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)    
  4.     at java.lang.ClassLoader.defineClass(ClassLoader.java:616)    
  5.     at java.lang.ClassLoader.defineClass(ClassLoader.java:466)    
  6.     at org.victorzhzh.common.GeneratorClassLoader.defineClassFromClassFile(GeneratorClassLoader.java:14)    
  7.     at org.victorzhzh.core.field.TransformTest.addAge(TransformTest.java:22)    
  8.     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)    
  9.     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)    
  10.     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)    
  11.     at java.lang.reflect.Method.invoke(Method.java:597)    
  12.     at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)    
  13.     at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)    
  14.     at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)    
  15.     at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)    
  16.     at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)    
  17.     at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)    
  18.     at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)    
  19.     at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)    
  20.     at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)    
  21.     at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)    
  22.     at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)    
  23.     at org.junit.runners.ParentRunner.run(ParentRunner.java:236)    
  24.     at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:49)    
  25.     at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)    
  26.     at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)    
  27.     at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)    
  28.     at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)    
  29.     at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)    

 很奇怪,怎么会属性名重复,我们看一下原类,

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public String name = "zhangzhuo";    
  2. public String address = "xxxxx" ;   

 没有重复的名字,而我们添加的是age也不重复,为什么会报重复属性名错误呢?

原因是,在我们的Transform类中的visitField方法,这个方法会在每次属性被访问时调用,而ASM在对这个类操作时会遍历到每个属性,也就是说有一个属性就会调用一次visitField方法,有两个属性就会调用两次visitField方法,所以当我们原类中有两个属性时visitField方法被调用了两次,因此创建了两个同名的age属性。

 

从这个例子中我们可以将visitInnerClass、visitField、visitMethod和visitEnd这些方法分成两组,一组是visitInnerClass、visitField、visitMethod,这些方法有可能会被多次调用,因此在这些方法中创建属性时要注意会重复创建;另一组是visitEnd,这个方法只有在最后才会被调用且只调用一次,所以在这个方法中添加属性是唯一的,因此一般添加属性选择在这个方法里编码。

    当然这里只给出了如何创建一个属性,其实修改,删除也都一样,根据上述知识大家可以参考ASM的源码即可掌握修改删除等操作。

 

附GeneratorClassLoader类代码

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class GeneratorClassLoader extends ClassLoader {    
  2.     
  3.     @SuppressWarnings("rawtypes")    
  4.     public Class defineClassFromClassFile(String className, byte[] classFile)    
  5.             throws ClassFormatError {    
  6.         return defineClass(className, classFile, 0, classFile.length);    
  7.     }    
  8. }   


ASM系列之五:操作类方法

前面我们了解了如何使用ASM的CoreAPI来操作一个类的属性,现在我们来看一下如何修改一个类方法。

场景:假设我们有一个Person类,它当中有一个sleep方法,我们希望监控一下这个sleep方法的运行时间:

一般我们会在代码里这样写:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public void sleep() {    
  2.     long timer = System.currentTimeMillis();   
  3.     
  4.     
  5.     try {    
  6.     System.out.println("我要睡一会...");    
  7.     TimeUnit.SECONDS.sleep(2);    
  8.     } catch (InterruptedException e) {    
  9.     e.printStackTrace();    
  10.     }    
  11.     System.out.println(System.currentTimeMillis()-timer);  
  12.     
  13.     
  14. }  

 标红的两行代码是我们希望有的,但是一般不会将这样的代码和业务代码耦合在一起,所以借助asm来实现动态的植入这样两行代码,就可以使业务方法很清晰。因此我们需要能够修改方法的API,在ASM中提供了对应的API,即MethodAdapter,使用这个API我们就可以随心所欲的修改方法中的字节码,甚至可以完全重写方法,当然这样是没有必要的。下面我们来看一下如何使用这个API,代码如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class ModifyMethod extends MethodAdapter {    
  2.     
  3.     public ModifyMethod(MethodVisitor mv, int access, String name, String desc) {    
  4.         super(mv);    
  5.     }    
  6.     
  7.     @Override    
  8.     public void visitCode() {    
  9.         mv.visitFieldInsn(Opcodes.GETSTATIC,    
  10.                 Type.getInternalName(Person.class), "timer""J");    
  11.         mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System",    
  12.                 "currentTimeMillis""()J");    
  13.         mv.visitInsn(Opcodes.LSUB);    
  14.         mv.visitFieldInsn(Opcodes.PUTSTATIC,    
  15.                 Type.getInternalName(Person.class), "timer""J");    
  16.     }    
  17.     
  18.     @Override    
  19.     public void visitInsn(int opcode) {    
  20.         if (opcode == Opcodes.RETURN) {    
  21.             mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System""out",    
  22.                     "Ljava/io/PrintStream;");    
  23.             mv.visitFieldInsn(Opcodes.GETSTATIC,    
  24.                     Type.getInternalName(Person.class), "timer""J");    
  25.             mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System",    
  26.                     "currentTimeMillis""()J");    
  27.             mv.visitInsn(Opcodes.LADD);    
  28.             mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",    
  29.                     "println""(J)V");    
  30.         }    
  31.         mv.visitInsn(opcode);    
  32.     }    
  33. }   

MethodAdapter类实现了MethodVisitor接口,在MethodVisitor接口中严格地规定了每个visitXXX的访问顺序,如下:

visitAnnotationDefault?( visitAnnotation | visitParameterAnnotation | visitAttribute )*( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |visitLocalVariable | visitLineNumber )*visitMaxs )?visitEnd

首先,统一一个概念,ASM访问,这里所说的ASM访问不是指ASM代码去调用某个类的具体方法,而是指去分析某个类的某个方法的二进制字节码。

在这里visitCode方法将会在ASM开始访问某一个方法时调用,因此这个方法一般可以用来在进入分析JVM字节码之前来新增一些字节码,visitXxxInsn是在ASM具体访问到每个指令时被调用,上面代码中我们使用的是visitInsn方法,它是ASM访问到无参数指令时调用的,这里我们判但了当前指令是否为无参数的return来在方法结束前添加一些指令。

通过重写visitCode和visitInsn两个方法,我们就实现了具体的业务逻辑被调用前和被调用后植入监控运行时间的代码。

 

ModifyMethod类只是对方法的修改类,那如何让外部类调用它,要通过我们上一篇中使用过的类,ClassAdapter的一个子类,在这里我们定义一个ModifyMethodClassAdapter类,代码如下:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class ModifyMethodClassAdapter extends ClassAdapter {    
  2.     
  3.     public ModifyMethodClassAdapter(ClassVisitor cv) {    
  4.         super(cv);    
  5.     }    
  6.     
  7.     @Override    
  8.     public MethodVisitor visitMethod(int access, String name, String desc,    
  9.             String signature, String[] exceptions) {    
  10.         if (name.equals("sleep")) {    
  11.             return new ModifyMethod(super.visitMethod(access, name, desc,    
  12.                     signature, exceptions), access, name, desc);    
  13.         }    
  14.         return super.visitMethod(access, name, desc, signature, exceptions);    
  15.     }    
  16.     
  17.     @Override    
  18.     public void visitEnd() {    
  19.         cv.visitField(Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC, "timer""J",    
  20.                 nullnull);    
  21.     }    
  22.     
  23. }   

 上述代码中我们使用visitEnd来添加了一个timer属性,用于记录时间,我们重写了visitMethod方法,当ASM访问的方法是sleep方法时,我们调用已经定义的ModifyMethod方法,让这个方法作为访问者,去访问对应的方法。

这样两个类就实现了我们要的添加执行时间的业务。

看一下测试类:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class ModifyMethodTest {    
  2.     @Test    
  3.     public void modiySleepMethod() throws Exception {    
  4.         ClassReader classReader = new ClassReader(    
  5.                 "org.victorzhzh.common.Person");    
  6.         ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);    
  7.         ClassAdapter classAdapter = new ModifyMethodClassAdapter(classWriter);    
  8.         classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);    
  9.     
  10.         byte[] classFile = classWriter.toByteArray();    
  11.     
  12.         GeneratorClassLoader classLoader = new GeneratorClassLoader();    
  13.         @SuppressWarnings("rawtypes")    
  14.         Class clazz = classLoader.defineClassFromClassFile(    
  15.                 "org.victorzhzh.common.Person", classFile);    
  16.         Object obj = clazz.newInstance();    
  17.         System.out.println(clazz.getDeclaredField("name").get(obj));    
  18.         clazz.getDeclaredMethod("sleep").invoke(obj);    
  19.     }    
  20. }   

 通过反射机制调用我们修改后的Person类,运行结果如下:

[plain] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. zhangzhuo    
  2. 我要睡一会...    
  3. 2023   

2023就是我们让sleep方法沉睡的时间,看一下我们的原始Person类:

[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public class Person {    
  2.     public String name = "zhangzhuo";    
  3.     
  4.     public void sayHello() {    
  5.         System.out.println("Hello World!");    
  6.     }    
  7.     
  8.     public void sleep() {    
  9.         try {    
  10.             System.out.println("我要睡一会...");    
  11.             TimeUnit.SECONDS.sleep(2);//沉睡两秒    
  12.         } catch (InterruptedException e) {    
  13.             e.printStackTrace();    
  14.         }    
  15.     }    
  16. }   

以上几篇文章都是关于ASM的大体介绍,ASM的功能可以说是十分强大,要学好这个东西个人有几点体会:

第一、要熟悉Java字节码结构,及指令:因为我们在很多时候都是要写最原始的字节吗指令的,虽然ASM也为我们提供相应的简化API替我们来做这些事情,但是最基本的东西还是要了解和掌握的,这样才能使用的更好;

第二、充分理解访问者模式有助于我们理解ASM的CoreAPI;

第三、掌握基本的ClassVisitor、ClassAdapter、MethodVisitor、MethodAdapter、FieldVisitor、FieldWriter、ClassReader和ClassWriter这几个类对全面掌握CoreAPI可以有很大的帮助;

第四、在掌握了CoreAPI后再去研究TreeAPI,这样更快速;

最后,希望这几篇文章能对研究ASM的朋友有所帮助


转载自:http://victorzhzh.iteye.com/category/140253

参考:http://blog.csdn.net/zhangjg_blog/article/details/22976929

http://my.oschina.net/u/1166271/blog/162796

阅读更多
换一批

没有更多推荐了,返回首页