任何足够先进的科技,都与魔法无异
相信你对Java编译以后的class字节码无论是在文件格式以及元数据方面已经有了很多的认识和了解,接下来我们不停留在理论的基础上,动手去操作和控制class字节码,这个介于在Java编译器和Java虚拟机之间的产物。
class 文件已经不是由你的 Java 源码编译而来,而是由程序动态生成。 能够做这件事的,有JDK中的动态代理API, 还有一个叫做 cglib 的开源库。 这两个库都是偏重于动态代理的, 也就是以动态生成 class 的方式来支持代理的动态创建。
有一个叫做 ASM 的库, 能够直接生成class文件,它的 api 对于动态代理的 API 来说更加原生, 每个api都和 class 文件格式中的特定部分相吻合, 也就是说, 如果对 class 文件的格式比较熟练, 使用这套 API 就会相对简单。 下面我们通过一个实例来讲解 ASM 的使用, 并且在使用的过程中, 会对应 class 文件中的各个部分来说明。除此之外, 还有一个叫做 ASM 的库, 能够直接生成class文件,它的 api 对于动态代理的 API 来说更加原生, 每个api都和 class 文件格式中的特定部分相吻合, 也就是说, 如果对 class 文件的格式比较熟练, 使用这套 API 就会相对简单。下面我们通过一个实例来讲解 ASM 的使用, 并且在使用的过程中, 会对应 class 文件中的各个部分来说明。
ASM 库是一款基于 Java 字节码层面的代码分析和修改工具,那 ASM 和访问者模式有什么关系呢?访问者模式主要用于修改和操作一些数据结构比较稳定的数据,通过前面的学习,我们知道 .class 文件的结构是固定的,主要有常量池、字段表、方法表、属性表等内容,通过使用访问者模式在扫描 .class 文件中各个表的内容时,就可以修改这些内容了。
ASM 可以直接生产二进制的 .class 文件,也可以在类被加载入 JVM 之前动态修改类行为。下文将通过两个例子,分别介绍如何生成一个 class 文件和修改 Java 类中方法的字节码。
ASM 会比较困难,ASM 官方也提供了一个帮助工具 ASMifier,我们可以先写出目标代码,然后通过 javac 编译成 .class 文件,然后通过 ASMifier 分析此 .class 文件就可以得到需要插入的代码对应的 ASM 代码了。
package work;
public class Example {
public static void main(String[] var0) {
System.out.println("createExampleClass");
}
}
这个 Example 类很简单,只有简单的包名,加上一个静态 main 方法,打印输出 createExampleClass 。
下面开始介绍如何使用 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, "" , "()V", null, null);
// 生成构造方法的字节码指令
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "" , "()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.println(fos.getFD());
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("类加载器是:" + Log.getClassLoader());
// 利用反射获取main方法
Method method = Log.getDeclaredMethod("main", String[].class);
String[] arg = {"ad"};
method.invoke(null, (Object) arg);
} catch (Exception e) {
e.printStackTrace();
}
}
}
下面是自定义的一个 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 (file.exists()) {
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();
}
}
return out.toByteArray();
} 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 方法定义一个类。
定义默认构造方法, 并生成默认构造方法的字节码指令
相关代码片段如下:
// 生成默认的构造方法
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "" , "()V", null, null);
// 生成构造方法的字节码指令
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "" , "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
// 构造函数访问结束
mv.visitEnd();
使用上面创建的 ClassWriter 对象, 调用该对象的 visitMethod 方法, 得到一个 MethodVisitor 对象, 这个对象定义一个方法。 对应 class 文件中的一个 method_info 。
生成class数据, 保存到磁盘中, 加载class数据
javac Example.java // 生成 Example class 文件
java -classpath asm-all-6.0_ALPHA.jar org.objectweb.asm.util.ASMifier Example.class // 利用 ASMifier 将class 文件转为 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, "" , "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "" , "()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();
return cw.toByteArray();
}
}
还是在原来的代码基础上,Main 类下面新增一个方法 modifyMethod 方法,具体代码如下:
private static void modifyMethod() {
byte[] code = null;
try {
// 需要注意把 . 变成 /, 比如 com.example.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 = writer.toByteArray();
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() {
super.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);
// 把当前的时间戳存起来
mv.visitVarInsn(LSTORE, 1);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("ChangeAdapter onMethodEnter ");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitLabel(l0);
mv.visitLdcInsn(new Long(100L));
mv.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);
mv.visitLabel(l1);
Label l3 = new Label();
mv.visitJumpInsn(GOTO, l3);
mv.visitLabel(l2);
mv.visitFrame(Opcodes.F_FULL, 2, new Object[] {"[Ljava/lang/String;", Opcodes.LONG}, 1, new Object[] {"java/lang/InterruptedException"});
mv.visitVarInsn(ASTORE, 3);
mv.visitVarInsn(ALOAD, 3);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/InterruptedException", "printStackTrace", "()V", false);
mv.visitLabel(l3);
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
// 把当前的时间戳存起来
mv.visitVarInsn(LSTORE, 3);
}
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
// 把之前存储的时间戳取出来
mv.visitVarInsn(LLOAD, 3);
mv.visitVarInsn(LLOAD, 1);
mv.visitInsn(LSUB);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(J)V", false);
}
@Override
public void visitMaxs(int i, int i1) {
super.visitMaxs(i, i1);
}
在 adapter 中,有两个非常重要的方法:
上面的代码是为了计算某个方法的耗时,我们先是在方法开始前记录了当前的时间戳,同时为了避免程序执行过快,还让该线程睡了100ms。在方法结束前,将之前的时间戳取出来,同时获取当前的时间戳,两者相减,就是方法运行耗时。
public class ChangeVisitor extends ClassVisitor {
ChangeVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM5, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
System.out.print(name);
if (name.equals("main")) {
return new ChangeAdapter(Opcodes.ASM4, methodVisitor, access, name, desc);
}
return methodVisitor;
}
}
ChangeVisitor 主要就是对 ASM 访问 class 文件方法的时候,做个拦截。如果发现方法名是 main,就让其走前面写好的 ChangeAdapter,这样,我们就可以改写 class 文件的方法了。
前面修改的 main 是没有返回值的,那么如果存在返回值?这么写还合适吗?
如果你添加了非静态方法的调用,去看生成的 class 文件也许可能是对的,但是在调用的时候就会报错。示例如下:
protected void onMethodExit(int opcode) {
mv.visitVarInsn(LLOAD, longT);
mv.visitInsn(LSUB);
mv.visitVarInsn(LSTORE, longT);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("work2 createExampleClass");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitVarInsn(LLOAD, longT);
}
从class 文件来看,生成的 class 文件是没有问题的,结果在反射调用的时候报了异常: