通过ASM库⽣成和修改class⽂件
在主要详细讲解了Class⽂件的格式,并且在上⼀篇⽂章中做了总结。众所周知,JVM 在运⾏时,加载并执⾏class⽂件,这个class⽂件基本上都是由我们所写的java源⽂件通过 javac 编译⽽得到的。但是,我们有时候会遇到这种情况:在前期(编写程序时)不知道要写什么类,只有到运⾏时,才能根据当时的程序执⾏状态知道要使⽤什么类。举⼀个常见的例⼦就是 JDK 中的动态代理。这个代理能够使⽤⼀套API代理所有的符合要求的类,那么这个代理就不可能在 JDK 编写的时候写出来,因为当时还不知道⽤户要代理什么类。
当遇到上述情况时,就要考虑这种机制:在运⾏时动态⽣成class⽂件。也就是说,这个 class ⽂件已经不是由你的 Java 源码编译⽽来,⽽是由程序动态⽣成。能够做这件事的,有JDK中的动态代理API,还有⼀个叫做 cglib 的开源库。这两个库都是偏重于动态代理的,也就是以动态⽣成 class 的⽅式来⽀持代理的动态创建。除此之外,还有⼀个叫做 ASM 的库,能够直接⽣成class⽂件,它的 api 对于动态代理的 API 来说更加原⽣,每个api都和 class ⽂件格式中的特定部分相吻合,也就是说,如果对 class ⽂件的格式⽐较熟练,使⽤这套 API 就会相对简单。下⾯我们通过⼀个实例来讲解 ASM 的使⽤,并且在使⽤的过程中,会对应 class ⽂件中的各个部分来说明。
ASM 库的介绍和使⽤
ASM 库是⼀款基于 Java 字节码层⾯的代码分析和修改⼯具,那 ASM 和访问者模式有什么关系呢?访问者模式主要⽤于修改和操作⼀些数据结构⽐较稳定的数据,通过前⾯的学习,我们知道 .class ⽂件的结构是固定的,主要有常量池、字段表、⽅法表、属性表等内容,通过使⽤访问者模式在扫描 .class ⽂件中各个表的内容时,就可以修改这些内容了。在学习 ASM 之前,可以通过这篇⽂章学习⼀下访问者模式。ASM 可以直接⽣产⼆进制的 .class ⽂件,也可以在类被加载⼊ JVM 之前动态修改类⾏为。下⽂将通过两个例⼦,分别介绍如何⽣成⼀个class ⽂件和修改 Java 类中⽅法的字节码。
在刚开始使⽤的时候,可能对字节码的执⾏不是很清楚,使⽤ ASM 会⽐较困难,ASM 官⽅也提供了⼀个帮助⼯具 ASMifier,我们可以先写出⽬标代码,然后通过 javac 编译成 .class ⽂件,然后通过 ASMifier 分析此 .class ⽂件就可以得到需要插⼊的代码对应的 ASM 代码了。
ASM ⽣成 class ⽂件
下⾯简单看⼀个 java 类:
package work;
public class Example {public static void main(String[] var0) {
System.out.println("createExampleClass");
}
}
这个 Example 类很简单,只有简单的包名,加上⼀个静态 main ⽅法,打印输出 createExampleClass 。
现在问题来了,你如何⽣成这个 Example.java 的 class ⽂件,不能在开发时通过上⾯的源码来编译成,⽽是要动态⽣成。
下⾯开始介绍如何使⽤ ASM 动态⽣成上述源码对应的字节码。
代码⽰例
public class Main extends ClassLoader {
// 此处记得替换成⾃⼰的⽂件地址
public static final String PATH = "/Users/xxx/IdeaProjects/untitled/src/work/";
public static void main(String[] args) {
createExampleClass();
}
private static void createExampleClass() {
ClassWriter cw = new ClassWriter(0);
// 定义⼀个叫做Example的类,并且这个类是在 work ⽬录下⾯
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "work/Example", null, "java/lang/Object", null);
// ⽣成默认的构造⽅法
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
// ⽣成构造⽅法的字节码指令
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
// 构造函数访问结束
mv.visitEnd();
// ⽣成main⽅法中的字节码指令
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitCode();
// 获取该⽅法
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// 加载字符串参数
mv.visitLdcInsn("createExampleClass");
/
/ 调⽤该⽅法
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
// 获取⽣成的class⽂件对应的⼆进制流
byte[] code = cw.toByteArray();
// 将⼆进制流写到本地磁盘上
FileOutputStream fos = null;
try {
fos = new FileOutputStream(PATH + "Example.class");
fos.write(code);
System.out.FD());
fos.close();
} catch (Exception e) {
System.out.print(" FileOutputStream error " + e.getMessage());
e.printStackTrace();
}
loadclass("Example.class", "work.Example");
}
private static void loadclass(String className, String packageNamePath) {
//通过反射调⽤main⽅法
MyClassLoader myClassLoader = new MyClassLoader(PATH + className);
// 类的全称,对应包名
try {
// 加载class⽂件
Class<?> Log = myClassLoader.loadClass(packageNamePath);
System.out.println("类加载器是:" + ClassLoader());
// 利⽤反射获取main⽅法
Method method = DeclaredMethod("main", String[].class);
String[] arg = {"ad"};
method.invoke(null, (Object) arg);字符串常量池存的是实例还是引用?
} catch (Exception e) {
e.printStackTrace();
}
}
}
为了证明表⽰我们⽣成的 class 可以正常调⽤,还需要将其加载,然后通过反射调⽤该类的⽅法,这样才能说明⽣成的 class ⽂件是没有问题并且可运⾏的。
下⾯是⾃定义的⼀个 class 加载类:
public class MyClassLoader extends ClassLoader {
// 指定路径
private String path;
public MyClassLoader(String classPath) {
path = classPath;
}
/**
* 重写findClass⽅法
*
* @param name 是我们这个类的全路径
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class log = null;
/
/ 获取该class⽂件字节码数组
byte[] classData = getData();
if (classData != null) {
// 将class的字节码数组转换成Class类的实例
log = defineClass(name, classData, 0, classData.length);
}
return log;
}
/**
* 将class⽂件转化为字节码数组
*
* @return
*/
private byte[] getData() {
File file = new File(path);
if (ists()) {
FileInputStream in = null;
ByteArrayOutputStream out = null;
try {
in = new FileInputStream(file);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int size = 0;
while ((size = in.read(buffer)) != -1) {
out.write(buffer, 0, size);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
ByteArray();
} else {
return null;
}
}
}
代码详解
下⾯详细介绍⽣成class的过程:
⾸先定义⼀个类
相关代码⽚段如下:
ClassWriter cw = new ClassWriter(0);
// 定义⼀个叫做Example的类,并且这个类是在 work ⽬录下⾯
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "work/Example", null, "java/lang/Object", null);
ClassWriter 类是 ASM 中的核⼼ API ,⽤于⽣成⼀个类的字节码。 ClassWriter 的 visit ⽅法定义⼀个类。
第⼀个参数 V1_8 是⽣成的 class 的版本号,对应class⽂件中的主版本号和次版本号,即 minor_version 和 major_version 。
第⼆个参数ACC_PUBLIC表⽰该类的访问标识。这是⼀个public的类。对应class⽂件中的access_flags 。
第三个参数是⽣成的类的类名。需要注意,这⾥是类的全限定名。如果⽣成的class带有包名,如Example,那么这⾥传⼊的参数必须是com/jg/xxx/Example  。对应 class ⽂件中的 this_class  。
第四个参数是和泛型相关的,这⾥我们不关新,传⼊null表⽰这不是⼀个泛型类。这个参数对应class⽂件中的Signature属性
(attribute)。
第五个参数是当前类的⽗类的全限定名。该类直接继承Object。这个参数对应class⽂件中的super_class 。
第六个参数是 String[] 类型的,传⼊当前要⽣成的类的直接实现的接⼝。这⾥这个类没实现任何接⼝,所以传⼊null 。这个参数对应class⽂件中的interfaces 。
定义默认构造⽅法,并⽣成默认构造⽅法的字节码指令
相关代码⽚段如下:
// ⽣成默认的构造⽅法
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
// ⽣成构造⽅法的字节码指令
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
// 构造函数访问结束
mv.visitEnd();
使⽤上⾯创建的 ClassWriter 对象,调⽤该对象的 visitMethod ⽅法,得到⼀个 MethodVisitor 对象,这个对象定义⼀个⽅法。对应 class ⽂件中的⼀个 method_info 。
第⼀个参数是 ACC_PUBLIC ,指定要⽣成的⽅法的访问标志。这个参数对应 method_info 中的 access_flags 。
第⼆个参数是⽅法的⽅法名。对于构造⽅法来说,⽅法名为 <init> 。这个参数对应 method_info 中的 name_index , name_index 引⽤常量池中的⽅法名字符串。
第三个参数是⽅法描述符,在这⾥要⽣成的构造⽅法⽆参数,⽆返回值,所以⽅法描述符为 ()V  。这个参数对应 method_info 中的descriptor_index 。
第四个参数是和泛型相关的,这⾥传⼊null表⽰该⽅法不是泛型⽅法。这个参数对应 method_info 中的 Signature 属性。
第五个参数指定⽅法声明可能抛出的异常。这⾥⽆异常声明抛出,传⼊ null 。这个参数对应 method_info 中的 Exceptions 属性。接下来调⽤ MethodVisitor 中的多个⽅法,⽣成当前构造⽅法的字节码。对应 method_info 中的 Code 属性。
1. 调⽤ visitVarInsn ⽅法,⽣成 aload 指令,将第 0 个本地变量(也就是 this)压⼊操作数栈。
2. 调⽤ visitMethodInsn⽅法,⽣成 invokespecial 指令,调⽤⽗类(也就是 Object)的构造⽅法。
3. 调⽤ visitInsn ⽅法,⽣成 return 指令,⽅法返回。
4. 调⽤ visitMaxs ⽅法,指定当前要⽣成的⽅法的最⼤局部变量和最⼤操作数栈。对应 Code 属性中的 max_stack 和 max_locals 。
5. 最后调⽤ visitEnd ⽅法,表⽰当前要⽣成的构造⽅法已经创建完成。
定义main⽅法,并⽣成main⽅法中的字节码指令
这⾥与构造函数⼀样,就不多说了。
⽣成class数据,保存到磁盘中,加载class数据
// 获取⽣成的class⽂件对应的⼆进制流
byte[] code = cw.toByteArray();
// 将⼆进制流写到本地磁盘上
FileOutputStream fos = null;
try {
fos = new FileOutputStream(PATH + "Example.class");
fos.write(code);
fos.close();
} catch (Exception e) {
System.out.print(" FileOutputStream error " + e.getMessage());
e.printStackTrace();
}
loadclass("Example.class", "work.Example");
这段代码执⾏完,可以看到控制台有以下输出:
⽣成 ASM 代码
那么还有个问题是前⾯的 ASM 代码是如何⽣成的呢?
还是以前⽂提到的 EXample.java 为例:
javac Example.java  // ⽣成 Example class ⽂件
java -classpath asm-all-6.0_ALPHA.jar org.objectweb.asm.util.ASMifier Example.class// 利⽤ ASMifier 将class ⽂件转为 asm 代码
在 Terminal 窗⼝中输⼊这两个命令,就可以得到下⾯的 asm 代码:
import java.util.*;
import org.objectweb.asm.*;
public class ExampleDump implements Opcodes {
public static byte[] dump () throws Exception {
ClassWriter cw = new ClassWriter(0);
FieldVisitor fv;
MethodVisitor mv;
AnnotationVisitor av0;
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, "Example", null, "java/lang/Object", null);
{
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
}
{
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("createExampleClass");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
}
cw.visitEnd();
ByteArray();
}
}
可以看到输出结果与前⾯的⽣成的 class ⽂件的代码是⼀样的。
到这⾥,相信你对 ASM 的使⽤已经有了初步的了解了,当然可能不是很熟悉,但是多写写练练掌握格式就好多了。
利⽤ ASM 修改⽅法
下⾯介绍如何修改⼀个 class ⽂件的⽅法。
还是在原来的代码基础上,Main 类下⾯新增⼀个⽅法 modifyMethod ⽅法,具体代码如下:
private static void modifyMethod() {
byte[] code = null;
try {
// 需要注意把 . 变成 /, ⽐如 ample.a.class 变成 com/example/a.class
InputStream inputStream = new FileInputStream(PATH + "Example.class");
ClassReader reader = new ClassReader(inputStream);                              // 1. 创建 ClassReader 读⼊ .class ⽂件到内存中
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS);                // 2. 创建 ClassWriter 对象,将操作之后的字节码的字节数组回写
ClassVisitor change = new ChangeVisitor(writer);                                        // 3. 创建⾃定义的 ClassVisitor 对象
reader.accept(change, ClassReader.EXPAND_FRAMES);
code = ByteArray();
System.out.println(code);
FileOutputStream fos = new FileOutputStream(PATH + "Example.class");
fos.write(code);
fos.close();
} catch (Exception e) {
System.out.println("FileInputStream " + e.getMessage());
e.printStackTrace();
}
try {
if (code != null) {
System.out.println(code);
FileOutputStream fos = new FileOutputStream(PATH + "Example.class");
fos.write(code);
fos.close();
}
} catch (Exception e) {
System.out.println("FileOutputStream ");
e.printStackTrace();
}
loadclass("Example.class", "work.Example");
}
新建⼀个 adapter,继承⾃ AdviceAdapter,AdviceAdapter 本质也是⼀个 MethodVisitor,但是⾥⾯对很多对⽅法的操作逻辑进⾏了封装,使得我们不⽤关⼼ ASM 内部的访问逻辑,只需要在对应的⽅法下⾯添加代码逻辑即可。
public class ChangeAdapter extends AdviceAdapter {
private String methodName = null;
ChangeAdapter(int api, MethodVisitor mv, int access, String name, String desc) {
super(api, mv, access, name, desc);
methodName = name;
}
@Override
protected void onMethodEnter() {
Label l0 = new Label();
Label l1 = new Label();
Label l2 = new Label();
mv.visitTryCatchBlock(l0, l1, l2, "java/lang/InterruptedException");
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
// 把当前的时间戳存起来

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。