值表达式基础

值表达式结合了 Spring Expression Language (SpEL)Property Placeholder Resolution(属性占位符解析)。它们将强大的程序化表达式求值与求助于属性占位符解析从 Environment 获取值(例如配置属性)的简便性结合在一起。

表达式应由可信输入(例如注解值)定义,而不应由用户输入确定。

范围

值表达式用于跨注解的各种上下文。Spring Data 主要在两种上下文提供值表达式求值:

  • 映射模型注解:例如 @Document@Field@Value 以及 Spring Data 模块中自带映射模型(如 MongoDB、Elasticsearch、Cassandra、Neo4j)的其他相关实体读取器注解。基于提供自身映射模型的库(JPA、LDAP)构建的模块不支持在映射注解中使用值表达式。

    以下代码演示了如何在映射模型注解的上下文中使用表达式。

    示例 1. @Document 注解用法
    @Document("orders-#{tenantService.getOrderCollection()}-${tenant-config.suffix}")
    class Order {
      // …
    }
  • Repository 查询方法:主要通过 @Query 注解使用。

    以下代码演示了如何在 Repository 查询方法的上下文中使用表达式。

    示例 2. @Query 注解用法
    class OrderRepository extends Repository<Order, String> {
    
      @Query("select u from User u where u.tenant = ?${spring.application.name:unknown} and u.firstname like %?#{escape([0])}% escape ?#{escapeCharacter()}")
      List<Order> findContainingEscaped(String namePart);
    }
请查阅您所用模块的文档,以确定实际的按名称/按索引参数绑定语法。通常,表达式以 :#{…}/:${…}?#{…}/?${…} 作为前缀。

表达式语法

值表达式可以仅由一个 SpEL 表达式、一个 Property Placeholder(属性占位符)或一个混合了各种表达式(包括字面值)的复合表达式定义。

示例 3. 表达式示例
#{tenantService.getOrderCollection()}                          (1)
#{(1+1) + '-hello-world'}                                      (2)
${tenant-config.suffix}                                        (3)
orders-${tenant-config.suffix}                                 (4)
#{tenantService.getOrderCollection()}-${tenant-config.suffix}  (5)
1 使用单个 SpEL 表达式的值表达式。
2 使用静态 SpEL 表达式求值为 2-hello-world 的值表达式。
3 使用单个 Property Placeholder(属性占位符)的值表达式。
4 由字面值 orders- 和 Property Placeholder ${tenant-config.suffix} 组成的复合表达式。
5 使用 SpEL、Property Placeholders(属性占位符)和字面值的复合表达式。
使用值表达式为您的代码带来了很大的灵活性。这样做需要在每次使用时对表达式进行求值,因此,值表达式求值会对性能产生影响。

Spring Expression Language (SpEL)Property Placeholder Resolution(属性占位符解析)详细解释了 SpEL 和 Property Placeholders 的语法及功能。

解析与求值

值表达式由 ValueExpressionParser API 解析。ValueExpression 的实例是线程安全的,可以缓存供以后使用,以避免重复解析。

以下示例展示了值表达式 API 的用法

解析与求值
  • Java

  • Kotlin

ValueParserConfiguration configuration = SpelExpressionParser::new;
ValueEvaluationContext context = ValueEvaluationContext.of(environment, evaluationContext);

ValueExpressionParser parser = ValueExpressionParser.create(configuration);
ValueExpression expression = parser.parse("Hello, World");
Object result = expression.evaluate(context);
val configuration = ValueParserConfiguration { SpelExpressionParser() }
val context = ValueEvaluationContext.of(environment, evaluationContext)

val parser = ValueExpressionParser.create(configuration)
val expression: ValueExpression = parser.parse("Hello, World")
val result: Any = expression.evaluate(context)

SpEL 表达式

SpEL 表达式遵循模板风格,表达式应包含在 #{…} 格式中。表达式使用由 EvaluationContextProvider 提供的 EvaluationContext 进行求值。上下文本身是一个功能强大的 StandardEvaluationContext,允许进行广泛的操作、访问静态类型和上下文扩展。

请确保仅解析和求值来自可信源(如注解)的表达式。接受用户提供的表达式可能会为利用应用程序上下文和您的系统创建入口,从而导致潜在的安全漏洞。

扩展求值上下文

EvaluationContextProvider 及其响应式变体 ReactiveEvaluationContextProvider 提供对 EvaluationContext 的访问。ExtensionAwareEvaluationContextProvider 及其响应式变体 ReactiveExtensionAwareEvaluationContextProvider 是默认实现,它们从应用程序上下文(特别是 ListableBeanFactory)确定上下文扩展。

扩展实现了 EvaluationContextExtensionReactiveEvaluationContextExtension 来为 EvaluationContext 提供扩展支持。这些扩展包括根对象、属性和函数(顶级方法)。

以下示例展示了一个提供根对象、属性、函数和别名函数的上下文扩展。

实现 EvaluationContextExtension
  • Java

  • Kotlin

@Component
public class MyExtension implements EvaluationContextExtension {

    @Override
    public String getExtensionId() {
        return "my-extension";
    }

    @Override
    public Object getRootObject() {
        return new CustomExtensionRootObject();
    }

    @Override
    public Map<String, Object> getProperties() {

        Map<String, Object> properties = new HashMap<>();

        properties.put("key", "Hello");

        return properties;
    }

    @Override
    public Map<String, Function> getFunctions() {

        Map<String, Function> functions = new HashMap<>();

        try {
            functions.put("aliasedMethod", new Function(getClass().getMethod("extensionMethod")));
            return functions;
        } catch (Exception o_O) {
            throw new RuntimeException(o_O);
        }
    }

    public static String extensionMethod() {
        return "Hello World";
    }

    public static int add(int i1, int i2) {
        return i1 + i2;
    }

}

public class CustomExtensionRootObject {

	public boolean rootObjectInstanceMethod() {
		return true;
	}

}
@Component
class MyExtension : EvaluationContextExtension {

    override fun getExtensionId(): String {
        return "my-extension"
    }

    override fun getRootObject(): Any? {
        return CustomExtensionRootObject()
    }

    override fun getProperties(): Map<String, Any> {
        val properties: MutableMap<String, Any> = HashMap()

        properties["key"] = "Hello"

        return properties
    }

    override fun getFunctions(): Map<String, Function> {
        val functions: MutableMap<String, Function> = HashMap()

        try {
            functions["aliasedMethod"] = Function(javaClass.getMethod("extensionMethod"))
            return functions
        } catch (o_O: Exception) {
            throw RuntimeException(o_O)
        }
    }

    companion object {
        fun extensionMethod(): String {
            return "Hello World"
        }

        fun add(i1: Int, i2: Int): Int {
            return i1 + i2
        }
    }
}

class CustomExtensionRootObject {
	fun rootObjectInstanceMethod(): Boolean {
		return true
	}
}

一旦上述扩展注册成功,您就可以使用其导出的方法、属性和根对象来评估 SpEL 表达式

示例 4. 表达式求值示例
#{add(1, 2)}                                             (1)
#{extensionMethod()}                                     (2)
#{aliasedMethod()}                                       (3)
#{key}                                                   (4)
#{rootObjectInstanceMethod()}                            (5)
1 调用由 MyExtension 声明的方法 add,结果为 3,因为该方法将两个数字参数相加并返回总和。
2 调用由 MyExtension 声明的方法 extensionMethod,结果为 Hello World
3 调用方法 aliasedMethod。该方法被公开为函数,并重定向到由 MyExtension 声明的方法 extensionMethod,结果为 Hello World
4 求值 key 属性,结果为 Hello
5 在根对象实例 CustomExtensionRootObject 上调用方法 rootObjectInstanceMethod

您可以在 SecurityEvaluationContextExtension 找到实际的上下文扩展示例。

Property Placeholders(属性占位符)

遵循 ${…} 形式的 Property placeholders(属性占位符)引用通常由 PropertySource 通过 Environment 提供的属性。属性可用于解析系统属性、应用程序配置文件、环境配置或秘密管理系统贡献的属性源。您可以在 Spring Framework 关于 @Value 用法的文档中找到有关属性占位符的更多详细信息。