Add SpEL support for registered MethodHandles

This commit adds support for MethodHandles in SpEL, using the same
syntax as user-defined functions (which also covers reflective Methods).

The most benefit is expected with handles that capture a static method
with no arguments, or with fully bound handles (where all the arguments
have been bound, including a target instance as first bound argument
if necessary). Partially bound MethodHandle should also be supported.

A best effort approach is taken to detect varargs as there is no API
support to determine if an argument is a vararg or an explicit array,
unlike with Method. Argument conversions are also applied. Finally,
array repacking is not always necessary with varargs so it is only
performed when the vararg is the sole argument to the invoked method.

See gh-27099
Closes gh-30045
This commit is contained in:
Simon Baslé
2023-02-27 19:58:05 +01:00
committed by Simon Baslé
parent d3c3088c6b
commit 906c54faff
9 changed files with 393 additions and 4 deletions

View File

@@ -188,6 +188,45 @@ public class ExpressionLanguageScenarioTests extends AbstractExpressionTests {
}
}
/**
* Scenario: looking up your own MethodHandles and calling them from the expression
*/
@Test
public void testScenario_RegisteringJavaMethodsAsMethodHandlesAndCallingThem() throws SecurityException, NoSuchMethodException {
try {
// Create a parser
SpelExpressionParser parser = new SpelExpressionParser();
//this.context is already populated with all relevant MethodHandle examples
Expression expr = parser.parseRaw("#message('Message with %s words: <%s>', 2, 'Hello World', 'ignored')");
Object value = expr.getValue(this.context);
assertThat(value).isEqualTo("Message with 2 words: <Hello World>");
expr = parser.parseRaw("#messageTemplate('bound', 2, 'Hello World', 'ignored')");
value = expr.getValue(this.context);
assertThat(value).isEqualTo("This is a bound message with 2 words: <Hello World>");
expr = parser.parseRaw("#messageBound()");
value = expr.getValue(this.context);
assertThat(value).isEqualTo("This is a prerecorded message with 3 words: <Oh Hello World>");
Expression staticExpr = parser.parseRaw("#messageStatic('Message with %s words: <%s>', 2, 'Hello World', 'ignored')");
Object staticValue = staticExpr.getValue(this.context);
assertThat(staticValue).isEqualTo("Message with 2 words: <Hello World>");
staticExpr = parser.parseRaw("#messageStaticTemplate('bound', 2, 'Hello World', 'ignored')");
staticValue = staticExpr.getValue(this.context);
assertThat(staticValue).isEqualTo("This is a bound message with 2 words: <Hello World>");
staticExpr = parser.parseRaw("#messageStaticBound()");
staticValue = staticExpr.getValue(this.context);
assertThat(staticValue).isEqualTo("This is a prerecorded message with 3 words: <Oh Hello World>");
}
catch (EvaluationException | ParseException ex) {
throw new AssertionError(ex.getMessage(), ex);
}
}
/**
* Scenario: add a property resolver that will get called in the resolver chain, this one only supports reading.
*/

View File

@@ -16,6 +16,9 @@
package org.springframework.expression.spel;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
@@ -415,6 +418,38 @@ class SpelDocumentationTests extends AbstractExpressionTests {
assertThat(helloWorldReversed).isEqualTo("dlrow olleh");
}
@Test
void methodHandlesNotBound() throws Throwable {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
MethodType.methodType(String.class, Object[].class));
context.setVariable("message", mh);
String message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')")
.getValue(context, String.class);
assertThat(message).isEqualTo("Simple message: <Hello World>");
}
@Test
void methodHandlesFullyBound() throws Throwable {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
String template = "This is a %s message with %s words: <%s>";
Object varargs = new Object[] { "prerecorded", 3, "Oh Hello World!", "ignored" };
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
MethodType.methodType(String.class, Object[].class))
.bindTo(template)
.bindTo(varargs); //here we have to provide arguments in a single array binding
context.setVariable("message", mh);
String message = parser.parseExpression("#message()")
.getValue(context, String.class);
assertThat(message).isEqualTo("This is a prerecorded message with 3 words: <Oh Hello World!>");
}
// 7.5.10
@Test

View File

@@ -16,6 +16,9 @@
package org.springframework.expression.spel;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Arrays;
import java.util.GregorianCalendar;
@@ -37,6 +40,12 @@ class TestScenarioCreator {
setupRootContextObject(testContext);
populateVariables(testContext);
populateFunctions(testContext);
try {
populateMethodHandles(testContext);
}
catch (NoSuchMethodException | IllegalAccessException e) {
throw new RuntimeException(e);
}
return testContext;
}
@@ -62,6 +71,36 @@ class TestScenarioCreator {
}
}
/**
* Register some Java {@code MethodHandle} as well known functions that can be called from an expression.
* @param testContext the test evaluation context
*/
private static void populateMethodHandles(StandardEvaluationContext testContext) throws NoSuchMethodException, IllegalAccessException {
// #message(template, args...)
MethodHandle message = MethodHandles.lookup().findVirtual(String.class, "formatted",
MethodType.methodType(String.class, Object[].class));
testContext.registerFunction("message", message);
// #messageTemplate(args...)
MethodHandle messageWithParameters = message.bindTo("This is a %s message with %s words: <%s>");
testContext.registerFunction("messageTemplate", messageWithParameters);
// #messageTemplateBound()
MethodHandle messageBound = messageWithParameters
.bindTo(new Object[] { "prerecorded", 3, "Oh Hello World", "ignored"});
testContext.registerFunction("messageBound", messageBound);
//#messageStatic(template, args...)
MethodHandle messageStatic = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"message", MethodType.methodType(String.class, String.class, String[].class));
testContext.registerFunction("messageStatic", messageStatic);
//#messageStaticTemplate(args...)
MethodHandle messageStaticPartiallyBound = messageStatic.bindTo("This is a %s message with %s words: <%s>");
testContext.registerFunction("messageStaticTemplate", messageStaticPartiallyBound);
//#messageStaticBound()
MethodHandle messageStaticFullyBound = messageStaticPartiallyBound
.bindTo(new String[] { "prerecorded", "3", "Oh Hello World", "ignored"});
testContext.registerFunction("messageStaticBound", messageStaticFullyBound);
}
/**
* Register some variables that can be referenced from the tests
* @param testContext the test evaluation context
@@ -117,4 +156,8 @@ class TestScenarioCreator {
return String.valueOf(i) + "-" + Arrays.toString(strings);
}
public static String message(String template, String... args) {
return template.formatted((Object[]) args);
}
}