1、首先,来学习用ASM创建一个类,以及将类倾倒成class文件。用ASM在内存中创建一个类:
- ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
- // 类访问开始:必须
- cw.visit(Opcodes.V1_5, Opcodes.ACC_PUBLIC, "my/Foo", null,
- "java/lang/Object", null);
- // 构建构造函数
- MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "
" , "()V",- null, null);
- // 代码开始:必须
- mv.visitCode();
- mv.visitVarInsn(Opcodes.ALOAD, 0);
- mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "
" ,- "()V");
- mv.visitInsn(Opcodes.RETURN);
- // 计算栈和局部变量最大空间:必须,不计算会报java.lang.VerifyError异常,异常信息是:Stack size too large
- mv.visitMaxs(0, 0);
- // 代码结束:必须
- mv.visitEnd();
- // 构建main函数
- mv = cw.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "main",
- "([Ljava/lang/String;)V", null, null);
- mv.visitCode();
- mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out",
- "Ljava/io/PrintStream;");
- mv.visitLdcInsn("Hello World!");
- mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
- "println", "(Ljava/lang/String;)V");
- mv.visitInsn(Opcodes.RETURN);
- mv.visitMaxs(0, 0);
- mv.visitEnd();
- // 类结束:必须
- cw.visitEnd();
2、在内存中创建出该类之后,怎么将该类的字节码导出到数组?
继续上面的代码,go on…
- // 将在内存中创建的my.Foo类字节码导出到字节数组
- final byte[] bs = cw.toByteArray();
- // 加载my.Foo类的字节码
- Class clazz = new ClassLoader() {
- protected Class findClass(String name)
- throws ClassNotFoundException {
- return defineClass(name, bs, 0, bs.length);
- }
- }.loadClass("my.Foo");
如果能够成功载入,虚拟机自然不会报错,如若不然,虚拟机会报错,譬如:
如果插装插两次,JVM虚拟机在载入该类的时候,一定会报Exception in thread "main" java.lang.ClassFormatError: Duplicate field name&signature in class file my/Foo 这样的错误。
4、类载入后,可以结合Java反射的能力,创建对象,访问字段,访问方法:
- // 利用反射获取my.Foo类中的main方法
- Method method = clazz.getMethod("main", new Class[] { String[].class });
- // 用反射的方式调用my.Foo类中的main()
- method.invoke(null, (Object) new String[0]);
5、很多时候,需要将字节码从内存中倾倒出来,写成class文件:
- OutputStream out = new FileOutputStream("d:/my/Foo.class");
- out.write(bs);
- out.close();
6、接下来,继续深入下去,读取class文件,并且,修改它,然后再倾倒出一个新的class文件:
怎么读取类字节码?方法有很多:
a) 方法一:
Cla***eader cr = new Cla***eader("java.lang.Runnable");
b) 方法二:
InputStream is = cl.getResourceAsStream(classname.replace(’.’, ’/’) + ".class");
然后
Cla***eader cr = new Cla***eader(is);
c) 方法三:
URL url = HelloModifyASM.class.getResource("Foo.class");
Cla***eader cr = new Cla***eader(url.openStream());
适用场景各不同,有选择的用便是。
7、在类中添加一个静态字段,比如public static List _my_instances; 怎么做?
创建ClassAdapter的子类,然后覆写
- void visit(int version, int access, String name,
- String signature, String superName, String[] interfaces)
- 这个方法,并在方法里边调用 visitField语句:
- // 添加字段:public static List _my_instances;
- super.visitField(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC,
- "_my_instances", "Ljava/util/List;", null, null);
ASM,确实强!不得不服!呵呵
8、如何添加类的静态初始化块?
继续在ClassAdapter的visit方法中写visitMethod方法,为啥这么用?因为visit方法只执行一次,在这里写也是合适的位置。
- // 添加静态的初始化块
- MethodVisitor mv = super.visitMethod(Opcodes.ACC_STATIC, "
" ,- "()V", null, null);
- mv.visitCode();
- mv.visitTypeInsn(Opcodes.NEW, "java/util/ArrayList");
- mv.visitInsn(Opcodes.DUP);
- mv.visitMethodInsn(Opcodes.INVOKESPECIAL,
- "java/util/ArrayList", "
" , "()V");- mv.visitFieldInsn(Opcodes.PUTSTATIC, "my/Foo", "_my_instances",
- "Ljava/util/List;");
- mv.visitInsn(Opcodes.RETURN);
- mv.visitMaxs(0, 0);
- mv.visitEnd();
9、我想要修改无参构造函数,想在其中添加几条语句,怎么做?
和上面一样,同样是在要ClassAdapter的子类中动手脚,不过,此时要覆写的方法是:
- MethodVisitor visitMethod(int access, String name,
- String desc, String signature, String[] exceptions)
我们当然是要通过name和desc找到我们要处理的函数,然后再修改它。name就是函数名,当然,有些名字是编译器创建的(不是你在编码的时候写的,比如
- public MethodVisitor visitMethod(int access, String name,
- String desc, String signature, String[] exceptions) {
- MethodVisitor mv = super.visitMethod(access, name, desc,
- signature, exceptions);
- // 修改无参的构造函数:
- if (!"
" .equals(name) || !"()V".equals(desc))- return mv;
- return new MethodAdapter(mv) {
- public void visitInsn(int opcode) {
- if (opcode == Opcodes.RETURN) {
- visitFieldInsn(Opcodes.GETSTATIC, "my/Foo",
- "_my_instances", "Ljava/util/List;");
- visitVarInsn(Opcodes.ALOAD, 0);
- visitMethodInsn(Opcodes.INVOKEINTERFACE,
- "java/util/List", "add",
- "(Ljava/lang/Object;)Z");
- visitInsn(Opcodes.POP);
- visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
- visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
- visitInsn(Opcodes.DUP);
- visitLdcInsn("size: ");
- visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "
" , "(Ljava/lang/String;)V");- visitFieldInsn(Opcodes.GETSTATIC, "my/Foo", "_my_instances", "Ljava/util/List;");
- visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/List", "size", "()I");
- visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(I)Ljava/lang/StringBuilder;");
- visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;");
- visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
- }
- super.visitInsn(opcode);
- }
- };
- }
- };
我这里的操作是在无参构造函数返回之前,插入了一些指令。如果想要插入的代码比较复杂,指令肯定会很复杂,我是怎么知道要这么写的?这个问题嘛,有点难,不过呢,也是有招解的。当当当~~~当!看下面,哈哈。
10、ASM中提供了一个强悍的工具类:ASMifierClassVisitor,这个类很牛叉。给它一个类,它能把用ASM怎么生成出这个类的字节码的代码给出来。这个功能实在是太强悍了!!从细节上,我们也能看到ASM的良苦用心。赞一个!
asm guide上面是这么写的:
java -classpath asm.jar:asm-util.jar \
org.objectweb.asm.util.ASMifierClassVisitor \
java.lang.Runnable
其实吧,不适用于新版本。因为新版本的jar包名字改变了哈。最新版本,要看我的:
D:\tcc3\3.0\trunk\HelloASM\bin>java -cp ../lib/asm-3.3.1.jar;../lib/asm-util-3.3
.1.jar org.objectweb.asm.util.ASMifierClassVisitor com/taobao/Foo.class
请根据自己的实际情况,修改-cp部分的jar包的路径,我这里用的是相对路径,不要问我为什么用com/taobao/Foo.class,而不是com.taobao.Foo.class。因为,我也是试验出来的哈。如果写成com.taobao.Foo.class,会报错:
Exception in thread "main" java.io.FileNotFoundException: com.taobao.Foo.class (
系统找不到指定的文件。)
at java.io.FileInputStream.open(Native Method)
at java.io.FileInputStream.
at java.io.FileInputStream.
at org.objectweb.asm.util.ASMifierClassVisitor.main(Unknown Source)
当然啦。如果你仔细研读ASM的源码,估计也能解决。J
不过,我是凭直觉,一修改就对啦。哈哈此文是在一年前写的,当时在搞ASM相关的一些研究,也从其他网友的博客进行了学习,里边也有自己探索的一些成分。现在,把它回馈出来,我为人人,人人为我,呵呵。