読者です 読者をやめる 読者になる 読者になる

バイトコード操作でフィールドの追加をする

バイトコード操作を使って 本記事では、フィールドを追加してみます。

対象クラスをAfterのような形に操作してみます。 (テストではstaticな内部クラス)

// Before
class Some{}

// After
class Some{
  private String foo;
}

Javassistでフィールドを追加する。

まずはJavassistで追加してみます。 手続きチックですが、特に難しい内容ではないかと思います。

  @Test
  public void testAddFieldWithJavassist() throws NotFoundException, CannotCompileException, IOException {
    ClassPool pool = ClassPool.getDefault();
    CtClass clazz = pool.get(Some.class.getName());
    CtClass string = pool.get("java.lang.String");
    CtField field = new CtField(string, "foo", clazz);
    field.setModifiers(Modifier.PRIVATE);
    clazz.addField(field);
    byte[] bytes = clazz.toBytecode();
  }

cglibでフィールドを追加する。

では次にcglibでフィールドを追加してみましょう。 ドキュメントを見たところ 以下のコードがサンプルとして書かれています。

  e.setStrategy(new DefaultGeneratorStrategy() {
    protected ClassGenerator transform(ClassGenerator cg) {
      return new TransformingGenerator(cg,
        new AddPropertyTransformer(new String[]{ "foo" },
                                   new Class[]{ Integer.TYPE }));
    }});

早速ですが、このコードは動きません。 というか、動きも想定していたのと違います。

動かないのは置いといて想定していた動きと違う、というのは若干いちゃもんなのですが 少し説明します。 上記コードが動作すると、$cglib_props_fooという名前でフィールドがprivateで定義された上で getFoo, setFooといった、Java Beansを意識した「プロパティ」という概念の追加のようです。

それでは、こちらがcglibで書いたソースです。

  @Test
  public void testAddFieldWithcglib() throws IOException, Exception {
    GeneratorStrategy generator = new DefaultGeneratorStrategy() {
      @Override
      protected ClassGenerator transform(ClassGenerator cg) throws Exception {
        return new TransformingClassGenerator(cg, new ClassEmitterTransformer() {
          @Override
          public void end_class() {
            if (!TypeUtils.isAbstract(getAccess())) {
              this.declare_field(Constants.ACC_PRIVATE, "foo", Type.getType(String.class), null);
            }
            super.end_class();
          };
        });
      }
    };
    byte[] bytes = generator.generate(new ClassReaderGenerator(new ClassReader(Some.class.getName()), ClassReader.EXPAND_FRAMES));
    Dump.dump(bytes);
  }

サンプルコードとして記述したので今回は無名クラスでDefaultGeneratorStrategyを拡張する形で記述しました。 また、AddPropertyTransformerの親クラスである、ClassEmitterTransformerを拡張する形で フィールドの追加処理をend_classメソッドをオーバーライドして記述しました。

いくつかASMのAPIが顔を覗かせています。 また、なぜかsnake_caseのメソッドがいます。私、気になります!

なんかこう、もう少しやりようがあったのかもしれないですが 内部APIをそこまで覗く気になれなかったので置いておきます。

cglibにはEnhancerというクラスがいたりするのですが こいつではクラスの生成からインスタンスの生成までを隠蔽する形になっていたので 今回は利用できませんでした。バイトコードの生成がしたかったので。

ClassLoaderのdefineClassのメソッドで生成したバイト列を取得するようなものを作れば 取得ができるかと思います。 あるいはAttach API(参考)を使えば、バイトコードを取得できるかと思いますが いずれにしてもcglibでバイトコードを取得するのはオーバーエンジニアリングですね(※)。

※この記事を最後まで書いていて思いましたが Enhancerが生成するバイト列をどこかのメソッドをオーバーライドすれば取得できるかもしれません。

Byte Buddyでフィールドを追加する

Byte Buddyでフィールドを追加してみます。 楽ちんです。

メソッドチェインで書けて気持ちいいですね。

  @Test
  public void testAddFieldWithByteBuddy() throws IOException {
    byte[] bytes = new ByteBuddy().rebase(Some.class)
      .defineField("foo", String.class, Visibility.PRIVATE)
      .make()
      .getBytes();
    Dump.dump(bytes);
  }

まとめ

JavassistやByte Buddyではバイトコードの操作が楽な印象を受けます。 というか対象とする範囲がcglibとは違うんでしょうね。

また、cglibやByte BuddyではASMのラッパーとして書かれていますが JavassistではASMに依存してません。強い。

今回は簡単なサンプルでしたが 次はメソッドの追加でもやってみることにします。

ラムダ式、完全マスターした。

今日はここのところずっと調べていたラムダ式、およびメソッド参照について調べていましたので
それをこの記事でまとめたいと思います。

まずはじめに。

ここに、テストコードを含む、ラムダ式およびその他のコードをあわせた、3種類のコードを用意した。
もちろん、ラムダ式(というかメソッド参照)に加えて、staticなinner classとinner classを利用したコードである。

public class LambdaTest {  
  private static void privateMethod(String str) {  
  
  }  
  
  @Test  
  public void test1() {  
    System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");  
    Consumer<String> consumer = LambdaTest::privateMethod;  
    consumer.accept("lambda");  
  }  
  
  private static class StaticNestedClass implements Consumer<String> {  
    @Override  
    public void accept(String str) {  
      privateMethod(str);  
    }  
  }  
  
  @Test  
  public void test2() {  
    new StaticNestedClass().accept("staticNestedClass");  
  }  
  
  private class InnerClass implements Consumer<String> {  
    @Override  
    public void accept(String str) {  
      privateMethod(str);  
    }  
  }  
  
  @Test  
  public void test3() {  
    new InnerClass().accept("innerClass");  
  }  
}  

今回見るのはこのクラスをコンパイルしたときに吐き出される、バイトコードが中心となる。

ラムダ式・メソッド参照とは

まずはじめに、Java8から、ラムダ式およびメソッド参照の機能が追加された。
この機能はどういう機能かというと
関数型インターフェースをラムダ式と呼ばれる記法で定義することができたり
インスタンスやクラスのメソッドを関数型インターフェースとして取り扱いやすくするための機能である。

初めに見せたコード中に存在する、以下のコードはメソッド参照を利用して
クラス・メソッドを関数型インターフェースに変換している(うまい言い方が思い浮かばないが、要は委譲である)。

    Consumer<String> consumer = LambdaTest::privateMethod;  

これをラムダ式で書くと以下のようになる。
JavaScriptだとarrow functionは=>になるので、よく間違える。

    Consumer<String> consumer = (str) -> privateMethod(str);  
    // 次のような形でも記述可能  
    //Consumer<String> consumer = (String str) -> privateMethod(str);  

ラムダ式・メソッド参照の簡単な概略については以上となる。

内部クラスとは何が違うのか

では、今回用意した3種類のコードは何が違うのか、というと。
javapしてみます。
まずは、InnerClassから

$ > javap -v LambdaTest$InnerClass.class  
...省略  
 public void accept(java.lang.String);  
  descriptor: (Ljava/lang/String;)V  
  flags: ACC_PUBLIC  
  Code:  
    stack=1, locals=2, args_size=2  
       0: aload_1  
       1: invokestatic  #23                 // Method com/github/wreulicke/lambda/LambdaTest.access$0:(Ljava/lang/String;)V  
       4: return  
    LineNumberTable:  
      line 42: 0  
      line 43: 4  
    LocalVariableTable:  
      Start  Length  Slot  Name   Signature  
          0       5     0  this   Lcom/github/wreulicke/lambda/LambdaTest$InnerClass;  
          0       5     1     t   Ljava/lang/String;  
...省略  

いくつか合成メソッドが生成されているが、一旦置いておく。
今回ソースで実装したacceptメソッドの記述を見ると
invokestaticでcom/github/wreulicke/lambda/LambdaTest.access$0:(Ljava/lang/String;)Vといったシグネチャのメソッドを呼び出している。

ソース上ではこんなものは定義していない。

では、LambdaTestのjavapをしてみよう。

$ > javap -v LambdaTest$InnerClass.class  
...省略  
  static void access$0(java.lang.String);  
    descriptor: (Ljava/lang/String;)V  
    flags: ACC_STATIC, ACC_SYNTHETIC  
    Code:  
      stack=1, locals=1, args_size=1  
         0: aload_0  
         1: invokestatic  #66                 // Method privateMethod:(Ljava/lang/String;)V  
         4: return  
      LineNumberTable:  
        line 8: 0  
      LocalVariableTable:  
        Start  Length  Slot  Name   Signature  
...省略  

コンパイラにより、バイトコード内に合成メソッドが定義されている。これを呼び出しているようだ。
中の処理はinvokestaticで実装で記述したprivateMethodの呼び出しをしているだけである。

じゃあStaticNestedなクラスでは何を呼び出しているかというと、一緒の合成メソッドを呼び出してあった。
なので飛ばす。

ではお待ちかねのメソッド参照で記述されたバイトコードを見てみよう。
ちなみに、ラムダ式を記述したときは、クラスファイルは生成されない。なので、ダンプした。
バイトコードのダンプの仕方は一番下にある。というかソースに書いてる。

...省略  
$ > javap -v LambdaTest$$Lambda$1.class  
  public void accept(java.lang.Object);  
    descriptor: (Ljava/lang/Object;)V  
    flags: ACC_PUBLIC  
    Code:  
      stack=1, locals=2, args_size=2  
         0: aload_1  
         1: checkcast     #14                 // class java/lang/String  
         4: invokestatic  #20                 // Method com/github/wreulicke/lambda/LambdaTest.privateMethod:(Ljava/lang/String;)V  
         7: return  
...省略  

おや?さっきと違ったコードが吐き出されている。
invokestaticで合成メソッド経由ではなく直接privateメソッドを呼び出している。

この点が内部クラスprivateMethodを呼び出した時に違う点だ。(他のアクセス修飾子の場合で網羅的に調べてない点についてはごめんなさい。)

自分はこの時にバイトコードにはアクセス修飾子は関係ないのかと勘違いしてしまった。
ちなみに、ASMでラムダ式が吐くようなバイトコードを吐いたところ、IllegalAccessErrorが発生したので
ちゃんとアクセス修飾子による制限は存在するようだ。

ラムダ式・メソッド参照で記述されたインスタンスはどこからやってくるのか

これについては既存の資料に大抵記述されている。

ラムダと invokedynamic の蜜月
Java8におけるindyとLambdaの絶妙な関係、もしくはSAMタイプを継承する内部クラスの.classファイルはどこへ行ったの? - uehaj's blog
Lambda が indy に出会ったら ... Java etc.../ウェブリブログ

invokedynamicとbootstrapメソッドに関して

バイトコードにinvokedynamic命令というものが存在しており
これはbootstrapメソッドと呼ばれるバイトコード内の情報を元に動的に呼び出し先を変更することができる機能である。
javapしてみると確認できる。
BootstrapMethodsという属性が存在しており
それに対して呼び出すメソッドとメソッドに渡される引数がバイトコード上に記述されている。

$ > javap -v LambdaTest.class  
...省略  
BootstrapMethods:  
  0: #77 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Metho  
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;  
    Method arguments:  
      #78 (Ljava/lang/Object;)V  
      #79 invokestatic com/github/wreulicke/lambda/LambdaTest.privateMethod:(Ljava/lang/String;)V  
      #80 (Ljava/lang/String;)V  
...省略  

LambdaMetafactory.metafactoryメソッドが呼び出される
第一引数のMethodHandles$LookupオブジェクトはJVMによって動的に解決される?。(詳しくは知らない。)

この時metafactoryメソッドによって生成されたCallSiteをJVMが実行して
ラムダ式で実装すべき型を実装したインスタンスが返却される。
今回のコードではConsumerである。

オマケ:ラムダ式を記述したクラスにInnerClassとしてLookupオブジェクトが定義される。

タイトル通りではあるが、ラムダ式を記述した際、JavaバイトコードにはInnerClasses属性が追記されている。アクセス権限を付与するためであろうか?

$ > javap -v LambdaTest.class  
...省略  
InnerClasses:  
     public static final #88= #84 of #86; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles  
...省略  

ラムダ式のクラスがロードされるまでのおおまかな流れ

大まかな流れとしては以下のようになる。
1. invokedynamicでLambdaMetafactory#metafactoryメソッドが呼ばれる
2. ラムダ式のクラスが生成される
3. CallSiteオブジェクトが返却される
4. 返却されたCallSiteオブジェクトがinvokedynamicの流れで実行される
5. ラムダ式で実装されたインスタンスが生成される。

どうやってクラスが生成されているか。

これは他のブログの方が書いてはいるが、ここにまとめておく。

ラムダ式が実際にどうやってロードされるのかを見てみよう。
invokedynamicで呼び出されたbootstrapメソッド内で
InnerClassLambdaMetafactoryインスタンス化された後、
細かいところを省略すると(というか読んでない)
buildCallSiteメソッドを呼び出し、CallSiteオブジェクトを返却している。

    public static CallSite metafactory(MethodHandles.Lookup caller,  
                                       String invokedName,  
                                       MethodType invokedType,  
                                       MethodType samMethodType,  
                                       MethodHandle implMethod,  
                                       MethodType instantiatedMethodType)  
            throws LambdaConversionException {  
        AbstractValidatingLambdaMetafactory mf;  
        mf = new InnerClassLambdaMetafactory(caller, invokedType,  
                                             invokedName, samMethodType,  
                                             implMethod, instantiatedMethodType,  
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);  
        mf.validateMetafactoryArgs();  
        return mf.buildCallSite();  
    }  
  

では、buildCallSiteメソッドでは何をやっているかというと
spinInnerClassを呼ぶと、ラムダ式のクラスが生成されて返却されている。

    @Override  
    CallSite buildCallSite() throws LambdaConversionException {  
        final Class<?> innerClass = spinInnerClass();  
        ...省略  
    }  

spinInnerClassでは何をやっているかというと
JDK内部に持っているASM(バイトコード操作のライブラリ)を用いて、ラムダ式で実装するべきクラスのバイトコードを生成している

また、ラムダ式をダンプした時から気になっていた、
内部クラスを記述したときの合成メソッドを呼び出すのではなく
内部クラスでもないのにinvokestaticでprivateメソッドを直接読んでいる件に関しては
spinInnerClassの最後の行にある
このメソッドによって内部クラスになっているのであろう。

        return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);  

オマケ:ラムダ式で生成されたバイトコードをダンプするには?

ラムダ式が生成するクラスはシステムプロパティのキー「jdk.internal.lambda.dumpProxyClasses」に
ダンプ先を指定することでダンプすることが可能である。

ちなみに、このプロパティの読み取りはInnerClassLambdaMetafactoryのstatic初期化子に記述されているので
InnerClassLambdaMetafactoryクラスが読み込まれる前にダンプ先を設定しておかなければならない。

まとめ

今回はラムダ式というかメソッド参照がどんなバイトコードを生成するのかについて調べた。
楽しい。

ラムダ式が分からない?そんなあなたに朗報!!
ダンプしてバイトコードを調べよう!!Javaプログラマならわかるよね!!

雑な煽りをして今日の記事は終わりです。

MethodHandlesで遊ぶ

初めに

今回出てくるのはこの辺

MethodHandleの朝はLookupオブジェクトを作ることから始まる。

Lookup lookup = MethodHandles.lookup();

フィールドを取得してみる。

  @Test
  public void test6() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findGetter(
      /* receiver type */ PublicTestObject.class,
      /* method name */ "stringField",
      /* return type */ String.class
    );
    assertThat(handle.invoke(new PublicTestObject("test"))).isEqualTo("test");
  }

  @AllArgsConstructor
  public static class PublicTestObject {
    public String stringField;
  }

はい。Lombokのアノテーションを使ってますが 大したコードじゃありません。 フィールドを参照するGetterとして、MethodHandle(関数みたいなもの)を取得してinvokeしています。

メソッドを実行してみる。

  @Test
  public void test1() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.bind(
      /* method receiver */ new TestObject(),
      /* method name */ "get",
      /* method signature */ MethodType.methodType(/* return type */ String.class)
    );
    assertThat(handle.invoke()).isEqualTo("getting.");
  }

  // 上記とほぼ等価
  // 中の処理は少し違うが、いまいち違いが分かっていない。
  // 中でinvokeSpecialみたいな文言が飛んでいるが、findSpecialと少し動きが違う・・・?
  @Test
  public void test2() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findVirtual(
      TestObject.class,
      "get",
      MethodType.methodType(String.class)
    );
    assertThat(handle.invoke(new TestObject())).isEqualTo("getting.");
  }


  @Test
  public void test3() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findVirtual(
      HasMethodObject.class,
      "getField",
      MethodType.methodType(String.class, String.class)
    );
    // 引数の部分適用
    MethodHandle handle2 = MethodHandles.insertArguments(handle, 1, "xxxx");
    assertThat(handle2.invoke(new HasMethodObject("yyy"))).isEqualTo("yyyxxxx");

  }
  @Test
  public void test4(){
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findVirtual(
      HasMethodObject.class,
      "getField",
      MethodType.methodType(String.class, String.class)
    );
    // 引数の再配置
    MethodHandle permuted = MethodHandles.permuteArguments(
      /* 再配置するMethodHandle */ handle,
      /* 再配置後のシグネチャ */ MethodType.methodType(String.class, String.class, HasMethodObject.class),
      /* 再配置前後の順序 */ 1, 0);
    assertThat(permuted.invoke("zzzz", new HasMethodObject("yyy"))).isEqualTo("yyyzzzz");
  }

  @Test
  public void test5() throws Throwable {
    Lookup lookup = MethodHandles.lookup();

    MethodType methodType = MethodType.genericMethodType(1, true)
      .changeReturnType(String.class)
      .changeParameterType(0, String.class);
    assertThat(methodType.toMethodDescriptorString())
      .isEqualTo("(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;");
    
    MethodHandle handle = lookup.findStatic(MessageFormat.class, "format", methodType);
    // 第一引数に部分適用
    MethodHandle formatter = handle.bindTo("format strings {0} {1}");

    // 可変長引数をいい感じに呼べるようにする
    assertThat(formatter.asVarargsCollector(Object[].class)
      .invoke("1", "2")).isEqualTo("format strings 1 2");

    // asVarargsCollectorを使わなかった場合:ダサい。
    assertThat(formatter.invoke(new Object[] {
      "1",
      "2"
    })).isEqualTo("format strings 1 2");
  }


  @Test
  public void test6() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodType methodType = MethodType.genericMethodType(1, true)
      .changeReturnType(String.class)
      .changeParameterType(0, String.class);

    // 呼び出す関数
    MethodHandle formatter = lookup.findStatic(MessageFormat.class, "format", methodType)
      .bindTo("format strings {0} {1}");
    // 前処理関数
    MethodHandle trace = lookup.findVirtual(PrintStream.class, "printf", MethodType.methodType(PrintStream.class, new Class[] {
      String.class,
      Object[].class
    }))
      .bindTo(System.out)
      .asType(MethodType.methodType(void.class, String.class, Object[].class))
      .bindTo("arguments %s %s");

    // also prints "arguments xxx yyy"
    assertThat(MethodHandles.foldArguments(formatter, trace)
      .invoke(new Object[] {
        "xxx",
        "yyy"
    })).isEqualTo("format strings xxx yyy");
  }

  @AllArgsConstructor
  public static class TestObject {

    public String get() {
      return "getting.";
    }
  }

  @AllArgsConstructor
  public static class HasMethodObject {
    private final String string;

    public String getField() {
      return string;
    }

    public String getField(String xxxx) {
      return string + xxxx;
    }
  }
  • メソッドの呼び出し
  • メソッド引数の部分適用
  • 関数引数の再配置
  • 前処理関数による関数の合成?いまいち言い方が分かりません。

色々触りました。 楽しそうなのは部分適用と引数の再配置ぐらいでしょうか?

SAM Type Proxy

  @SuppressWarnings("unused")
  private static boolean test(Object x) {
    return x instanceof String;
  }

  @Test
  public void test1() throws Throwable {
    Lookup lookup = MethodHandles.lookup();
    MethodHandle handle = lookup.findStatic(ProxiesTest.class, "test", MethodType.methodType(boolean.class, Object.class));

    @SuppressWarnings("unchecked")
    Predicate<Object> predicate = MethodHandleProxies.asInterfaceInstance(Predicate.class, handle);
    assertThat(predicate.test("test")).isEqualTo(true);
    assertThat(predicate.test(1)).isEqualTo(false);

    // デフォルトメソッドは呼び出せないみたい。
    assertThatThrownBy(predicate::negate).isInstanceOf(InternalError.class);
  }

単一メソッドを持つインターフェースのプロキシが簡単に作れる模様。 デフォルトメソッドは呼び出せない。 マジか。内部で元々ある、Proxy.newInstanceが使われてるみたいです。

終わり

簡単なコードを少し試してみましたが いまいち盛り上がりに欠ける感じになりました。 元々あるリフレクションと比べるとAPIJVMよりになっているように感じます。

また、ラムダと関連を伺えるようなオブジェクトがいくつか存在しています。

今回の記事はラムダ式がどうやって実行されるか、という話を調べた際の副産物です。

可変長引数の周りでだいぶ苦戦しました。

Enjoy java.

今回この記事で書いたコードはここに置いてます。 github.com

参考に見たリンク

java.lang.invoke で遊ぼう - Java読書会合宿2012 Java7でカリー化?(部分適用でした、、、) - tomoTakaの日記