バイトコード操作でフィールドの追加をする
バイトコード操作を使って 本記事では、フィールドを追加してみます。
対象クラスを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で遊ぶ
初めに
今回出てくるのはこの辺
- MethodHandles (Java Platform SE 8)
- MethodType (Java Platform SE 8)
- MethodHandle (Java Platform SE 8)
- MethodHandles.Lookup (Java Platform SE 8)
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が使われてるみたいです。
終わり
簡単なコードを少し試してみましたが いまいち盛り上がりに欠ける感じになりました。 元々あるリフレクションと比べるとAPIがJVMよりになっているように感じます。
また、ラムダと関連を伺えるようなオブジェクトがいくつか存在しています。
今回の記事はラムダ式がどうやって実行されるか、という話を調べた際の副産物です。
可変長引数の周りでだいぶ苦戦しました。
Enjoy java.
今回この記事で書いたコードはここに置いてます。 github.com
参考に見たリンク
java.lang.invoke で遊ぼう - Java読書会合宿2012 Java7でカリー化?(部分適用でした、、、) - tomoTakaの日記