【问题标题】:Java 8: convert lambda to a Method instance with closure includedJava 8:将 lambda 转换为包含闭包的 Method 实例
【发布时间】:2016-01-21 13:46:00
【问题描述】:

(这很难搜索,因为结果都是关于“方法引用”)

我想为 lambda 表达式获取一个 Method 实例,以便与旧的基于反射的 API 一起使用。应该包含 clousure,因此调用 thatMethod.invoke(null, ...) 应该与调用 lambda 具有相同的效果。

我看过MethodHandles.Lookup,但它似乎只与逆变换有关。但我猜bind 方法可能有助于包含闭包?

编辑:

说我有 lambda 表达式:

Function<String, String> sayHello = name -> "Hello, " + name;

我有一个旧框架 (SpEL),它有一个类似的 API

registerFunction(String name, Method method)

它将调用给定的Method,没有this 参数(即假定方法是静态的)。所以我需要得到一个特殊的 Method 实例,其中包括 lambda 逻辑 + clousure 数据。

【问题讨论】:

  • 对于 lambda 表达式,没有什么比 Method 实例更好的了。 Lambda 是匿名函数的语法糖。他们给你一个实例,但你不能用它做一个方法
  • @Jatin 很明显,合成的函数接口实现仍然只是一个普通的Object,有普通的方法,可以使用普通的反射来访问。我只是想知道如何将clousure 也包裹起来。

标签: reflection lambda java-8


【解决方案1】:

如果你找不到优雅的方式,这里是丑陋的方式 (Ideone)。涉及反射时的常见警告:可能会在未来的版本中中断等。

public static void main(String[] args) throws Exception {
  Function<String, String> sayHello = name -> "Hello, " + name;
  Method m = getMethodFromLambda(sayHello);
  registerFunction("World", m);
}

static void registerFunction(String name, Method method) throws Exception {
  String result = (String) method.invoke(null, name);
  System.out.println("result = " + result);
}

private static Method getMethodFromLambda(Function<String, String> lambda) throws Exception {
  Constructor<?> c = Method.class.getDeclaredConstructors()[0];
  c.setAccessible(true);
  Method m = (Method) c.newInstance(null, null, null, null, null, 0, 0, null, null, null, null);
  m.setAccessible(true); //sets override field to true

  //m.methodAccessor = new LambdaAccessor(...)
  Field ma = Method.class.getDeclaredField("methodAccessor");
  ma.setAccessible(true);
  ma.set(m, new LambdaAccessor(array -> lambda.apply((String) array[0])));

  return m;
}

static class LambdaAccessor implements MethodAccessor {
  private final Function<Object[], Object> lambda;
  public LambdaAccessor(Function<Object[], Object> lambda) {
    this.lambda = lambda;
  }

  @Override public Object invoke(Object o, Object[] os) {
    return lambda.apply(os);
  }
}

【讨论】:

  • 我向你致敬!
  • 我在MethodHandle 资产中四处寻找可能性,似乎它可以做的最好的事情就是生成实例方法,所以由于动态需求,这里是死胡同。
  • 最有趣的部分是您使m 可访问的方式;而不是在其上调用setAccessible(true),您获取后端字段override 以在该Field 上调用setAccessible(true),然后使用它来将其设置为true。我猜,如果你经常使用setAccessible(true),就会发生这种情况……
  • @Holger haha​​ - 我不知道override 正在这样做!刚才看了Method里面invoke的代码,改了override,避免调用其他方法!
  • 最有趣的部分是 Ideone 抛出了一个巨大的 JVM 核心转储
【解决方案2】:

嗯,lambda 表达式在编译过程中被脱糖到方法中,只要它们不捕获this(不要访问非static 成员),这些方法将是static。棘手的部分是获取这些方法,因为功能接口实例与其目标方法之间没有可检查的连接。

为了说明这一点,这里是最简单的情况:

public class LambdaToMethod {
    public static void legacyCaller(Object arg, Method m) {
        System.out.println("calling Method \""+m.getName()+"\" reflectively");
        try {
            m.invoke(null, arg);
        } catch(ReflectiveOperationException ex) {
            ex.printStackTrace();
        }
    }
    public static void main(String[] args) throws URISyntaxException
    {
        Consumer<String> consumer=s -> System.out.println("lambda called with "+s);
        for(Method m: LambdaToMethod.class.getDeclaredMethods())
            if(m.isSynthetic() && m.getName().contains("lambda")) {
                legacyCaller("a string", m);
                break;
            }
    }
}

这很顺利,因为只有一个 lambda 表达式,因此只有一个候选方法。该方法的名称是特定于编译器的,可能包含一些序列号或哈希码等。

kludge 是使 lambda 表达式可序列化并检查其序列化形式:

static Method lambdaToMethod(Serializable lambda) {
    for(Class<?> cl=lambda.getClass(); cl!=null; cl=cl.getSuperclass()) try {
        Method m=cl.getDeclaredMethod("writeReplace");
        m.setAccessible(true);
        try {
            SerializedLambda sl=(SerializedLambda)m.invoke(lambda);
            return LambdaToMethod.class.getDeclaredMethod(sl.getImplMethodName(),
                MethodType.fromMethodDescriptorString(sl.getImplMethodSignature(),
                    LambdaToMethod.class.getClassLoader()).parameterArray());
        } catch(ReflectiveOperationException ex) {
            throw new RuntimeException(ex);
        }
    } catch(NoSuchMethodException ex){}
    throw new AssertionError();
}
public static void main(String[] args)
{
    legacyCaller("a string", lambdaToMethod((Consumer<String>&Serializable)
        s -> System.out.println("first lambda called with "+s)));
    legacyCaller("a string", lambdaToMethod((Consumer<String>&Serializable)
        s -> System.out.println("second lambda called with "+s)));
}

这行得通,但是,可序列化的 lambda 代价很高。


最简单的解决方案是向 lambda 表达式的参数添加注释,以便在迭代方法时找到,但是,目前,javac 没有正确存储注释,另请参阅 this question 关于此主题。


但您也可以考虑创建普通的static 方法来保存代码而不是 lambda 表达式。为方法获取Method 对象非常简单,您仍然可以使用方法引用从它们中创建功能接口实例……

【讨论】:

    【解决方案3】:

    由于问题特别提到了 SpEL(我在使用 SpEL 时也发现了这个问题),在不使用 Method 引用的情况下将自定义函数添加到评估上下文的另一种方法是添加自定义 MethodResolver (@ 987654321@, GitHub) 到StandardEvaluationContext。这种方法的一个好处是可以使用它向评估上下文中添加静态和非静态方法,而使用registerFunction 方法只能添加静态方法。

    将自定义MethodResolver 添加到StandardEvaluationContext 的代码相当简单。下面是一个可执行示例,展示了如何执行此操作:

    public static void main(String[] args) throws Exception {
        Function<String, String> sayHello = name -> "Hello, " + name;
    
        // The evaluation context must have a root object, which can be set in the StandardEvaluationContext
        // constructor or in the getValue method of the Expression class. Without a root object, the custom
        // MethodResolver will not be called to resolve the function.
        Object                    rootObject                = new Object();
        StandardEvaluationContext standardEvaluationContext = new StandardEvaluationContext(rootObject);
    
        // Add the custom MethodResolver to the evaluation context that will return a MethodExecutor that
        // Spring can use to execute the sayHello function when an expression contains "sayHello('<any string>')".
        standardEvaluationContext.addMethodResolver((context, targetObject, methodName, argumentTypes) -> {
            MethodExecutor methodExecutor = null;
    
            if (methodName.equals("sayHello")
                && argumentTypes.size() == 1
                && String.class.isAssignableFrom(argumentTypes.get(0).getObjectType())
            ) {
                methodExecutor = (innerContext, target, arguments) -> {
                    final String name = arguments[0].toString();
                    return new TypedValue(sayHello.apply(name));
                };
            }
    
            return methodExecutor;
        });
    
        // Create an expression parser, parser the expression, and get the evaluated value of the expression.
        SpelExpressionParser expressionParser = new SpelExpressionParser();
        Expression           expression       = expressionParser.parseExpression("sayHello('World!')");
        String               expressionValue  = expression.getValue(standardEvaluationContext, String.class);
    
        // Output the expression value, "Hello, World!", to the console.
        System.out.println(expressionValue);
    }
    

    执行上述代码输出到控制台的表达式的值为:

    Hello, World!
    

    请注意,当使用MethodResolver 将函数添加到评估上下文时,函数不应在表达式字符串中以# 为前缀。这是使用MethodResolver 和使用registerFunction 方法将函数添加到评估上下文之间的主要区别。

    sayHello('World!')  // will work!
    #sayHello('World!') // will not work!
    

    如果您正在考虑将现有解决方案从使用 registerFunction 方法迁移到使用 MethodResolver 方法,请记住这一点。

    【讨论】:

      猜你喜欢
      • 2016-09-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多