值表达式基础
值表达式是 Spring 表达式语言(SpEL)与属性占位符解析的结合。它将对编程式表达式的强大求值能力,与借助属性占位符解析从 Environment 获取配置属性等值的简便方式结合在一起。
表达式应当由受信任的输入(例如注解值)来定义,而不应由用户输入来决定。
作用范围
值表达式被用于跨注解的各种上下文。Spring Data 主要在两种上下文中提供值表达式求值:
-
映射模型注解:例如
@Document、@Field、@Value,以及那些自带映射模型、并分别提供 Entity Reader 的 Spring Data 模块中的其他注解,如 MongoDB、Elasticsearch、Cassandra、Neo4j。那些构建于自带映射模型的库(JPA、LDAP)之上的模块,不支持在映射注解中使用值表达式。以下代码演示了如何在映射模型注解的上下文中使用表达式。
示例 1.
@Document注解用法@Document("orders-#{tenantService.getOrderCollection()}-${tenant-config.suffix}")
class Order {
// …
} -
仓库查询方法:主要通过
@Query。以下代码演示了如何在仓库查询方法的上下文中使用表达式。
示例 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 表达式、一个属性占位符,或混合了各种表达式(包括字面量)的复合表达式来定义。
示例 3. 表达式示例
#{tenantService.getOrderCollection()} // <1>
#{(1+1) + '-hello-world'} // <2>
${tenant-config.suffix} // <3>
orders-${tenant-config.suffix} // <4>
#{tenantService.getOrderCollection()}-${tenant-config.suffix} // <5>
使用单个 SpEL 表达式的值表达式。
使用静态 SpEL 表达式的值表达式,其求值结果为
2-hello-world。使用单个属性占位符的值表达式。
由字面量
orders-与属性占位符${tenant-config.suffix}组成的复合表达式。同时使用 SpEL、属性占位符和字面量的复合表达式。
使用值表达式会为您的代码带来很大的灵活性。但这样做需要在每次使用时对表达式求值,因此值表达式求值会对性能表现产生影响。
Spring 表达式语言(SpEL)与属性占位符解析详细讲解了 SpEL 与属性占位符的语法和能力。
解析与求值
值表达式由 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)中确定上下文扩展。
扩展实现 EvaluationContextExtension 或 ReactiveEvaluationContextExtension,以向 EvaluationContext 注入:根对象、命名属性以及函数(顶层方法)。
以下示例展示了一个上下文扩展,它提供了一个根对象、若干属性、若干函数以及一个别名函数。
- 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>
调用由
MyExtension声明的add方法,结果为3,因为该方法将两个数值参数相加并返回其和。调用由
MyExtension声明的extensionMethod方法,结果为Hello World。调用
aliasedMethod方法。该方法以函数形式暴露,并转发到由MyExtension声明的extensionMethod方法,结果为Hello World。对
key属性求值,结果为Hello。在根对象实例
CustomExtensionRootObject上调用rootObjectInstanceMethod方法。
您可以在 SecurityEvaluationContextExtension 中找到一个真实的上下文扩展示例。
属性占位符
形如 ${…} 的属性占位符引用的属性,通常由 PropertySource 通过 Environment 提供。属性可用于针对系统属性、应用配置文件、环境配置,或由密钥管理系统提供的属性源进行解析。有关属性占位符的更多详细信息,请参阅 Spring Framework 关于 @Value 用法的文档。