Skip to content

函数 (Functions)

你可以通过注册用户定义的函数来扩展 SpEL。这些函数可以在表达式中通过 #functionName(…​) 语法调用。与标准的方法调用一样,函数调用也支持可变参数 (Varargs)

函数可以通过 EvaluationContext 实现类中的 setVariable() 方法注册为“变量”。

提示

StandardEvaluationContext 还定义了 registerFunction(…​) 方法,这提供了将函数注册为 java.lang.reflect.Methodjava.lang.invoke.MethodHandle 的便捷方式。

警告

由于函数与评估上下文中的变量共享公共命名空间,因此必须小心确保函数名和变量名不重叠。

基于反射的方法注册

以下示例展示了如何注册一个通过反射调用的用户定义函数(使用 java.lang.reflect.Method):

假设有以下字符串反转的工具方法:

java
public abstract class StringUtils {
	public static String reverseString(String input) {
		return new StringBuilder(input).reverse().toString();
	}
}
kotlin
fun reverseString(input: String): String {
	return StringBuilder(input).reverse().toString()
}

你可以像这样注册并使用该方法:

java
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);
kotlin
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

java
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);
kotlin
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 表达式中不需要提供参数:

java
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);
kotlin
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. setVariableregisterFunction 的区别

你会发现文档中交替使用了这两个概念。其实在底层,StandardEvaluationContextregisterFunction 只是 setVariable 的一个便捷分装。

  • 实质:SpEL 在查找 #name(...) 时,会去查看名为 name 的变量是否是一个可调用的方法对象(MethodMethodHandle)。
  • 灵活性:这也意味着你可以在运行时动态地更换某个函数名对应的具体实现。

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,开发者经常会犯如下错误:

java
context.setVariable("user", userObject);
context.setVariable("user", checkUserMethod); // 这里覆盖了 userObject!

建议:在项目中统一规范:

  • 变量名:名词,首字母小写(如 #userInfo)。
  • 函数名:动词标识符,或带下沉前缀(如 #isAdult#fn_reverse)。

Based on Spring Framework.