12

(This is difficult to search because results are all about "method reference")

I want to get a Method instance for a lambda expression for use with a legacy reflection-based API. The clousure should be included, so calling thatMethod.invoke(null, ...) should have the same effect as calling the lambda.

I have looked at MethodHandles.Lookup, but it only seems to be relevant for the reverse transform. But I guess the bind method may help to include the clousure?

Edit:

Say I have am lambda experssion:

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

and I have a legacy framework (SpEL) that has an API like

registerFunction(String name, Method method)

which will call the given Method with no this argument (i.e. Method assumed to be static). So I'll need to get a special Method instance that includes the lambda logic + the clousure data.

Xobotun
  • 1,121
  • 1
  • 18
  • 29
billc.cn
  • 7,187
  • 3
  • 39
  • 79
  • There is nothing like a `Method` instance for a lambda expression. Lambda's are syntactic sugar for anonymous functions. They give you an instance, but you cannot make a method out of it – Jatin Jan 21 '16 at 13:49
  • @Jatin Obviously, the synthesised functional interface impelementation is still just a normal Object with normal methods, which you can use the normal reflection to access. I am just wondering how to get the clousure wrapped as well. – billc.cn Jan 21 '16 at 13:52

3 Answers3

12

In case you don't find an elegant way, here is the ugly way (Ideone). Usual warning when reflection is involved: may break in future releases etc.

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);
  }
}
assylias
  • 321,522
  • 82
  • 660
  • 783
  • 4
    I salute you for the amazing hackery in this! – billc.cn Jan 21 '16 at 14:43
  • I looked around for possibilities in the `MethodHandle` estate and it seems the best it can do is generate instance methods, so a dead-end here due to the dynamic requirement. – billc.cn Jan 21 '16 at 17:30
  • 1
    The funniest part is the way you make `m` accessible; instead of calling `setAccessible(true)` on it, you acquire the backend field `override` to call `setAccessible(true)` on that `Field` to use it afterwards to set it to `true`. I guess, that’s what will happen, if you use `setAccessible(true)` too often… – Holger Jan 21 '16 at 17:30
  • 1
    @Holger haha - I had no idea that `override` was doing that! I just looked at the code of `invoke` in Method and changed `override` to avoid other methods being called! – assylias Jan 21 '16 at 17:53
  • The funniest part is Ideone throwing a huge core dump of the JVM – usr-local-ΕΨΗΕΛΩΝ Oct 24 '18 at 13:08
  • @usr-local-ΕΨΗΕΛΩΝ Nice one indeed :-) – assylias Oct 24 '18 at 13:14
7

Well, lambda expressions are desugared into methods during compilation and as long as they don’t capture this (don’t access non-static members), these methods will be static. The tricky part is to get to these methods as there is no inspectable connection between the functional interface instance and its target method.

To illustrate this, here the simplest case:

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;
            }
    }
}

This works smoothly as there is only one lambda expression and hence, one candidate method. The name of that method is compiler specific and may contain some serial numbers or hash codes, etc.

On kludge is to make the lambda expression serializable and inspect its serialized form:

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)));
}

This works, however, serializable lambdas come at a high price.


The simplest solution would be to add an annotation to a parameter of the lambda expression to be found when iterating over the methods, however, currently, javac doesn’t store the annotation properly, see also this question about this topic.


But you may also consider just creating ordinary static methods holding the code instead of a lambda expression. Getting a Method object for a method is straight-forward and you still can create a functional interface instance out of them using method references…

Community
  • 1
  • 1
Holger
  • 285,553
  • 42
  • 434
  • 765
0

Since the question mentions SpEL specifically (and I found the question when also working with SpEL), an alternative way to add a custom function to the evaluation context without using Method references is to add a custom MethodResolver (javadoc, GitHub) to the StandardEvaluationContext. A benefit of this approach is that one can add both static and non-static methods to the evaluation context using it, where only static methods could be added using the registerFunction approach.

The code to add a custom MethodResolver to the StandardEvaluationContext is fairly straightforward. Below is an executable example showing how to do so:

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);
}

The value of the expression that was output to the console by executing the above code was:

Hello, World!

Note that when using a MethodResolver to add a function to the evaluation conext, the function should not be prefixed with a # in the expression string. This is a major difference between using the MethodResolver and using the registerFunction method to add a function to the evaluation context.

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

Keep this in mind if you are considering migrating an existing solution from using the registerFunction approach to using the MethodResolver approach.

sawprogramming
  • 737
  • 1
  • 9
  • 15