玩转Java字节码(下)

  接上篇文章,操作字节码的框架有很多Javassist,ASM,BCEL等,这里我用ASM来举例。在了解字节码组成后,很容易通过ASM构建一个Class出来,代码如下:

package classloader;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * Created by dorole.com on 2016/6/13.
 */
public class HelloClassGeneratorTest {
    public static void main(String[] args) throws IOException {
        ClassWriter classWriter = new ClassWriter(0);
        classWriter.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC, "Hello", null, "java/lang/Object", null);

        // 构造方法
        MethodVisitor constructorMethod = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        constructorMethod.visitCode();
        constructorMethod.visitVarInsn(Opcodes.ALOAD, 0);
        constructorMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        constructorMethod.visitInsn(Opcodes.RETURN);
        constructorMethod.visitMaxs(1, 1);
        constructorMethod.visitEnd();

        // sayHello方法
        MethodVisitor helloMethod = classWriter.visitMethod(Opcodes.ACC_PUBLIC, "sayHello", "(Ljava/lang/String;)Ljava/lang/String;", null, null);
        helloMethod.visitCode();
        helloMethod.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        helloMethod.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        helloMethod.visitInsn(Opcodes.DUP);
        helloMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        helloMethod.visitVarInsn(Opcodes.ALOAD, 0);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        helloMethod.visitLdcInsn(" -> ");
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        helloMethod.visitVarInsn(Opcodes.ALOAD, 1);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        helloMethod.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
        helloMethod.visitVarInsn(Opcodes.ALOAD, 1);
        helloMethod.visitInsn(Opcodes.ARETURN);
        helloMethod.visitMaxs(3, 2);
        helloMethod.visitEnd();

        classWriter.visitEnd();

        byte[] data = classWriter.toByteArray();
        File file = new File("D://Hello.class");
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        fileOutputStream.write(data);
        fileOutputStream.close();
    }
}

  内容很简单,通过ClassWriter创建了一个Hello类,定义了一个构造方法和sayHello方法,接收一个String参数,返回String类型。将得到的Class字节码保存到D://Hello.class文件中,这个文件可以用jd-gui反编译回Java源代码。

  ASM设计模式很有意思,访问者模式(Visitor Pattern),我常常把它比作一个拥有多层的抽屉的模具,一层一层打开,放原材料,最后产出一个东西。MethodVisitor的用法可以参考文档,这里不详述,大致上都是一些字节码指令操作。

  以上代码也是模仿了javap反编译出来的内容,运行后无误的话可以看到在D盘有了一个Hello.class文件,接下来我们要加载这个文件,并运行其中的方法。代码如下:

package classloader;

/**
 * Created by dorole.com on 2016/6/13.
 */
public class MyLoader extends ClassLoader {
    public Class<?> defineMyClass(byte[] b, int off, int len) {
        return super.defineClass(null, b, off, len);
    }
}

  自定义一个Classloader,仅调用父类的defineClass即可。

package classloader;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * Created by dorole.com on 2016/6/13.
 */
public class MyLoaderTest {
    public static void main(String[] args) throws IOException, IllegalAccessException, InstantiationException {
        File file = new File("D://Hello.class");
        InputStream input = new FileInputStream(file);
        byte[] result = new byte[1024];

        int count = input.read(result);
        MyLoader loader = new MyLoader();
        Class clazz = loader.defineMyClass(result, 0, count);
        System.out.println(clazz.getCanonicalName());

        Object o = clazz.newInstance();
        try {
            Method method = clazz.getMethod("sayHello", String.class);
            Object returnObject = method.invoke(o, "World!");
            System.out.println("returnObject = " + returnObject);
        } catch (IllegalArgumentException | InvocationTargetException
                | NoSuchMethodException | SecurityException e) {
            e.printStackTrace();
        }
    }
}

  将class以字节数组形式读取给Classloader来加载,通过反射调用sayHello方法,专递一个World!参数进去,可以看到输出以下结果:

Hello
Hello -> World!
returnObject = World!

  这样一个从Java代码编译与反编译,自己生成字节码到加载运行,还能反编译回去。这样一个完整的循环就算完成了,虽然谈不上很有深度,但从这里能引发很多值得思考的地方。更多内容还是要参考Java虚拟机规范。