前些日工作中需要改一个jar包中的程序代码,想了很多方法 包括直接修改、动态代理等等,但都效果不好。最后无意中发现了ASM这个框架,感觉正是我需要的。研究几日,用到一些基础功能就实现了所要效果,所以写出来给大家共享,自己忘了也好参考参考.下面是ASM一些基本介绍,就当是抛砖引玉了~~其中参考了几篇其他iteye朋友的文章,主要是JVM、字节码和类加载方面的东西,有助于更好的理解ASM。这里我主要说说ASM的东西,其他的大家去看看吧。
一、什么是ASM
ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。
二、ASM核心类介绍:
①ClassReader类:这个类会提供你要转变的类的字节数组,它的accept方法,接受一个具体的ClassVisitor,并调用实现中具体的visit,visitSource, visitOuterClass, visitAnnotation, visitAttribute, visitInnerClass, visitField,visitMethod和 visitEnd方法。
②ClassWriter类:这个类是ClassVisitor的一个实现类,这个类中的toByteArray方法会将最终修改的字节码以byte数组形式返回,在这个类的构造时可以指定让系统自动为我们计算栈和本地变量的大小(COMPUTE_MAXS),也可以指定系统自动为我们计算栈帧的大小(COMPUTE_FRAMES)。
③ClassAdapter类:这个类也是ClassVisitor的一个实现类,这个类可以看成是一个事件过滤器,在这个类里,它对ClassVisitor的实现都是委派给一个具体的ClassVisitor实现类,即调用那个实现类实现的方法。
④FieldVisitor接口:
这个接口定义了和属性结构相对应的方法,这些方法可以操作属性。
⑤MethodVisitor接口:这个接口定义了和方法结构相对应的方法,这些方法可以去操作源方法,具体的可以查看一下源码。
三、操作流程:
一般情况下,我们需要操作一个类时,首先是获得其二进制的字节码,即用ClassReader来读取一个类,然后需要一个能将二进制字节码写回的类,即用ClassWriter类,最后就是一个事件过滤器,即ClassAdapter。事件过滤器中的某些方法可以产生一个新的XXXVisitor对象,当我们需要修改对应的内容时只要实现自己的XXXVisitor并返回就可以了。
四、程序示例:
所需开发包:asmSDK
所需开发包:asmSDK
功能:使用ASM在Person.class文件上动态添加一条打印语句
开发工具:jdk6、 Myeclipse
下面是程序的树形图
Person.java文件代码如下:
public class Person {
public final String sleep() {
try {
Thread.sleep(2);//沉睡两秒
} catch (InterruptedException e) {
e.printStackTrace();
}
return "123";
}
}
ModifyMethodClassAdapter.java代码如下:
package com.asm1;
import org.objectweb.asm.ClassAdapter;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
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) {
//(Ljava/lang/String;[BII)Ljava/lang/Class;
//(Ljava/lang/String;[BIILjava/security/ProtectionDomain;)Ljava/lang/Class;
int a=10;
if (name.equals("sleep")&&desc.equals("()Ljava/lang/String;")) {
return new ModifyMethod(super.visitMethod(access, name, desc,
signature, exceptions), access, name, desc);
}
//System.out.println(a);
return super.visitMethod(access, name, desc, signature, exceptions);
}
@Override
public void visitEnd() {
cv.visitField(Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC, "timer", "J",
null, null);
}
}
注意name.equals("sleep")&&desc.equals("()Ljava/lang/String;")这行,ASM会根据方法名和方法描述来查找方法,下面是方法描述在类中和二进制中的对应关系
|
ModifyMethod.java代码如下:
package com.asm1;
import org.objectweb.asm.MethodAdapter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
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, "java/lang/System", "out",
"Ljava/io/PrintStream;");
mv.visitLdcInsn("我是赛亚人");
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream",
"println", "(Ljava/lang/String;)V");
}
}
代码中各方法
visitFieldInsn:访问一个属性的指令,该指令负责加载或储存一个对象的属性值
visitLdcInsn: 将常量池中的字符串常量加载进栈顶
visitMethodInsn:访问方法的指令,该指令负责调用一个方法
visitLdcInsn: 将常量池中的字符串常量加载进栈顶
visitMethodInsn:访问方法的指令,该指令负责调用一个方法
这里只用到三个方法,其他的可以看下ASM API文档
下面写了一个test类,里面是要生成的代码,
test.java代码如下:
public class test {
public static void main(String[] args) {
System.out.println("我是赛亚人");
}
}
首先我们使用javap命令计算字节码指令:
javac test.java
javap -verbose test
生成字节码如下:
该文件是class的字节码指令集,可查看虚拟机字节码指令表来进一步了解。
可以看出
ModifyMethod.java的程序和该字节码是一一对应的。比如
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out","Ljava/io/PrintStream;");和
getstatic #2;//Field java/lang/System.out:Ljava/io/PrintStream;,
只要我们使用javap计算出程序的字节码指令就可以使用ASM程序动态添加了。
测试类ModifyMethodTest.java代码如下:
ClassReader classReader = new ClassReader("com.asm1.Person"); //com.asm1.Person
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassAdapter classAdapter = new ModifyMethodClassAdapter(classWriter);
classReader.accept(classAdapter, ClassReader.SKIP_DEBUG);
byte[] classFile = classWriter.toByteArray();
File f = new File("F:\\abc\\aaa.class");
FileOutputStream fs = new FileOutputStream(f);
fs.write(classFile);
fs.close();
System.out.println("success");
运行该程序,在abc文件夹下会生成一个aaa.class文件,使用反编译软件打开该文件如下:
如图所示,成功添加了一行打印语句。
说明使用ASM动态修改class成功!!