AOP 的利器:ASM 3.0 介绍
http://www.ibm.com/developerworks/cn/java/j-lo-asm30/
一、什么是ASM
ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
使用ASM框架需要导入asm的jar包,下载链接:asm-3.2.jar。
二、如何使用ASM
ASM框架中的核心类有以下几个:
① ClassReader:该类用来解析编译过的class字节码文件。
② ClassWriter:该类用来重新构建编译后的类,比如说修改类名、属性以及方法,甚至可以生成新的类的字节码文件。
③ ClassAdapter:该类也实现了ClassVisitor接口,它将对它的方法调用委托给另一个ClassVisitor对象。
示例1.通过asm生成类的字节码
1 package com.asm3; 2 3 import java.io.File; 4 import java.io.FileNotFoundException; 5 import java.io.FileOutputStream; 6 import java.io.IOException; 7 8 import org.objectweb.asm.ClassWriter; 9 import org.objectweb.asm.Opcodes; 10 11 /** 12 * 通过asm生成类的字节码 13 * @author Administrator 14 * 15 */ 16 public class GeneratorClass { 17 18 public static void main(String[] args) throws IOException { 19 //生成一个类只需要ClassWriter组件即可 20 ClassWriter cw = new ClassWriter(0); 21 //通过visit方法确定类的头部信息 22 cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT+Opcodes.ACC_INTERFACE, 23 "com/asm3/Comparable", null, "java/lang/Object", new String[]{"com/asm3/Mesurable"}); 24 //定义类的属性 25 cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC, 26 "LESS", "I", null, new Integer(-1)).visitEnd(); 27 cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC, 28 "EQUAL", "I", null, new Integer(0)).visitEnd(); 29 cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC, 30 "GREATER", "I", null, new Integer(1)).visitEnd(); 31 //定义类的方法 32 cw.visitMethod(Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT, "compareTo", 33 "(Ljava/lang/Object;)I", null, null).visitEnd(); 34 cw.visitEnd(); //使cw类已经完成 35 //将cw转换成字节数组写到文件里面去 36 byte[] data = cw.toByteArray(); 37 File file = new File("D://Comparable.class"); 38 FileOutputStream fout = new FileOutputStream(file); 39 fout.write(data); 40 fout.close(); 41 } 42 }
生成一个类的字节码文件只需要用到ClassWriter类即可,生成Comparable.class后用javap指令对其进行反编译:javap -c Comparable.class >test.txt ,编译后的结果如下:
1 public interface com.asm3.Comparable extends com.asm3.Mesurable { 2 public static final int LESS; 3 4 public static final int EQUAL; 5 6 public static final int GREATER; 7 8 public abstract int compareTo(java.lang.Object); 9 }
注:一个编译后的java类不包含package和import段,因此在class文件中所有的类型都使用的是全路径。
示例2.修改类的字节码文件
C.java
1 package com.asm5; 2 3 public class C { 4 public void m() throws InterruptedException{ 5 Thread.sleep(100); 6 } 7 }
将C.java类的内容改为如下:
1 package com.asm5; 2 3 public class C { 4 public static long timer; 5 public void m() throws InterruptedException{ 6 timer -= System.currentTimeMillis(); 7 Thread.sleep(100); 8 timer += System.currentTimeMillis(); 9 } 10 }
为了弄清楚ASM是如何实现的,我们先编译这两个类,然后比对它们的TraceClassVisitor的输出,我们可以发现如下的不同(粗体表示)
GETSTATIC C.timer : J
INVOKESTATIC java/lang/System.currentTimilis()J
LSUB
PUTSTATIC C.timer : J
LDC 100
INVOKESTATIC java/lang/Thread.sleep(J)V
GETSTATIC C.timer : J
INVOKESTATIC java/lang/System.currentTimilis()J
LADD
PUTSTATIC C.timer : J
RETURN
MAXSTACK=4
MAXLOCALS=1
通过比对上面的指令,我们可以发现必须在m()方法的最前面增加四条指令,在RETURN指令前也增加四条指令,同时这四条必须位于xRETURN和ATHROW之前,因为这些指令都会结束方法的执行。
具体代码如下:
AddTimeClassAdapter.java
1 package com.asm5; 2 3 import org.objectweb.asm.ClassAdapter; 4 import org.objectweb.asm.ClassVisitor; 5 import org.objectweb.asm.FieldVisitor; 6 import org.objectweb.asm.MethodAdapter; 7 import org.objectweb.asm.MethodVisitor; 8 import org.objectweb.asm.Opcodes; 9 10 public class AddTimeClassAdapter extends ClassAdapter { 11 private String owner; 12 private boolean isInterface; 13 public AddTimeClassAdapter(ClassVisitor cv) { 14 super(cv); 15 } 16 @Override 17 public void visit(int version, int access, String name, String signature, 18 String superName, String[] interfaces) { 19 cv.visit(version, access, name, signature, superName, interfaces); 20 owner = name; 21 isInterface = (access & Opcodes.ACC_INTERFACE) != 0; 22 } 23 @Override 24 public MethodVisitor visitMethod(int access, String name, String desc, 25 String signature, String[] exceptions) { 26 MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); 27 if(!name.equals("<init>") && !isInterface && mv!=null){ 28 //为方法添加计时功能 29 mv = new AddTimeMethodAdapter(mv); 30 } 31 return mv; 32 } 33 @Override 34 public void visitEnd() { 35 //添加字段 36 if(!isInterface){ 37 FieldVisitor fv = cv.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_STATIC, "timer", "J", null, null); 38 if(fv!=null){ 39 fv.visitEnd(); 40 } 41 } 42 cv.visitEnd(); 43 } 44 45 class AddTimeMethodAdapter extends MethodAdapter{ 46 public AddTimeMethodAdapter(MethodVisitor mv) { 47 super(mv); 48 } 49 @Override 50 public void visitCode() { 51 mv.visitCode(); 52 mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J"); 53 mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J"); 54 mv.visitInsn(Opcodes.LSUB); 55 mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J"); 56 } 57 @Override 58 public void visitInsn(int opcode) { 59 if((opcode>=Opcodes.IRETURN && opcode<=Opcodes.RETURN) || opcode==Opcodes.ATHROW){ 60 mv.visitFieldInsn(Opcodes.GETSTATIC, owner, "timer", "J"); 61 mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J"); 62 mv.visitInsn(Opcodes.LADD); 63 mv.visitFieldInsn(Opcodes.PUTSTATIC, owner, "timer", "J"); 64 } 65 mv.visitInsn(opcode); 66 } 67 @Override 68 public void visitMaxs(int maxStack, int maxLocal) { 69 mv.visitMaxs(maxStack+4, maxLocal); 70 } 71 } 72 73 }
Generator.java
1 package com.asm5; 2 3 import java.io.File; 4 import java.io.FileNotFoundException; 5 import java.io.FileOutputStream; 6 import java.io.IOException; 7 8 import org.objectweb.asm.ClassAdapter; 9 import org.objectweb.asm.ClassReader; 10 import org.objectweb.asm.ClassWriter; 11 12 13 14 public class Generator { 15 16 public static void main(String[] args){ 17 try { 18 ClassReader cr = new ClassReader("com/asm5/C"); 19 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 20 ClassAdapter classAdapter = new AddTimeClassAdapter(cw); 21 //使给定的访问者访问Java类的ClassReader 22 cr.accept(classAdapter, ClassReader.SKIP_DEBUG); 23 byte[] data = cw.toByteArray(); 24 File file = new File(System.getProperty("user.dir") + "\\WebRoot\\WEB-INF\\classes\\com\\asm5\\C.class"); 25 FileOutputStream fout = new FileOutputStream(file); 26 fout.write(data); 27 fout.close(); 28 System.out.println("success!"); 29 } catch (FileNotFoundException e) { 30 e.printStackTrace(); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } 34 } 35 36 }
下面是一个测试类:
1 package com.asm5; 2 3 public class Test { 4 public static void main(String[] args) throws InterruptedException, NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { 5 C c = new C(); 6 c.m(); 7 Class cc = c.getClass(); 8 System.out.println(cc.getField("timer").get(c)); 9 } 10 }
输出结果为:100
一、什么是ASM
ASM是一个JAVA字节码分析、创建和修改的开源应用框架。在ASM中提供了诸多的API用于对类的内容进行字节码操作的方法。与传统的BCEL和SERL不同,在ASM中提供了更为优雅和灵活的操作字节码的方式。目前ASM已被广泛的开源应用架构所使用,例如:Spring、Hibernate等。
二、ASM能干什么
分析一个类、从字节码角度创建一个类、修改一个已经被编译过的类文件
三、ASM初探例子
这里我们使用ASM的CoreAPI(ASM提供了两组API:Core和Tree,Core是基于访问者模式来操作类的,而Tree是基于树节点来操作类的)创建一个MyClass类,目标类如下:
- public class MyClass {
- private String name;
- public Myclass(){
- this.name = "zhangzhuo";
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
这个类在构造方法中初始化了属性name,并提供了两个public方法来修改和访问name属性。
接下来就要书写创建这个类的代码了,现将代码给出,然后逐步解释,代码如下:
代码1:
- public class GenerateClass {
- public void generateClass() {
- //方法的栈长度和本地变量表长度用户自己计算
- ClassWriter classWriter = new ClassWriter(0);
- //Opcodes.V1_6指定类的版本
- //Opcodes.ACC_PUBLIC表示这个类是public,
- //“org/victorzhzh/core/classes/MyClass”类的全限定名称
- //第一个null位置变量定义的是泛型签名,
- //“java/lang/Object”这个类的父类
- //第二个null位子的变量定义的是这个类实现的接口
- classWriter.visit(Opcodes.V1_6, Opcodes.ACC_PUBLIC,
- "org/victorzhzh/core/classes/MyClass", null,
- "java/lang/Object", null);
- ClassAdapter classAdapter = new MyClassAdapter(classWriter);
- classAdapter.visitField(Opcodes.ACC_PRIVATE, "name",
- Type.getDescriptor(String.class), null, null);//定义name属性
- classAdapter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null,
- null).visitCode();//定义构造方法
- String setMethodDesc = "(" + Type.getDescriptor(String.class) + ")V";
- classAdapter.visitMethod(Opcodes.ACC_PUBLIC, "setName", setMethodDesc,
- null, null).visitCode();//定义setName方法
- String getMethodDesc = "()" + Type.getDescriptor(String.class);
- classAdapter.visitMethod(Opcodes.ACC_PUBLIC, "getName", getMethodDesc,
- null, null).visitCode();//定义getName方法
- byte[] classFile = classWriter.toByteArray();//生成字节码
- MyClassLoader classLoader = new MyClassLoader();//定义一个类加载器
- Class clazz = classLoader.defineClassFromClassFile(
- "org.victorzhzh.core.classes.MyClass", classFile);
- try {//利用反射方式,访问getName
- Object obj = clazz.newInstance();
- Method method = clazz.getMethod("getName");
- System.out.println(obj.toString());
- System.out.println(method.invoke(obj, null));
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- class MyClassLoader extends ClassLoader {
- public Class defineClassFromClassFile(String className, byte[] classFile)
- throws ClassFormatError {
- return defineClass(className, classFile, 0, classFile.length);
- }
- }
- public static void main(String[] args) {
- GenerateClass generateClass = new GenerateClass();
- generateClass.generateClass();
- }
- }
代码2:
- public class MyClassAdapter extends ClassAdapter {
- public MyClassAdapter(ClassVisitor cv) {
- super(cv);
- }
- @Override
- public MethodVisitor visitMethod(int access, String name, String desc,
- String signature, String[] exceptions) {
- MethodVisitor methodVisitor = cv.visitMethod(access, name, desc,
- signature, exceptions);
- if (name.equals("<init>")) {
- return new InitMethodAdapter(methodVisitor);
- } else if (name.equals("setName")) {
- return new SetMethodAdapter(methodVisitor);
- } else if (name.equals("getName")) {
- return new GetMethodAdapter(methodVisitor);
- } else {
- return super.visitMethod(access, name, desc, signature, exceptions);
- }
- }
- //这个类生成具体的构造方法字节码
- class InitMethodAdapter extends MethodAdapter {
- public InitMethodAdapter(MethodVisitor mv) {
- super(mv);
- }
- @Override
- public void visitCode() {
- mv.visitVarInsn(Opcodes.ALOAD, 0);
- mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object",
- "<init>", "()V");//调用父类的构造方法
- mv.visitVarInsn(Opcodes.ALOAD, 0);
- mv.visitLdcInsn("zhangzhuo");//将常量池中的字符串常量加载刀栈顶
- mv.visitFieldInsn(Opcodes.PUTFIELD,
- "org/victorzhzh/core/classes/MyClass", "name",
- Type.getDescriptor(String.class));//对name属性赋值
- mv.visitInsn(Opcodes.RETURN);//设置返回值
- mv.visitMaxs(2, 1);//设置方法的栈和本地变量表的大小
- }
- };
- //这个类生成具体的setName方法字节码
- class SetMethodAdapter extends MethodAdapter {
- public SetMethodAdapter(MethodVisitor mv) {
- super(mv);
- }
- @Override
- public void visitCode() {
- mv.visitVarInsn(Opcodes.ALOAD, 0);
- mv.visitVarInsn(Opcodes.ALOAD, 1);
- mv.visitFieldInsn(Opcodes.PUTFIELD,
- "org/victorzhzh/core/classes/MyClass", "name",
- Type.getDescriptor(String.class));
- mv.visitInsn(Opcodes.RETURN);
- mv.visitMaxs(2, 2);
- }
- }
- //这个类生成具体的getName方法字节
- class GetMethodAdapter extends MethodAdapter {
- public GetMethodAdapter(MethodVisitor mv) {
- super(mv);
- }
- @Override
- public void visitCode() {
- mv.visitVarInsn(Opcodes.ALOAD, 0);
- mv.visitFieldInsn(Opcodes.GETFIELD,
- "org/victorzhzh/core/classes/MyClass", "name",
- Type.getDescriptor(String.class));//获取name属性的值
- mv.visitInsn(Opcodes.ARETURN);//返回一个引用,这里是String的引用即name
- mv.visitMaxs(1, 1);
- }
- }
- }
运行结果:
- org.victorzhzh.core.classes.MyClass@1270b73
- zhangzhuo
这个例子只是简单地介绍了一下ASM如何创建一个类,接下来的几个章节,将详细介绍ASM的CoreAPI和TreeAPI中如何操作类。
上一篇文章中我们看到了如何使用ASM生成一个简单的JAVA类,里面使用到了很多的基本概念,比如:方法描述、引用描述等,下面将一一介绍。
一、类版本:
一个Java二进制的类文件,都有一个版本,因此ASM中提供了几个常量来指定一个类的版,这些常量定义在org.objectweb.asm.Opcodes接口中,如下:
- int V1_1 = 3 << 16 | 45;
- int V1_2 = 0 << 16 | 46;
- int V1_3 = 0 << 16 | 47;
- int V1_4 = 0 << 16 | 48;
- int V1_5 = 0 << 16 | 49;
- int V1_6 = 0 << 16 | 50;
- int V1_7 = 0 << 16 | 51;
二、内部名字:
在Java二进制文件中使用的是JVM的内部名字,而不是我们所熟悉的以“.”分割的全限定名,内部名字是以“/”替代“.”的全名,例如:java.lang.String在JVM中的内部名字是java/lang/String。在ASM中可以使用org.objectweb.asm.Type类中的静态方法getInternalName(final Class c) 来获得,如下:
- public class InternalNameTransform {
- public static void main(String[] args) {
- System.out.println(Type.getInternalName(String.class));
- System.out.println(Type.getInternalName(Integer.class));
- System.out.println(Type.getInternalName(InternalNameTransform.class));
- }
- }
运行结果:
- java/lang/String
- java/lang/Integer
- 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)方法,如下:
- public class TypeDescriptors {
- public static void main(String[] args) {
- System.out.println(Type.getDescriptor(TypeDescriptors.class));
- System.out.println(Type.getDescriptor(String.class));
- }
- }
运行结果:
- Lorg/victorzhzh/core/structure/TypeDescriptors;
- 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方法,如下:
- public class MethodDescriptors {
- public static void main(String[] args) throws Exception {
- Method m = String.class.getMethod("substring", int.class);
- System.out.println(Type.getMethodDescriptor(m));
- }
- }
运行结果:
- (I)Ljava/lang/String;
其实在org.objectweb.asm.Type类中提供了很多方法让我们去了解一个类,有兴趣的可以看一下它的源码,这对我们了解一个类和操作一个类还是有大帮助的。
在ASM的Core API中使用的是访问者模式来实现对类的操作,主要包含如下类:
一、ClassVisitor接口:
在这个接口中主要提供了和类结构同名的一些方法,这些方法可以对相应的类结构进行操作。如下:
- public interface ClassVisitor {
- void visit(int version,int access,String name,String signature,String superName,String[] interfaces);
- void visitSource(String source, String debug);
- void visitOuterClass(String owner, String name, String desc);
- AnnotationVisitor visitAnnotation(String desc, boolean visible);
- void visitAttribute(Attribute attr);
- void visitInnerClass(String name,String outerName,String innerName,int access);
- FieldVisitor visitField(int access,String name,String desc,String signature,Object value);
- MethodVisitor visitMethod(int access,String name,String desc,String signature,String[] exceptions);
- void visitEnd();
- }
这里定义的方法调用是有顺序的,在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中的定义,如下:
- public interface AnnotationVisitor {
- void visit(String name, Object value);
- void visitEnum(String name, String desc, String value);
- AnnotationVisitor visitAnnotation(String name, String desc);
- AnnotationVisitor visitArray(String name);
- void visitEnd();
- }
调用顺序如下:
(visit | visitEnum | visitAnnotation | visitArray)* visitEnd
六、FieldVisitor接口:
这个接口定义了和属性结构相对应的方法,这些方法可以操作属性,如下:
- public interface FieldVisitor {
- AnnotationVisitor visitAnnotation(String desc, boolean visible);
- void visitAttribute(Attribute attr);
- void visitEnd();
- }
调用顺序:
( visitAnnotation | visitAttribute )* visitEnd .
七、MethodVisitor接口:
这个接口定义了和方法结构相对应的方法,这些方法可以去操作源方法,具体的可以查看一下源码。
八、操作流程:
一般情况下,我们需要操作一个类时,首先是获得其二进制的字节码,即用ClassReader来读取一个类,然后需要一个能将二进制字节码写回的类,即用ClassWriter类,最后就是一个事件过滤器,即ClassAdapter。事件过滤器中的某些方法可以产生一个新的XXXVisitor对象,当我们需要修改对应的内容时只要实现自己的XXXVisitor并返回就可以了。
九、例子:
在这个例子中,我们将对Person类的sayName方法做出一些修改,源类:
- public class Person {
- private String name;
- public void sayName() {
- System.out.println(name);
- }
- }
如果我们定义一个Person类然后调用其sayName()方法将会得到的是一个null,行成的二进制字节码如下:
- public void sayName();
- Code:
- Stack=2, Locals=1, Args_size=1
- <span style="color: #ff0000;">0: getstatic #17; //Field java/lang/System.out:Ljava/io/PrintStream;
- 3: aload_0
- 4: getfield #23; //Field name:Ljava/lang/String;
- 7: invokevirtual #25; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 10: return</span>
- }
我们修改一下这个方法,让它输出"zhangzhuo",代码如下:
- public class GenerateNewPerson {
- public static void main(String[] args) throws Exception {
- // 使用全限定名,创建一个ClassReader对象
- ClassReader classReader = new ClassReader(
- "org.victorzhzh.core.ic.Person");
- // 构建一个ClassWriter对象,并设置让系统自动计算栈和本地变量大小
- ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
- ClassAdapter classAdapter = new GeneralClassAdapter(classWriter);
- classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);
- byte[] classFile = classWriter.toByteArray();
- // 将这个类输出到原先的类文件目录下,这是原先的类文件已经被修改
- File file = new File(
- "target/classes/org/victorzhzh/core/ic/Person.class");
- FileOutputStream stream = new FileOutputStream(file);
- stream.write(classFile);
- stream.close();
- }
- }
- public class GeneralClassAdapter extends ClassAdapter {
- public GeneralClassAdapter(ClassVisitor cv) {
- super(cv);
- }
- @Override
- public MethodVisitor visitMethod(int access, String name, String desc,
- String signature, String[] exceptions) {
- MethodVisitor mv = cv.visitMethod(access, name, desc, signature,
- exceptions);
- // 当是sayName方法是做对应的修改
- if (name.equals("sayName")) {
- MethodVisitor newMv = new SayNameMethodAdapter(mv);
- return newMv;
- } else {
- return mv;
- }
- }
- // 定义一个自己的方法访问类
- class SayNameMethodAdapter extends MethodAdapter {
- public SayNameMethodAdapter(MethodVisitor mv) {
- super(mv);
- }
- // 在源方法前去修改方法内容,这部分的修改将加载源方法的字节码之前
- @Override
- public void visitCode() {
- // 记载隐含的this对象,这是每个JAVA方法都有的
- mv.visitVarInsn(Opcodes.ALOAD, 0);
- // 从常量池中加载“zhangzhuo”字符到栈顶
- mv.visitLdcInsn("zhangzhuo");
- // 将栈顶的"zhangzhuo"赋值给name属性
- mv.visitFieldInsn(Opcodes.PUTFIELD,
- Type.getInternalName(Person.class), "name",
- Type.getDescriptor(String.class));
- }
- }
- }
这时,我们在查看一下Person的字节码:
- public void sayName();
- Code:
- Stack=2, Locals=1, Args_size=1
- 0: aload_0
- 1: ldc #13; //String zhangzhuo
- 3: putfield #15; //Field name:Ljava/lang/String;
- =============以上是我们新增加的内容================================
- 6: getstatic #21; //Field java/lang/System.out:Ljava/io/PrintStream;
- 9: aload_0
- 10: getfield #15; //Field name:Ljava/lang/String;
- 13: invokevirtual #27; //Method java/io/PrintStream.println:(Ljava/lang/String;)V
- 16: return
- }
再次调用Person对象,输出结果为:zhangzhuo
在上一篇文章中,我们看到了ASM中的Core API中使用的是XXXVisitor操作类中的对应部分。本文将展示如何使用ASM中的Core API对类的属性的操作。
首先,我们定义一个原类Person,如下:
- public class Person {
- public String name = "zhangzhuo";
- public String address = "xxxxx" ;
- }
这里,我们将属性定义为public类型,目的是为了我们使用反射去调用这个属性,接下来我们要为这个类添加一个int类型的属性,名字叫age。
第一个问题,ASM的Core API允许我们在那些方法中来添加属性?
在ASM的Core API中你要为类添加属性就必须要自己去实现ClassVisitor这个接口,这个接口中的visitInnerClass、visitField、visitMethod和visitEnd方法允许我们进行添加一个类属性操作,其余的方法是不允许的。这里我们依然使用Core API中的ClassAdapter类,我们继承这个类,定义一个去添加属性的类,ClassAdapter实现了ClassVisitor。
第二个问题,我们要在这些方法中写什么样的代码才能添加一个属性?
在使用ASM的Core API添加一个属性时只需要调用一句语句就可以,如下:
- classVisitor.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class),
- null, null);
第一个参数指定的是这个属性的操作权限,第二个参数是属性名,第三个参数是类型描述,第四个参数是泛型类型,第五个参数是初始化的值,这里需要注意一下的是第五个参数,这个参数只有属性为static时才有效,也就是数只有为static时,这个值才真正会赋值给我们添加的属性上,对于非static属性,它将被忽略。
好了,让我们看一段代码,在visitEnd去添加一个名字为age的属性:
- public class Transform extends ClassAdapter {
- public Transform(ClassVisitor cv) {
- super(cv);
- }
- @Override
- public void visitEnd() {
- cv.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class),
- null, null);
- }
- }
非常简单吧,只要一句话就可以添加一个属性到我们的类中,看一下我们的测试类:
- public class TransformTest {
- @Test
- public void addAge() throws Exception {
- ClassReader classReader = new ClassReader(
- "org.victorzhzh.core.field.Person");
- ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
- ClassAdapter classAdapter = new Transform(classWriter);
- classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);
- byte[] classFile = classWriter.toByteArray();
- GeneratorClassLoader classLoader = new GeneratorClassLoader();
- Class clazz = classLoader.defineClassFromClassFile(
- "org.victorzhzh.core.field.Person", classFile);
- Object obj = clazz.newInstance();
- System.out.println(clazz.getDeclaredField("name").get(obj));//----(1)
- System.out.println(clazz.getDeclaredField("age").get(obj));//----(2)
- }
- }
在这里,如果我们的age没有被添加进去那么(2)这个地方将会报错,看一下结果:
- zhangzhuo
- 0
int类型在没有被初始化时,默认值为0,而第二行输出0,说明我们添加了一个属性age
接下来,我们在visitField方法中在次添加age属性,如下:
- public class Transform extends ClassAdapter {
- public Transform(ClassVisitor cv) {
- super(cv);
- }
- @Override
- public FieldVisitor visitField(int access, String name, String desc,
- String signature, Object value) {
- cv.visitField(Opcodes.ACC_PUBLIC, "age", Type.getDescriptor(int.class),
- null, null);
- return super.visitField(access, name, desc, signature, value);
- }
- }
这时,我们再次运行测试类,结果如下:
- java.lang.ClassFormatError: Duplicate field name&signature in class file org/victorzhzh/core/field/Person
- at java.lang.ClassLoader.defineClass1(Native Method)
- at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
- at java.lang.ClassLoader.defineClass(ClassLoader.java:616)
- at java.lang.ClassLoader.defineClass(ClassLoader.java:466)
- at org.victorzhzh.common.GeneratorClassLoader.defineClassFromClassFile(GeneratorClassLoader.java:14)
- at org.victorzhzh.core.field.TransformTest.addAge(TransformTest.java:22)
- at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
- at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
- at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
- at java.lang.reflect.Method.invoke(Method.java:597)
- at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
- at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
- at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
- at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
- at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
- at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
- at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
- at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
- at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
- at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
- at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
- at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
- at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:49)
- at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
- at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
- at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
- at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
- at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
很奇怪,怎么会属性名重复,我们看一下原类,
- public String name = "zhangzhuo";
- public String address = "xxxxx" ;
没有重复的名字,而我们添加的是age也不重复,为什么会报重复属性名错误呢?
原因是,在我们的Transform类中的visitField方法,这个方法会在每次属性被访问时调用,而ASM在对这个类操作时会遍历到每个属性,也就是说有一个属性就会调用一次visitField方法,有两个属性就会调用两次visitField方法,所以当我们原类中有两个属性时visitField方法被调用了两次,因此创建了两个同名的age属性。
从这个例子中我们可以将visitInnerClass、visitField、visitMethod和visitEnd这些方法分成两组,一组是visitInnerClass、visitField、visitMethod,这些方法有可能会被多次调用,因此在这些方法中创建属性时要注意会重复创建;另一组是visitEnd,这个方法只有在最后才会被调用且只调用一次,所以在这个方法中添加属性是唯一的,因此一般添加属性选择在这个方法里编码。
当然这里只给出了如何创建一个属性,其实修改,删除也都一样,根据上述知识大家可以参考ASM的源码即可掌握修改删除等操作。
附GeneratorClassLoader类代码
- public class GeneratorClassLoader extends ClassLoader {
- @SuppressWarnings("rawtypes")
- public Class defineClassFromClassFile(String className, byte[] classFile)
- throws ClassFormatError {
- return defineClass(className, classFile, 0, classFile.length);
- }
- }
前面我们了解了如何使用ASM的CoreAPI来操作一个类的属性,现在我们来看一下如何修改一个类方法。
场景:假设我们有一个Person类,它当中有一个sleep方法,我们希望监控一下这个sleep方法的运行时间:
一般我们会在代码里这样写:
- public void sleep() {
- <span style="color: #ff0000;">long timer = System.currentTimeMillis();</span>
- try {
- System.out.println("我要睡一会...");
- TimeUnit.SECONDS.sleep(2);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- <span style="color: #ff0000;">System.out.println(System.currentTimeMillis()-timer);</span>
- }
标红的两行代码是我们希望有的,但是一般不会将这样的代码和业务代码耦合在一起,所以借助asm来实现动态的植入这样两行代码,就可以使业务方法很清晰。因此我们需要能够修改方法的API,在ASM中提供了对应的API,即MethodAdapter,使用这个API我们就可以随心所欲的修改方法中的字节码,甚至可以完全重写方法,当然这样是没有必要的。下面我们来看一下如何使用这个API,代码如下:
- public class ModifyMethod extends MethodAdapter {
- public ModifyMethod(MethodVisitor mv, int access, String name, String desc) {
- super(mv);
- }
- @Override
- public void visitCode() {
- mv.visitFieldInsn(Opcodes.GETSTATIC,
- Type.getInternalName(Person.class), "timer", "J");
- mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System",
- "currentTimeMillis", "()J");
- mv.visitInsn(Opcodes.LSUB);
- mv.visitFieldInsn(Opcodes.PUTSTATIC,
- Type.getInternalName(Person.class), "timer", "J");
- }
- @Override
- public void visitInsn(int opcode) {
- if (opcode == Opcodes.RETURN) {
- mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
- "Ljava/io/PrintStream;");
- mv.visitFieldInsn(Opcodes.GETSTATIC,
- Type.getInternalName(Person.class), "timer", "J");
- mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System",
- "currentTimeMillis", "()J");
- mv.visitInsn(Opcodes.LADD);
- mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
- "println", "(J)V");
- }
- mv.visitInsn(opcode);
- }
- }
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类,代码如下:
- public class ModifyMethodClassAdapter extends ClassAdapter {
- public ModifyMethodClassAdapter(ClassVisitor cv) {
- super(cv);
- }
- @Override
- public MethodVisitor visitMethod(int access, String name, String desc,
- String signature, String[] exceptions) {
- if (name.equals("sleep")) {
- return new ModifyMethod(super.visitMethod(access, name, desc,
- signature, exceptions), access, name, desc);
- }
- return super.visitMethod(access, name, desc, signature, exceptions);
- }
- @Override
- public void visitEnd() {
- cv.visitField(Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC, "timer", "J",
- null, null);
- }
- }
上述代码中我们使用visitEnd来添加了一个timer属性,用于记录时间,我们重写了visitMethod方法,当ASM访问的方法是sleep方法时,我们调用已经定义的ModifyMethod方法,让这个方法作为访问者,去访问对应的方法。
这样两个类就实现了我们要的添加执行时间的业务。
看一下测试类:
- public class ModifyMethodTest {
- @Test
- public void modiySleepMethod() throws Exception {
- ClassReader classReader = new ClassReader(
- "org.victorzhzh.common.Person");
- ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
- ClassAdapter classAdapter = new ModifyMethodClassAdapter(classWriter);
- classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);
- byte[] classFile = classWriter.toByteArray();
- GeneratorClassLoader classLoader = new GeneratorClassLoader();
- @SuppressWarnings("rawtypes")
- Class clazz = classLoader.defineClassFromClassFile(
- "org.victorzhzh.common.Person", classFile);
- Object obj = clazz.newInstance();
- System.out.println(clazz.getDeclaredField("name").get(obj));
- clazz.getDeclaredMethod("sleep").invoke(obj);
- }
- }
通过反射机制调用我们修改后的Person类,运行结果如下:
- zhangzhuo
- 我要睡一会...
- 2023
2023就是我们让sleep方法沉睡的时间,看一下我们的原始Person类:
- public class Person {
- public String name = "zhangzhuo";
- public void sayHello() {
- System.out.println("Hello World!");
- }
- public void sleep() {
- try {
- System.out.println("我要睡一会...");
- TimeUnit.SECONDS.sleep(2);//沉睡两秒
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
以上几篇文章都是关于ASM的大体介绍,ASM的功能可以说是十分强大,要学好这个东西个人有几点体会:
第一、要熟悉Java字节码结构,及指令:因为我们在很多时候都是要写最原始的字节吗指令的,虽然ASM也为我们提供相应的简化API替我们来做这些事情,但是最基本的东西还是要了解和掌握的,这样才能使用的更好;
第二、充分理解访问者模式有助于我们理解ASM的CoreAPI;
第三、掌握基本的ClassVisitor、ClassAdapter、MethodVisitor、MethodAdapter、FieldVisitor、FieldWriter、ClassReader和ClassWriter这几个类对全面掌握CoreAPI可以有很大的帮助;
第四、在掌握了CoreAPI后再去研究TreeAPI,这样更快速;
最后,希望这几篇文章能对研究ASM的朋友有所帮助
问题的提出
在大部分情况下,需要多重继承往往意味着糟糕的设计。但在处理一些遗留项目的时候,多重继承可能是我们能做出的选择中代价最小的。由于 Java 语言本身不支持多重继承,这常常会给我们带来麻烦,最后的结果可能就是大量的重复代码。本文试图使用 ASM 框架来解决这一问题。在扩展类的功能的同时,不产生任何重复代码。
考虑如下的实际情况:有一组类,名为 SubClass1、SubClass2、SubClass3 和 SubClass4,它们共同继承了同一个父类 SuperClass。现在,我们需要这组类中的一部分,例如 SubClass1 和SubClass2,这两个类还要实现另外两个接口,它们分别为:IFibonacciComputer 和ITimeRetriever。然而,这两个接口已经有了各自的实现类 FibonacciComputer 和TimeRetriever。并且这两个类的实现逻辑就是我们想要的,我们不想做任何改动,只希望在SubClass1 和 SubClass2 两个类中包含这些实现逻辑。
它们的结构如图 1 所示:
图 1. 结构类图
由于 SubClass1,SubClass2 已经继承了 SuperClass,所以我们无法让它们再继承FibonacciComputer 或 TimeRetriever。
所以,想要它们再实现 IFibonacciComputer 和 ITimeRetriever 这两个接口,必然会产生重复代码。
下面,我们就使用 ASM 来解决这个问题。
Java class 文件格式以及类加载器介绍
在后面的内容中,需要对 Java class 文件格式以及类加载器的知识有一定的了解,所以这里先对这些内容做一个简单介绍:
class 文件格式
Java class 文件的结构如图 2 所示(图中“*”表示出现 0 次或任意多次):
图 2.Java class 文件结构
详细说明如下:
- Magic Number: 每个 class 文件的前 4 个字节被称为“魔数”,它的内容为:0xCAFEBABE。魔数的作用在于可以轻松地分辨出一个文件是不是 class 文件。
- Version: 该项指明该 class 文件的版本号。
- Constant Pool: 常量池是 class 文件中结构最为复杂,也最为重要的部分。常量池包含了与文件中类和接口相关的常量。常量池中存储了诸如文字字符串,final 变量值。Java 虚拟机把常量池组织为入口列表的形式。常量池中许多入口都指向其他的常量入口,而且 class文件中紧随着常量池的许多条目也都会指向常量池的入口。除了字面常量之外,常量池还可以容纳以下几种符号引用:类和接口的全限定名,字段的名称和描述符和方法的名称和描述符等。
- Modifiers: 该项指明该文件中定义的是类还是接口,以及声明中用了哪种修饰符,类或接口是私有的,还是公共的,类的类型是否是 final 的,等等。
- This class: 该项是对常量池的索引。在这个位置,Java 虚拟机能够找到一个容纳了类或接口全限定名的入口。这里需要注意的是:在 class 文件中,所有类的全限定名都是以内部名称形式表示的。内部名称是将原先类全限定名中的“.”替换为“/”。例如:java.lang.String 的内部名称为 java/lang/String。
- Super Class: 该项也是对常量池的索引,指明了该类超类的内部名称。
- Interfaces: 该项指明了由该类直接实现或由接口扩展的父接口的信息。
注:Modifiers,This Class,Super Class 和 Interfaces 这四项的和就是一个类的声明部分。
- Annotation: 该项存储的是注解相关的内容,注解可能是关于类的,方法的以及字段的。
- Attribute: 该项用来存储关于类,字段以及方法的附加信息。在 Java 5 引入了注解之后,该部分内容几乎已经没有用处。
- Field: 该项用来存储类的字段信息。
- Method: 该项用来存储类的方法信息。
类装载器介绍
类装载器负责查找并装载类。每个类在被使用之前,都必须先通过类装载器装载到 Java 虚拟机当中。Java 虚拟机有两种类装载器 :
- 启动类装载器
启动类装载器是 Java 虚拟机实现的一部分,每个 Java 虚拟机都必须有一个启动类装载器,它知道怎么装载受信任的类,比如 Java API 的 class 文件。
- 用户自定义装载器
用户自定义装载器是普通的 Java 对象,它的类必须派生自 java.lang.ClassLoader类。ClassLoader 类中定义的方法为程序提供了访问类装载机制的接口。
ASM 简介以及编程模型
ASM 简介
ASM 是一个可以用来生成\转换和分析 Java 字节码的代码库。其他类似的工具还有 cglib、serp和 BCEL等。相较于这些工具,ASM 有以下的优点 :
- ASM 具有简单、设计良好的 API,这些 API 易于使用。
- ASM 有非常良好的开发文档,以及可以帮助简化开发的 Eclipse 插件
- ASM 支持 Java 6
- ASM 很小、很快、很健壮
- ASM 有很大的用户群,可以帮助新手解决开发过程中遇到的问题
- ASM 的开源许可可以让你几乎以任何方式使用它
编程模型
ASM 提供了两种编程模型:
- Core API,提供了基于事件形式的编程模型。该模型不需要一次性将整个类的结构读取到内存中,因此这种方式更快,需要更少的内存。但这种编程方式难度较大。
- Tree API,提供了基于树形的编程模型。该模型需要一次性将一个类的完整结构全部读取到内存当中,所以这种方法需要更多的内存。这种编程方式较简单。
下文中,我们将只使用 Core API,因此我们只介绍与其相关的类。
Core API 中操纵字节码的功能基于 ClassVisitor 接口。这个接口中的每个方法对应了 class 文件中的每一项。Class 文件中的简单项的访问使用一个单独的方法,方法参数描述了这个项的内容。而那些具有任意长度和复杂度的项,使用另外一类方法,这类方法会返回一个辅助的 Visitor接口,通过这些辅助接口的对象来完成具体内容的访问。例如 visitField 方法和 visitMethod方法,分别返回 FieldVisitor 和 MethodVisitor 接口的对象。
清单 1 为 ClassVisitor 中的方法列表:
清单 1.ClassVisitor 接口中的方法
public interface ClassVisitor { // 访问类的声明部分 void visit(int version, int access, String name, String signature,String superName, String[] interfaces); // 访问类的代码 void visitSource(String source, String debug); // 访问类的外部类 void visitOuterClass(String owner, String name, String desc); // 访问类的注解 AnnotationVisitor visitAnnotation(String desc, boolean visible); // 访问类的属性 void visitAttribute(Attribute attr); // 访问类的内部类 void visitInnerClass(String name, String outerName, String innerName,int access); // 访问类的字段 FieldVisitor visitField(int access, String name, String desc, String signature, Object value); // 访问类的方法 MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions); // 访问结束 void visitEnd(); } |
ClassVisitor 接口中的方法在被调用的时候是有严格顺序的,其顺序如清单 2 所示(其中“?”表示被调用 0 次或 1 次。“*”表示被调用 0 次或任意多次):
清单 2.ClassVisitor 中方法调用的顺序
visit visitSource? visitOuterClass? ( visitAnnotation| visitAttribute)* ( visitInnerClass| visitField| visitMethod)* visitEnd
|
这就是说,visit 方法必须最先被调用,然后是最多调用一次 visitSource 方法,然后是最多调用一次 visitOuterClass 方法。然后是 visitAnnotation 和 visitAttribute 方法以任意顺序被调用任意多次。再然后是以任任意顺序调用 visitInnerClass ,visitField 或 visitMethod 方法任意多次。最终,调用一次 visitEnd 方法。
ASM 提供了三个基于 ClassVisitor 接口的类来实现 class 文件的生成和转换:
- ClassReader:ClassReader 解析一个类的 class 字节码,该类的 accept 方法接受一个ClassVisitor 的对象,在 accept 方法中,会按上文描述的顺序逐个调用 ClassVisitor 对象的方法。它可以被看做事件的生产者。
- ClassAdapter:ClassAdapter 是 ClassVisitor 的实现类。它的构造方法中需要一个ClassVisitor 对象,并保存为字段 protected ClassVisitor cv。在它的实现中,每个方法都是原封不动的直接调用 cv 的对应方法,并传递同样的参数。可以通过继承 ClassAdapter并修改其中的部分方法达到过滤的作用。它可以看做是事件的过滤器。
- ClassWriter:ClassWriter 也是 ClassVisitor 的实现类。ClassWriter 可以用来以二进制的方式创建一个类的字节码。对于 ClassWriter 的每个方法的调用会创建类的相应部分。例如:调用 visit 方法就是创建一个类的声明部分,每调用一次 visitMethod 方法就会在这个类中创建一个新的方法。在调用 visitEnd 方法后即表明该类的创建已经完成。它最终生成一个字节数组,这个字节数组中包含了一个类的 class 文件的完整字节码内容 。可以通过 toByteArray 方法获取生成的字节数组。ClassWriter 可以看做事件的消费者。
通常情况下,它们是组合起来使用的。
下面举一个简单的例子:假设现在需要对 class 文件的版本号进行修改,将其改为 Java 1.5。操作方法如下:
- 首先通过 ClassReader 读取这个类;
- 然后创建一个 ClassAdapter 的子类 ChangeVersionAdapter。在 ChangeVersionAdapter中,覆盖 visit 方法,在调用 ClassVisitor#visit 方法时修改其中关于版本号的参数。该方法的签名如下:visit(int version, int access, String name, String signature, String superName, String[] interfaces),其中每个参数的含义如下:
- version:class 文件的版本号,这就是我们需要修改的内容;
- access:该类的访问级别;
- name:该类的内部名称;
- signature:该类的签名,如果该类与泛型无关,这个参数就是 null;
- superName:父类的内部名称;
- interfaces:该类实现的接口的内部名称;
明白这些参数的含义之后,修改就很容易,只需要在调用 cv.visit 的时候,将 version 参数指定为 Opcodes.V1_5 即可(Opcodes 是 ASM 中的一个类),其他参数不加修改原样传递。这样,经过该 ClassAdapter 过滤后的类的版本号就都是 Java 1.5 了。
- 在创建 ChangeVersionAdapter 对象时,构造方法传递一个 ClassWriter 的对象。该ClassWriter 会随着 ChangeVersionAdapter 方法的调用按顺序创建出类的每一个部分。由于在 visit 方法中,version 参数已经被过滤为 Opcodes.V1_5,所以该 ClassWriter 最终产生的 class 字节码的版本号就是 V1.5 的;
- 然后通过 ClassWriter#toByteArray 方法获取修改后的类的完整的字节码内容;
- 当然,想要使用这个类,还需要通过一个自定义类加载器,将获得到的字节码加载到虚拟机当中,才可以创建这个类的实例;
代码片段如清单 3 所示:
清单 3. 使用 ASM 的代码示例
… // 使用 ChangeVersionAdapter 修改 class 文件的版本 ClassReader cr = new ClassReader(className); ClasssWriter cw = new ClassWriter(0); // ChangeVersionAdapter 类是我们自定义用来修改 class 文件版本号的类 ClassAdapter ca = new ChangeVersionAdapter (cw); cr.accept(ca, 0); byte[] b2 = cw.toByteArray(); … |
图 3 是相应的 Sequence 图:
图 3. 修改版本号的 Sequence 图
代码示例
在了解了以上的知识之后再回到我们刚开始提出的问题中,我们希望 SubClass1 和 SubClass2 在继承自 SuperClass 的同时还要实现 IFibonacciComputer 以及 ITimeRetriever 两个接口。
为了后文描述方便,这里先确定三个名词:
- 实现类即完成了接口实现的类,这里为 FibonacciComputer 以及 TimeRetriever。
- 待增强类即需要实现功能增强,加入实现逻辑的类,这里为 SubClass1 和 SubClass2。
- 增强类即在待增强类的基础上,加入了接口实现的类。这个类目前没有实际的类与之对应。
如果只能在源代码级别进行修改,我们能做的仅仅是将实现类的代码拷贝进待增强类。(当然,有稍微好一点的做法在每一个待增强类中包含一个实现类,以组合的方式实现接口。但这仍然不能避免多个待增强类中的代码重复。)
在学习了 ASM 之后,我们可以直接从字节码的层次来进行修改。回忆一下上文中的内容:使用ClassWrite 可以直接创建类的字节码,不同的方法创建了 class 文件的不同部分,尤其重要的是以下几个方法:
- visit 方法创建一个类的声明部分
- visitField 方法创建一个类的字段
- visitMethod 方法创建一个类的方法
- visitEnd 方法,表明该类已经创建完成
所以,现在我们可以直接从字节码的层次完成这一需求:动态的创建一个新的类(即增强类)继承自待增强类,同时在该类中,将实现类的实现方法添加进来。
完整的实现逻辑如下:
- 创建一个 ClassAdapter 的子类 AddImplementClassAdapter,这个类将被用来访问待增强类。AddImplementClassAdapter 期待一个 ClassWriter 作为 ClassVisitor。该ClassWriter 在访问待增强类的同时逐步完成增强类的创建。
- 在 AddImplementClassAdapter 中覆盖 visitEnd 方法,在调用 ClassVisitor#visitEnd 方法之前,使用该 ClassVisitor 逐个访问每一个实现类。由于实现类中的内容也需要进行一定的过滤,所以这里的 ClassVisitor 在访问实现类的时候也需要经过一个 ClassAdapter进行过滤。创建另一个 ClassAdapter 的子类 ImplementClassAdapter 来完成这个过滤。由于这个 ClassVisitor 是一个 ClassWriter。这样做的效果就是将实现类的内容添加到增强类中。
- 在完成了所有实现类的访问之后,调用 ClassVisitor#visitEnd 方法表明增强类已经创建完成。
- 使用一个 ClassReader 的对象读取待增强类。
- 创建一个 AddImplementClassAdapter 的实例,同时提供一个 ClassWriter 作为ClassVisitor。
- 通过 accept 方法将前面创建的 AddImplementClassAdapter 对象传递给这个 ClassReader对象。
- 在访问完待增强类之后,ClassWriter 即完成了增强类的创建,所以最后通过 toByteArray方法获取这个增强类的字节码。
- 最后通过一个自定义类加载器将其加载到虚拟机当中。
下面是代码示例与讲解:
首先需要修改 SubClass1 以及 SubClass2 两个类,使其声明实现 IFibonacciComputer 和ITimeRetriever 这两个接口。由于这两个类并没有真正的包含实现接口的代码,所以它们现在必须标记为抽象类。修改后的类结构如图 4 所示:
图 4. 修改后的类图
然后创建以下几个类:
- AddImplementClassAdapter: 过滤待增强类,并引导 ClassWriter 创建增强类的适配器。
- ImplementClassAdapter: 实现类的适配器,过滤多余内容,将实现类中的内容通过ClassWriter 添加到增强类中。
- ModifyInitMethodAdapter: 修改待增强类构造方法的适配器。
- SimpleClassLoader: 自定义类加载器,用来加载动态生成的增强类。
- EnhanceFactory: 提供对外接口,方便使用。
- EnhanceException: 对异常的统一包装,方便异常处理。
下面,我们来逐一实现这些类:
AddImplementClassAdapter: 首先在构造方法中,我们需要记录下待增强类的 Class 对象,增强类的类名,实现类的 Class 对象,以及一个 ClassWriter 对象,该构造方法代码如清单 4所示:
清单 4.AddImpelementClassAdapter 构造方法代码
public AddImplementClassAdapter( ClassWriter writer, String enhancedClassName,Class targetClass, Class... implementClasses) { super(writer); this.classWriter = writer; this.implementClasses = implementClasses; this.originalClassName = targetClass.getName(); this.enhancedClassName = enhancedClassName; } |
在 visit 方法中需要完成增强类声明部分的创建,增强类继承自待增强类。该方法代码如清单 6所示:
清单 5.visit 方法代码
// 通过 visit 方法完成增强类的声明部分 public void visit(int version, int access, String name, String signature,String superName, String[] interfaces) { cv.visit(version, Opcodes.ACC_PUBLIC, // 将 Java 代码中类的名称替换为虚拟机中使用的形式 enhancedClassName.replace('.', '/'), signature, name,interfaces); } |
visitMethod 方法中需要对构造方法做单独处理,因为 class 文件中的构造方法与源代码中的构造方法有三点不一样的地方:
- 每个 class 文件中至少有一个构造方法。即便你在类的源代码中没有编写构造方法,编译器也会为你生成一个默认构造方法 ;
- 在 class 文件中与源代码中的构造方法名称不一样,class 文件的构造方法名称都为“”;
- class 文件中每个构造方法都会最先调用一次父类的构造方法。
鉴于这些原因,增强类的构造方法需要在待增强类构造方法的基础上进行修改。修改的内容就是对于父构造方法的调用,因为增强类和待增强类的父类是不一样的。
visitMethod 方法中需要判断如果是构造方法就通过 ModifyInitMethodAdapter 修改构造方法。其他方法直接返回 null 丢弃(因为增强类已经从待增强类中继承了这些方法,所以这些方法不需要在增强类中再出现一遍),该方法代码如清单 7 所示:
清单 6.visitMethod 方法代码
public MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions) { if (INTERNAL_INIT_METHOD_NAME.equals(name)) { // 通过 ModifyInitMethodAdapter 修改构造方法 MethodVisitor mv = classWriter.visitMethod(access, INTERNAL_INIT_METHOD_NAME, desc, signature, exceptions); return new ModifyInitMethodAdapter(mv, originalClassName); } return null; } |
最后,在 visitEnd 方法,使用 ImplementClassAdapter 与 ClassWriter 将实现类的内容添加到增强类中,最后再调用 visitEnd 方法表明增强类已经创建完成:
清单 7.visitEnd 方法代码
public void visitEnd() { for (Class clazz : implementClasses) { try { // 逐个将实现类的内容添加到增强类中。 ClassReader reader = new ClassReader(clazz.getName()); ClassAdapter adapter = new ImplementClassAdapter(classWriter); reader.accept(adapter, 0); } catch (IOException e) { e.printStackTrace(); } } cv.visitEnd(); } |
ImplementClassAdapter:该类对实现类进行过滤。
首先在 visit 方法中给于空实现将类的声明部分过滤掉,代码如清单 8 所示:
清单 8.visit 方法代码
public void visit(int version, int access, String name, String signature,String superName, String[] interfaces) { // 空实现,将该部分内容过滤掉 } |
然后在 visitMethod 中,将构造方法过滤掉,对于其他方法,调用 ClassVisitor#visitMethod进行访问。由于这里的 ClassVisitor 是一个 ClassWriter,这就相当于在增强类中创建了该方法,代码如清单 9 所示:
清单 9.visitMethod 方法代码
public MethodVisitor visitMethod(int access, String name, String desc,String signature, String[] exceptions) { // 过滤掉实现类中的构造方法 if (AddImplementClassAdapter.INTERNAL_INIT_METHOD_NAME.equals(name)){ return null; } // 其他方法原样保留 return cv.visitMethod(access, name, desc, signature, exceptions); } |
ModifyInitMethodAdapter:上文中已经提到,ModifyInitMethodAdapter 是用来对增强类的构造方法进行修改的。MethodAdapter 中的 visitMethodInsn 是对方法调用指令的访问。该方法的参数含义如下:
- opcode 为调用方法的 JVM 指令,
- owner 为被调用方法的类名,
- name 为方法名,
- desc 为方法描述。
所以,我们需要将对于待增强类父类构造方法的调用改为对于待增强类构造方法的调用(因为增强类的父类就是待增强类),其代码如清单 10 所示:
清单 10. ModifyInitMethodAdapter 类代码
/** 专门用来修改构造方法的方法适配器 */ public class ModifyInitMethodAdapter extends MethodAdapter { private String className; public ModifyInitMethodAdapter(MethodVisitor mv, String name) { super(mv); this.className = name; } public void visitMethodInsn(int opcode, String owner, String name,String desc) { // 将 Java 代码中的类全限定名替换为虚拟机中使用的形式 if (name.equals(AddImplementClassAdapter.INTERNAL_INIT_METHOD_NAME)) { mv.visitMethodInsn(opcode, className.replace(".", "/"), name, desc); } } } |
SimpleClassLoader:该自定义类装载器通过提供一个 defineClass 方法来装载动态生成的增强类。方法的实现是直接调用父类的 defineClass 方法,其代码如清单 11 所示:
清单 11. SimpleClassLoader 类代码
public class SimpleClassLoader extends ClassLoader { public Class defineClass(String className, byte[] byteCodes) { // 直接通过父类的 defineClass 方法加载类的结构 return super.defineClass(className, byteCodes, 0, byteCodes.length); } } |
EnhanceException:这是一个异常包装类,其中包含了待增强类和实现类的信息,其逻辑很简单,代码如清单 12 所示:
清单 12. EnhanceException 类代码
/** 异常类 */ public class EnhanceException extends Exception { private Class enhanceClass; private Class [] implementClasses; // 异常类构造方法 public EnhanceException(Exception ex,Class ec,Class... imClazz){ super(ex); this.enhanceClass = ec; this.implementClasses = imClazz; } public Class getEnhanceClass() { return enhanceClass; } public Class[] getImplementClasses() { return implementClasses; } } |
EnhanceFactory:最后,通过 EnhanceFactory 提供对外调用接口,调用接口有两个:
- public static Class addImplementation(
Class clazz,Class... implementClasses) - public static T newInstance(Class clazz,
Class... impls)
为了方便使用,这两个方法都使用了泛型。它们的参数是一样的:第一个参数都是待增强类的Class 对象,后面是任意多个实现类的 Class 对象,返回的类型和待增强类一致,用户在获取返回值之后不需要进行任何类型转换即可使用。
第一个方法创建出增强类的 Class 对象,并通过自定义类加载器加载,其代码如清单 13 所示:
清单 13. addImplementation 方法代码
/** 静态工具方法,在待增强类中加入实现类的内容,返回增强类。 */ public static Class addImplementation(Class clazz, Class... implementClasses) throws EnhanceException { String enhancedClassName = clazz.getName() + ENHANCED; try { // 尝试加载增强类 return (Class) classLoader.loadClass(enhancedClassName); } // 如果没有找到增强类,则尝试直接在内存中构建出增强类的结构 catch (ClassNotFoundException classNotFoundException) { ClassReader reader = null; try { reader = new ClassReader(clazz.getName()); } catch (IOException ioexception) { throw new EnhanceException(ioexception, clazz, implementClasses); } ClassWriter writer = new ClassWriter(0); // 通过 AddImplementClassAdapter 完成实现类内容的织入 ClassVisitor visitor = new AddImplementClassAdapter( enhancedClassName, clazz, writer, implementClasses); reader.accept(visitor, 0); byte[] byteCodes = writer.toByteArray(); Class result = (Class) classLoader.defineClass( enhancedClassName, byteCodes); return result; } } |
第二个方法先调用前一个方法,获取 增强类的 Class对象,然后使用反射创建实例,其代码如清单 14 所示:
清单 14. newInstance 方法代码
/** 通过待增强类和实现类,得到增强类的实例对象 */ public static T newInstance(Class clazz, Class... impls) throws EnhanceException { Class c = addImplementation(clazz, impls); if (c == null) { return null; } try { // 通过反射创建实例 return c.newInstance(); } catch (InstantiationException e) { throw new EnhanceException(e, clazz, impls); } catch (IllegalAccessException e) { throw new EnhanceException(e, clazz, impls); } } |
下面是测试代码,先通过 EnhanceFactory 创建增强类的实例,然后就可以像普通对象一样的使用,代码如清单 15 所示:
清单 15. 使用演示代码
// 不用 new 关键字,而使用 EnhanceFactory.newInstance 创建增强类的实例 SubClass1 obj1 = EnhanceFactory.newInstance(SubClass1.class, TimeRetriever.class,FibonacciComputer.class); // 调用待增强类中的方法 obj1.methodInSuperClass(); obj1.methodDefinedInSubClass1(); // 调用实现类中的方法 System.out.println("The Fibonacci number of 10 is "+obj1.compute(10)); System.out.println("Now is :"+obj1.tellMeTheTime()); System.out.println("--------------------------------------"); // 对于 SubClass2 的增强类实例的创建也是一样的 SubClass2 obj2 = EnhanceFactory.newInstance(SubClass2.class, TimeRetriever.class,FibonacciComputer.class); // 调用待增强类中的方法 obj2.methodInSuperClass(); obj2.methodDefinedInSubClass2(); // 调用实现类中的方法 System.out.println("The Fibonacci number of 10 is "+obj1.compute(10)); System.out.println("Now is :"+obj1.tellMeTheTime()); |
这里,我们演示了使用 ASM 创建一个新的类,并且修改该类中的内容的方法。而这一切都是在运行的环境中动态生成的,这一点相较于源代码级别的实现有以下的好处:
- 没有重复代码 这是我们的主要目的,由于增强类是在运行的环境中生成的,并且动态的包含了实现类中的内容,所以,不会产生任何重复代码。
- 灵活性 使用 EnhanceFactory# addImplementation 方法,对于接口的实现完全是在运行时确定的,因此可以灵活的选择。
- 可复用性 EnhanceFactory# addImplementation 是一个可以完全复用的方法,我们可以在任何需要的地方使用它。
需要注意的是,这里我们并没有真正的实现“多重继承”,由于 class 文件格式的限制,我们也不可能真正实现“多重继承”,我们只是在一个类中包含了多个实现类的内容而已。但是,如果你使用增强类的实例通过 instanceof 之类的方法来判断它是否是实现类的实例的时候,你会得到false,因为增强类并没有真正的继承自实现类。
另外,为了让演示代码足够的简单,对于这个功能的实现还存在一些问题,例如:
- FibonacciComputer 和 TimeRetriever 这两个类中,可能会包含一些其他方法,这些方法并非是为了实现接口的方法,而这些方法也会被增强类所包含。
- 如果多个实现类与待增强类中包含了同样签名的方法时,在创建增强类的过程中会产生异常,因为一个类中不能包含同样方法签名的两个方法。
- 如果实现类中包含了一些字段,并且实现类的构造方法中初始化了这些字段。但增强类中的这些字段并没有被初始化。因为实现类的构造方法被忽略了。
- 无法对同一个类做多次不同类型的增强。
不过,在理解了上文中的知识之后,这些问题也都是可以解决的。
作为一个可以操作字节码的工具而言,ASM 的功能远不止于此。它还可以用来实现 AOP,实现性能监测,方法调用统计等功能。通过 Google,可以很容易的找到这类文章。
示例代码包含在 ASM_Demo.zip 中,该文件中包含了上文中提到的所有代码。
该 zip 文件为 eclipse 项目的归档文件。可以通过 Eclipse 菜单导入至 Eclipse 中,导入方法:File -> Import … -> Existing Projects into Workspace, 然后选择该 zip 文件即可。
想要编译该项目,还需要 ASM 框架的 jar 包。请在以下地址下载 ASM 框架:http://forge.ow2.org/projects/asm/
目前该框架正式版的版本号为:3.3.1。
下载该框架归档文件后解压缩, 并通过 eclipse 将 asm-all-3.3.1.jar(可能是其他版本号)添加到项目编译的类路径中即可。
代码中包含的 Main 类,是一个包含了 main 方法的可执行类,在 eclipse 中运行该类即可看到运行结果。
目的:
程序分析:用于分析程序,动态生成proxy等。
程序生成:可在内存中生成java类并编译,所谓的just in time complie
程序转换:优化程序插入debugging或平台观察代码(AOP)
ASM库提供生成、转换类的2种API,一直API是基于事件一种是基于数的。基于事件的就类似于xml的SAX,而基于树的就像DOM.这2种 API都有各自的有点与缺点。基于事件的API快速并且需要的内存比基于树的少。
组织结构:
org.objectweb.asm与org.objectweb.asm.signature是定义基于事件的API并提供了class的读写 组建。
org.objectweb.asm.util是工具包。
org.objectweb.asm.commons定义了预定义类的转行器
org.objectweb.asm.tree定义了基于树的API
org.objectweb.asm.tree.analysis定义了基于树API的分析框架和预定义类的分析器
java字节码的结构
+----------------------------------------------------+
|Modifiers, name, super class, interfaces |
+----------------------------------------------------+
|Constant pool: numeric, string and type constants |
+----------------------------------------------------+
|Source file name (optional) |
+----------------------------------------------------+
|Enclosing class reference |
+----------------------------------------------------+
|Annotation* |
+----------------------------------------------------+
|Attribute* |
+----------------------------------------------------+
|Inner class* Name |
+----------------------------------------------------+
|Field* Modifiers, name, type |
| Annotation* |
| Attribute* |
+----------------------------------------------------+
|Method* Modifiers, name, return and parameter types |
| Annotation* |
| Attribute* |
| Compiled code |
+----------------------------------------------------+
内部名
在class文件里面使用的是内部名,如String的内部名是 java/lang/String
类型描述:
java类型->类型描述
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;
方法描述:
方法描述是类型描述列表,用于描述方法的参数类型以及返回值类型
方法声明->方法描述
void m(int i, float f)->(IF)V
int m(Object o)->(Ljava/lang/Object;)I
int[] m(int i, String s)->(ILjava/lang/String;)[I
Object m(int[] i)->([I)Ljava/lang/Object;
生成和解析编译好的类文件是基于ClassVisitor接口。这个接口的每个方法对应类文件相同的段名如下:
public interface ClassVisitor {
void visit(int version, int access, String name, String signature,
String superName, String[] interfaces);
void visitSource(String source, String debug);
void visitOuterClass(String owner, String name, String desc);
AnnotationVisitor visitAnnotation(String desc, boolean visible);
void visitAttribute(Attribute attr);
void visitInnerClass(String name, String outerName, String innerName,
int access);
FieldVisitor visitField(int access, String name, String desc,
String signature, Object value);
MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions);
void visitEnd();
}
使用方法必须符合下面规则:
visit visitSource? visitOuterClass? ( visitAnnotation | visitAttribute )*
( visitInnerClass | visitField | visitMethod )*
visitEnd
visit必须是第一个被调用的方法,然后调用visitOuterClass,接着调用visitAnnotation和 visitAttribute依次类推,最后调用visitEnd
ASM提供3个核心基于ClassVisitor的3个核心组件用于产生解析类:
1、ClassReader解析已经编译好的类文件,调用对应的VisitXxx方法
2、ClassWriter实现了ClassVisitor接口,生成类
3、ClassAdapter实现了ClassVisitor接口,代理调用接口的所有方法
以下是类解析的例子相关信息,通过Visitor打印类
- package com.appspot.coder9527;
- import java.io.IOException;
- import org.objectweb.asm.AnnotationVisitor;
- import org.objectweb.asm.Attribute;
- import org.objectweb.asm.ClassReader;
- import org.objectweb.asm.ClassVisitor;
- import org.objectweb.asm.FieldVisitor;
- import org.objectweb.asm.MethodVisitor;
- public class ClassInfoVisitor implements ClassVisitor {
- @Override
- public void visit( int version, int access, String name, String signature,
- String superName, String[] interfaces) {
- System.out.println(name + " extends " + superName + " {" );
- }
- @Override
- public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
- return null ;
- }
- @Override
- public void visitAttribute(Attribute attr) {
- }
- @Override
- public void visitEnd() {
- System.out.println("}" );
- }
- @Override
- public FieldVisitor visitField( int access, String name, String desc,
- String signature, Object value) {
- System.out.println(" " + desc + " " + name);
- return null ;
- }
- @Override
- public void visitInnerClass(String name, String outerName,
- String innerName, int access) {
- }
- @Override
- public MethodVisitor visitMethod( int access, String name, String desc,
- String signature, String[] exceptions) {
- System.out.println(" " + name + desc);
- return null ;
- }
- @Override
- public void visitOuterClass(String owner, String name, String desc) {
- }
- @Override
- public void visitSource(String source, String debug) {
- }
- public static void main(String argv[]) throws IOException {
- ClassReader reader = new ClassReader( "com.appspot.coder9527.ClassInfoVisitor" );
- ClassInfoVisitor visitor = new ClassInfoVisitor();
- reader.accept(visitor, 0 );
- }
- }
- package com.appspot.coder9527;
- import java.io.IOException;
- import org.objectweb.asm.AnnotationVisitor;
- import org.objectweb.asm.Attribute;
- import org.objectweb.asm.ClassReader;
- import org.objectweb.asm.ClassVisitor;
- import org.objectweb.asm.FieldVisitor;
- import org.objectweb.asm.MethodVisitor;
- public class ClassInfoVisitor implements ClassVisitor {
- @Override
- public void visit(int version, int access, String name, String signature,
- String superName, String[] interfaces) {
- System.out.println(name + " extends " + superName + " {");
- }
- @Override
- public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
- return null;
- }
- @Override
- public void visitAttribute(Attribute attr) {
- }
- @Override
- public void visitEnd() {
- System.out.println("}");
- }
- @Override
- public FieldVisitor visitField(int access, String name, String desc,
- String signature, Object value) {
- System.out.println(" " + desc + " " + name);
- return null;
- }
- @Override
- public void visitInnerClass(String name, String outerName,
- String innerName, int access) {
- }
- @Override
- public MethodVisitor visitMethod(int access, String name, String desc,
- String signature, String[] exceptions) {
- System.out.println(" " + name + desc);
- return null;
- }
- @Override
- public void visitOuterClass(String owner, String name, String desc) {
- }
- @Override
- public void visitSource(String source, String debug) {
- }
- public static void main(String argv[]) throws IOException {
- ClassReader reader = new ClassReader("com.appspot.coder9527.ClassInfoVisitor");
- ClassInfoVisitor visitor = new ClassInfoVisitor();
- reader.accept(visitor, 0);
- }
- }
来看看ClassReader,要了解ClassReader.
首先要理解要理解Class文件定义的格式
struct Class_File_Format {
u4 magic_number; //4个字节的魔幻数 是16进制的0xcafe 0xbabe cafe呵呵有点好耍
u2 minor_version; //2个字节的主版本号
u2 major_version; //2个字节次版本号
u2 constant_pool_count; //常量池大小
cp_info constant_pool[constant_pool_count - 1]; //常量池信息
u2 access_flags; //类或接口访问表示
u2 this_class; //本类在常量池的索引
u2 super_class; //父类在常量池的索引
u2 interfaces_count; //接口计数
u2 interfaces[interfaces_count]; //接口对应常量池的索引
u2 fields_count; //类的域个数
field_info fields[fields_count]; //域数据,包括属性名称索引,域修饰符掩码等
u2 methods_count; //方法计数
method_info methods[methods_count]; //方法信息
u2 attributes_count; //类附加属性个数
attribute_info attributes[attributes_count];//类附加属性数据,包括源文件名等。
};
看看常量表cp_info的定义。
cp_info {
u1 tag;
u1 info[];
}
cp_info的tag保存着类型信息,如下:
类型 值
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
指定的类型又显示了info指向的每种结构,比如CONSTANT_Class这种类型的指向的结构是
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
以下是Class_File_Format的结构图
这里要说的ASM,并不是指汇编语言,而是一个操作Java bytecode的框架。对于Java平台而言,bytecode便是它的“汇编语言”,所以,ASM这个名字倒也算是实至名归。ASM本身很强大,有不少软件和框架选择它作为底层的实现,比如cglib。在这篇blog中,主要来关注一下它在代码生成方面的威力。
在起步阶段,Hello World总是一个很好的选择,也就是说,我们生成的目标代码是这样的:
public class AsmExample {
public static void main(String[] args) {
System.out.println("Hello, World");
}
}
在Java中,代码是以类的形式进行组织的,.class文件便是bytecode的载体。对照上面这段代码,首先,我们需要一个类。
public class AsmMain {
public static void main(String[] args) {
ClassWriter cw = new ClassWriter(true);
cw.visit(Opcodes.V1_1, Opcodes.ACC_PUBLIC, "AsmExample", null, "java/lang/Object", null);
...
cw.visitEnd();
}
}
在上面这段代码中,ClassWriter就是ASM中用来生成bytecode形式的类。在这里,我们要为我们生成的类设置一些属性,比如类名、访问级别和超类,以及在bytecode层次上需要的版本号等等。至此,对应的Java代码如下:
public class AsmExample {
}
有了类,接下来就是对应的方法了,先来看看基本的结构:
Method m = Method.getMethod("void main (String[])");
GeneratorAdapter mg = new GeneratorAdapter(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, m, null, null, cw);
...
mg.returnValue();
mg.endMethod();
首先我们设置了一个方法的签名,包括方法名,参数和返回类型。我们要生成这个方法,还需要设置一些方法的属性,比如访问级别等等,通过cw这个参数,方法同类关联在了一起。到这里,对应的Java代码是这样的:
public class AsmExample {
public static void main(String args[]) {
}
}
前面所做的都是搭建静态结构的工作,接下来,我们要进入的才是让程序动起来的部分。
mg.getStatic(Type.getType(System.class), "out", Type.getType(PrintStream.class));
mg.push("Hello world!");
mg.invokeVirtual(Type.getType(PrintStream.class), Method.getMethod("void println (String)"));
在这里,我们面对的实际上是JVM的指令,所以,如果面对汇编一样,所有的一切都一步一步说清楚。
首先是获得System.out。我们通过getStatic这个方法实现,它表示从哪个类中取出哪个static字段,其类型是什么。而且实际上,这条指令执行的结果是将这个取出的字段推到了堆栈上。随后,我们在把“Hello world!”也推入堆栈之中,很显然,这一切都是在为调用方法做准备。
对于参数(这里的“Hello world!”)入栈,我们很容易接受,但为什么要把System.out也送入堆栈呢?再次提醒一下,这里我们是在JVM一级进行思考,在这里,方法调用被打回了最原始的形态,在Java程序中被隐藏的this这时也要作为参数显式传递,也就是说,方法调用变成了这样:
println(System.out, "Hello world!");
万事俱备,调用方法。在Java中,方法调用需要区分类方法和实例方法,它们在虚拟机中有着不同的指令,这里我们要调用的是实例的方法,所以,这里用的是invokeVirtual,指定了类型,指定了方法,方法就可以调用了。如果要调用类的方法,也就是static方法,那就需要让invokeStatic上阵了。
对比一下invokeVirtual和invokeStatic的API定义,我们不难发现,它们之间实际上没有什么区别,之所以要弄出两个来,与Java的设计不无关系,它把属于类的东西看作了一种特殊的东西,没有统一到对象体系之中。如果为Ruby设计虚拟机,可以消除这样的问题,因为在Ruby中,类的方法就是类对象的实例方法,这样将类的东西统一到对象体系之中,不必额外区分。
到这里,我们的目标便已完全实现:
public class AsmExample {
public static void main(String args[]) {
System.out.println("Hello world!");
}
}
之后,我们可以把定义的类转为字节,至于是加载到虚拟机中运行,还是保存到文件中,那就由自己的喜好了。
byte[] code = cw.toByteArray();
和ASM打交道,需要我们放低自己姿态,站在指令一级进行思考。比如,在这个层次上,实现判断语句,就需要设置label,然后进行相应的跳转;这里没有循环语句,需要自己用判断加跳转打造循环结构。不过,总的来说,很容易同Java程序对应上,就像我们上面所做的那样。《深入Java虚拟机》可以让我们更好的了解JVM,也可以让帮助我们更好的理解ASM的程序。
有几个帮手可以让我们更好进行bytecode生成这个游戏。javap,JDK带的一个工具,可以用来反汇编Java bytecode。在接触ASM的最初,我们对指令不是很熟悉的时候,可以考虑先把自己的目标写成Java程序,编译之后用“javap -c”来查看,所有的指令便一览无余,我们就可以照方抓药了。jad,它为我们提供了一个将Java class文件反编译为Java文件,通过它,我们就可以知道生成的bytecode究竟是不是自己想要的,我所展示与生成过程对应的Java代码便是借助于jad的力量完成的。
ASM很强大,这里只介绍了ASM中的代码生成,实际上,就连代码生成这一项工作介绍的都不那么完整,ASM还提供了另外一种生成方式,不过,用起来不如这里的GeneratorAdapter,需要更多的JVM指令的智慧,优势在于速度稍快一些。
读后感:这玩意没怎么把玩过,但我知道为了逃避开源代码使用问题,为了要改开源代码,使用ASM动态修改class文件。自己写的代码根本就根本使用ASM那么麻烦了,ASM毕竟牺牲效率为代价。