Javaのバイトコードで遊ぶ (Hello World)

ASMを使ってJavaバイトコードを生成して遊んでみます。 まずはその準備について。 Javaのバージョンは1.8.0_60、ASMのバージョンは5.0.3です。

生成したバイトコードを実行したり、クラスファイルに書き込みたいので そのためのユーティリティ(ExampleUtil)を準備します。 生成するクラスが持つmainメソッドを実行することにします。

private static class MyClassLoader extends ClassLoader {
  public Class<?> defineClass(String name, byte[] b) {
    return defineClass(name, b, 0, b.length);
  }
}

public static void execMain(String className, byte[] bytes)
    throws IllegalAccessException, IllegalArgumentException,
    NoSuchMethodException, SecurityException {
  MyClassLoader myClassLoader = new MyClassLoader();
  Class<?> c = myClassLoader.defineClass(className, bytes);
  try {
    c.getMethod("main", String[].class).invoke(null,
        new Object[] { new String[0] });
  } catch (InvocationTargetException e) {
    e.getCause().printStackTrace();
  }
}

public static void write(String className, byte[] bytes) throws IOException {
  File dir = new File("target" + File.separator + "output");
  if (!dir.exists())
    dir.mkdirs();
  DataOutputStream out = new DataOutputStream(
      new FileOutputStream(new File(dir, className + ".class")));
  out.write(bytes);
  out.flush();
  out.close();
}

次にバイトコードを生成する処理です。

private static byte[] generate(String className) {
  ClassWriter cw = new ClassWriter(0);
  MethodVisitor mv;

  cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null,
      "java/lang/Object", null);

  // constructor
  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();

  // 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;");

  // println
  mv.visitLdcInsn("Hello world.");
  mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
      "(Ljava/lang/Object;)V", false);

  mv.visitInsn(RETURN);
  mv.visitMaxs(2, 1);
  mv.visitEnd();
  cw.visitEnd();

  return cw.toByteArray();
}

generateメソッドが返すbyte[]を受け取って、実行したり、ファイルに出力したりする部分。

public static void main(String... args) throws Exception {
  String className = "HelloWorld";
  byte[] bytes = generate(className);
  ExampleUtil.execMain(className, bytes);
  ExampleUtil.write(className, bytes);
}

これを実行して、「Hello world.」という文字列が表示されればOKです。 出力したクラスファイルを実行するには以下のコマンド。

$ java HelloWorld

Hello world.

出力したクラスファイルの中身を覗く場合は以下。オプションは適宜変更すればOKです。

$ javap -v -p -s HelloWorld

Classfile /home/vagrant/sandbox/workspaces/asm_practice/AsmPlaying/target/output/HelloWorld.class
  Last modified 2015/10/13; size 341 bytes
  MD5 checksum 01828f9e45f7fe516438e7d3c51b04f4
public abstract class HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Utf8               HelloWorld
   #2 = Class              #1             // HelloWorld
   #3 = Utf8               java/lang/Object
   #4 = Class              #3             // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = NameAndType        #5:#6          // "<init>":()V
   #8 = Methodref          #4.#7          // java/lang/Object."<init>":()V
   #9 = Utf8               main
  #10 = Utf8               ([Ljava/lang/String;)V
  #11 = Utf8               java/lang/System
  #12 = Class              #11            // java/lang/System
  #13 = Utf8               out
  #14 = Utf8               Ljava/io/PrintStream;
  #15 = NameAndType        #13:#14        // out:Ljava/io/PrintStream;
  #16 = Fieldref           #12.#15        // java/lang/System.out:Ljava/io/PrintStream;
  #17 = Utf8               Hello world.
  #18 = String             #17            // Hello world.
  #19 = Utf8               java/io/PrintStream
  #20 = Class              #19            // java/io/PrintStream
  #21 = Utf8               println
  #22 = Utf8               (Ljava/lang/Object;)V
  #23 = NameAndType        #21:#22        // println:(Ljava/lang/Object;)V
  #24 = Methodref          #20.#23        // java/io/PrintStream.println:(Ljava/lang/Object;)V
  #25 = Utf8               Code
{
  public HelloWorld();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #16                 // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #18                 // String Hello world.
         5: invokevirtual #24                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
         8: return
}

これをベースに次回からいろいろなバイトコードを生成して遊んでみたいと思います。 最後にコードの全体を載せておきます。

ExampleUtil

package example;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;

public class ExampleUtil {

  private static class MyClassLoader extends ClassLoader {
    public Class<?> defineClass(String name, byte[] b) {
      return defineClass(name, b, 0, b.length);
    }
  }

  public static void execMain(String className, byte[] bytes)
      throws IllegalAccessException, IllegalArgumentException,
      NoSuchMethodException, SecurityException {
    MyClassLoader myClassLoader = new MyClassLoader();
    Class<?> c = myClassLoader.defineClass(className, bytes);
    try {
      c.getMethod("main", String[].class).invoke(null,
          new Object[] { new String[0] });
    } catch (InvocationTargetException e) {
      e.getCause().printStackTrace();
    }
  }

  public static void write(String className, byte[] bytes) throws IOException {
    File dir = new File("target" + File.separator + "output");
    if (!dir.exists())
      dir.mkdirs();
    DataOutputStream out = new DataOutputStream(
        new FileOutputStream(new File(dir, className + ".class")));
    out.write(bytes);
    out.flush();
    out.close();
  }

}

HelloWorld

package example;

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

public class HelloWorld implements Opcodes {
  private static byte[] generate(String className) {
    ClassWriter cw = new ClassWriter(0);
    MethodVisitor mv;

    cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, className, null,
        "java/lang/Object", null);

    // constructor
    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();

    // 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;");

    // println
    mv.visitLdcInsn("Hello world.");
    mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println",
        "(Ljava/lang/Object;)V", false);

    mv.visitInsn(RETURN);
    mv.visitMaxs(2, 1);
    mv.visitEnd();
    cw.visitEnd();

    return cw.toByteArray();
  }

  public static void main(String... args) throws Exception {
    String className = "HelloWorld";
    byte[] bytes = generate(className);
    ExampleUtil.execMain(className, bytes);
    ExampleUtil.write(className, bytes);
  }
}