函数 (Functions)
你可以通过注册用户定义的函数来扩展 SpEL。这些函数可以在表达式中通过 #functionName(…) 语法调用。与标准的方法调用一样,函数调用也支持可变参数 (Varargs)。
函数可以通过 EvaluationContext 实现类中的 setVariable() 方法注册为“变量”。
提示
StandardEvaluationContext 还定义了 registerFunction(…) 方法,这提供了将函数注册为 java.lang.reflect.Method 或 java.lang.invoke.MethodHandle 的便捷方式。
警告
由于函数与评估上下文中的变量共享公共命名空间,因此必须小心确保函数名和变量名不重叠。
基于反射的方法注册
以下示例展示了如何注册一个通过反射调用的用户定义函数(使用 java.lang.reflect.Method):
假设有以下字符串反转的工具方法:
public abstract class StringUtils {
public static String reverseString(String input) {
return new StringBuilder(input).reverse().toString();
}
}fun reverseString(input: String): String {
return StringBuilder(input).reverse().toString()
}你可以像这样注册并使用该方法:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// 注册函数
context.setVariable("reverseString",
StringUtils.class.getMethod("reverseString", String.class));
// 评估结果为 "olleh"
String helloWorldReversed = parser.parseExpression(
"#reverseString('hello')").getValue(context, String.class);val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
// 注册函数
context.setVariable("reverseString", ::reverseString.javaMethod)
// 评估结果为 "olleh"
val helloWorldReversed = parser.parseExpression(
"#reverseString('hello')").getValue(context, String::class.java)基于 MethodHandle 的函数注册
函数也可以注册为 java.lang.invoke.MethodHandle。如果 MethodHandle 的目标和参数在注册前已完全绑定,这可能会提供更高效的使用场景;不过,部分绑定的句柄也受支持。
考虑 String#formatted(Object…) 实例方法,它根据模板和可变数量的参数生成消息。你可以将其注册为 MethodHandle:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
MethodType.methodType(String.class, Object[].class));
context.setVariable("message", mh);
// 评估结果为 "Simple message: <Hello World>"
String message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')")
.getValue(context, String.class);val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted",
MethodType.methodType(String::class.java, Array<Any>::class.java))
context.setVariable("message", mh)
// 评估结果为 "Simple message: <Hello World>"
val message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')")
.getValue(context, String::class.java)如前所述,也支持绑定 MethodHandle 后再进行注册。如果目标和所有参数都已绑定,性能可能会更好。在这种情况下,SpEL 表达式中不需要提供参数:
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
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);
context.setVariable("message", mh);
// 评估结果为 "This is a prerecorded message with 3 words: <Oh Hello World!>"
String message = parser.parseExpression("#message()")
.getValue(context, String.class);val parser = SpelExpressionParser()
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
val template = "This is a %s message with %s words: <%s>"
val varargs = arrayOf("prerecorded", 3, "Oh Hello World!", "ignored")
val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted",
MethodType.methodType(String::class.java, Array<Any>::class.java))
.bindTo(template)
.bindTo(varargs)
context.setVariable("message", mh)
// 评估结果为 "This is a prerecorded message with 3 words: <Oh Hello World!>"
val message = parser.parseExpression("#message()")
.getValue(context, String::class.java)补充教学
1. setVariable 与 registerFunction 的区别
你会发现文档中交替使用了这两个概念。其实在底层,StandardEvaluationContext 的 registerFunction 只是 setVariable 的一个便捷分装。
- 实质:SpEL 在查找
#name(...)时,会去查看名为name的变量是否是一个可调用的方法对象(Method或MethodHandle)。 - 灵活性:这也意味着你可以在运行时动态地更换某个函数名对应的具体实现。
2. 为什么引入 MethodHandle?
在 Java 7 之后引入的 java.lang.invoke 包提供了比传统反射更底层的访问能力。
- 性能:
MethodHandle在执行时通常比java.lang.reflect.Method具有更小的开销,尤其是在 JIT 编译器能够深度优化的情况下。 - 安全性:
MethodHandle的权限是在查找(Lookup)时检查的,而不是每次调用时检查。 - SpEL 的新趋势:Spring 6+ 对
MethodHandle的支持增强,体现了框架向高性能、现代化 Java 特性靠拢的方向。
3. 实战场景:权限与动态字典
自定义函数是 SpEL 最强大的扩展点,常用于:
- 业务权限控制:注册一个
#hasPermission('EDIT', #target)函数。 - 动态枚举/字典值转换:注册一个
#dict('user_status', '1')自动返回“已激活”。 - 复杂数学运算:注册金融计算相关的静态私有方法供表达式使用。
4. 命名冲突:一个常见的“坑”
再次强调,由于函数和变量共用一个 Namespace,开发者经常会犯如下错误:
context.setVariable("user", userObject);
context.setVariable("user", checkUserMethod); // 这里覆盖了 userObject!建议:在项目中统一规范:
- 变量名:名词,首字母小写(如
#userInfo)。 - 函数名:动词标识符,或带下沉前缀(如
#isAdult或#fn_reverse)。