Java 8 ラムダ式の実装メソッド名を取得 - SerializedLambda

Java 8 ラムダ式の実装メソッド名を実行時に取得する方法を探ってみました。

ソースは http://github.com/fits/try_samples/tree/master/blog/20140413/

はじめに

前回ラムダ式デコンパイルしてみましたが、ラムダ式の処理は lambda$main$0 のような synthetic メソッドに実装されます。

そして、ラムダ式の部分 (下記 f 変数の値) は実行時に java.lang.invoke.LambdaMetafactory.metafactory() で動的にクラス・オブジェクトが生成されます。

Sample0.java
import java.util.function.Predicate;

class Sample0 {
    public static void main(String... args) {
        int a = 3;
        Predicate<Integer> f = (x) -> x % a == 0;
        System.out.println(f.test(6));
    }
}
Sample0 の javap 結果 (lambda$main$0 がラムダ式の実装)
> javap -p Sample0
Compiled from "Sample0.java"
class Sample0 {
  Sample0();
  public static void main(java.lang.String...);
  private static boolean lambda$main$0(int, java.lang.Integer);
}

そこで、実行時に生成されたラムダ式のオブジェクトから実装メソッド名などの情報を取得する事ができるのか調査してみました。

結論としては、通常のラムダ式では無理そうでしたが、シリアライズ可にする事で取得できると分かりました。

  • ラムダ式を Serializable にすると writeReplace メソッドからラムダ式の情報が入った java.lang.invoke.SerializedLambda を取得可能

ただし、writeReplace は private final なメソッドです。(シリアライズのための処理なので)

ちなみに、ラムダ式部分の動的なクラス生成などは java.lang.invoke.InnerClassLambdaMetafactory 内で ASM を使って実施されているようです。

シリアライズ可能なラムダ式から SerializedLambda を取得

それでは、シリアライズ可能なラムダ用のインターフェース (下記の SPredicate) を定義し、writeReplace メソッドを実行して SerializedLambda を取得してみます。

Sample1.java
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Method;
import java.util.function.Predicate;
import java.io.Serializable;

class Sample1 {
    public static void main(String... args) throws Exception {
        int a = 3;

        SPredicate<Integer> f = (x) -> x % a == 0;

        Method m = f.getClass().getDeclaredMethod("writeReplace");
        m.setAccessible(true);

        // リフレクションで writeReplace を実行し SerializedLambda を取得
        SerializedLambda sl = (SerializedLambda)m.invoke(f);

        System.out.println(sl);

        System.out.println("-----");

        // 実装先クラス名の出力
        System.out.println(sl.getImplClass());
        // 実装先メソッド名の出力
        System.out.println(sl.getImplMethodName());

        // 設定されている引数の出力
        for (int i = 0; i < sl.getCapturedArgCount(); i++) {
            System.out.println("arg: " + sl.getCapturedArg(i));
        }
    }

    // シリアライズ可能にした Predicate の定義
    public interface SPredicate<T> extends Predicate<T>, Serializable {
    }
}

実行してみると、実装メソッド名や設定されている引数(上記 a 変数に該当する 3 の値)を取得できました。

実行結果
> java Sample1
SerializedLambda[capturingClass=class Sample1, 
functionalInterfaceMethod=Sample1$SPredicate.test:(Ljava/lang/Object;)Z, 
implementation=invokeStatic Sample1.lambda$main$50fc8a8$1:(ILjava/lang/Integer;)Z, 
instantiatedMethodType=(Ljava/lang/Integer;)Z, numCaptured=1]
-----
Sample1
lambda$main$50fc8a8$1
arg: 3

CFR でラムダ式の実装メソッドをデコンパイル

SerializedLambda と前回の CFR を使えば、実行時にラムダ式の実装メソッドをデコンパイルしたりする事も可能です。

Sample2.java
import java.lang.invoke.SerializedLambda;
import java.util.function.Predicate;
import java.io.Serializable;

import org.benf.cfr.reader.util.getopt.GetOptParser;
import org.benf.cfr.reader.util.getopt.Options;
import org.benf.cfr.reader.util.getopt.OptionsImpl;
import org.benf.cfr.reader.entities.ClassFile;
import org.benf.cfr.reader.entities.Method;
import org.benf.cfr.reader.state.DCCommonState;
import org.benf.cfr.reader.util.output.ToStringDumper;

class Sample2 {
    public static void main(String... args) throws Exception {
        int a = 3;

        SPredicate<Integer> f = (x) -> x % a == 0;

        java.lang.reflect.Method m = f.getClass().getDeclaredMethod("writeReplace");
        m.setAccessible(true);

        SerializedLambda sl = (SerializedLambda)m.invoke(f);

        String src = decompileLambda(sl);

        System.out.println(src);
    }

    // ラムダ式の実装メソッドをデコンパイル
    private static String decompileLambda(SerializedLambda sl) throws Exception {
        ToStringDumper d = new ToStringDumper();

        Options options = new GetOptParser().parse(new String[] {sl.getImplClass()}, OptionsImpl.getFactory());
        DCCommonState dcCommonState = new DCCommonState(options);

        ClassFile c = dcCommonState.getClassFileMaybePath(options.getFileName());
        c = dcCommonState.getClassFile(c.getClassType());

        for (Method m : c.getMethodByName(sl.getImplMethodName())) {
            m.dump(d, true);
        }

        return d.toString();
    }

    public interface SPredicate<T> extends Predicate<T>, Serializable {
    }
}

実行結果は下記の通り、ラムダ式の実装メソッド(lambda$main$50fc8a8$1)のソースが出力されます。

実行結果
> java -cp .;cfr_0_78.jar Sample2
private static /* synthetic */ boolean lambda$main$50fc8a8$1(int n, java.lang.Integer n2) {
    return n2 % n == 0;
}

上記を工夫すれば groovy.sql.DataSet のような O-R マッピング処理(クロージャSQL の where 部分を定義)をラムダ式で実現できると思います。