如何使用Spring Expression Language?
1. 介绍
Spring Expression Lanuage(下文简称SpEL)是Spring提供的一种能够在运行时操作对象(读和写)的表达式语言。如下示例:
// 返回 1 + 2 的值
1 + 2
// 返回 user 变量的 name 属性
#user.name
// 返回一个字符串
'Hello, World!'
The Spring Expression Language (“SpEL” for short) is a powerful expression language that supports querying and manipulating an object graph at runtime[1].
2. 表达式评估
SpEL表达式在被评估后,会返回评估结果。
- 创建表达式解析器
ExpressionParser
。 - 通过表达式解析器解析表达式文本,以得到表达式
Expression
。 - 评估表达式得到评估结果。
示例如下:
ExpressionParser expressionParser = new SpelExpressionParser();
Expression expression = expressionParser.parseExpression("1 + 2");
Integer result = expression.getValue(expression, Integer.class); // 3
SpEL表达式评估时支持设置根对象(rootObject
)和评估上下文(EvaluationContext
),评估上下文可以设置变量。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("name", "foo");
log.info("{}", parser.parseExpression("#root + ', ' + #name").getValue(context, "Hi")); // "Hi, foo"
3. SpEL在BeanDefinition中的应用
SpEL表达式可以被用来配置Bean的元数据,语法为#{ <expression string> }
。示例如下:
public class FieldValueTestBean {
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
}
SpEl表达式也可以在XML中使用。示例如下:
<bean id="testBean" class="FieldValueTestBean">
<property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>
</bean>
4. SpEl语言特性
更多示例见SpEL官方文档[2]。
4.1. 字面量表达式
SpEL表达式支持以下几种字面量表达式(Literal Expressions):
- 字符串(如
'foo'
)。 - 数字(如
-3.14
)。 - 布尔值(
true
或false
)。 - 空值(
null
)。
示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("'Hello, World!'").getValue()); // "Hello, World!"
log.info("{}", parser.parseExpression("0x7FFFFFFF").getValue()); // 2147483647
log.info("{}", parser.parseExpression("true").getValue()); // true
log.info("{}", parser.parseExpression("null").getValue()); // null
4.2. 对象属性和集合
SpEL表达式支持访问对象属性,并支持通过索引访问各种数据结构。
- 访问对象属性
JavaBean innerBean = JavaBean.builder().name("inner").build();
JavaBean bean = JavaBean.builder().name("foo").child(innerBean).build();
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("name").getValue(bean)); // "foo"
log.info("{}", parser.parseExpression("child.name").getValue(bean)); // "inner"
log.info("{}", parser.parseExpression("child['name']").getValue(bean)); // "inner"
- 通过索引访问列表中的元素
JavaBean bean = JavaBean.builder().ids(new ArrayList<>(Arrays.asList(1, 2, 3))).build();
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("ids[1]").getValue(bean)); // 2
- 通过索引访问映射中的元素
Map<String, String> map = new HashMap<>();
map.put("k1", "v1");
JavaBean bean = JavaBean.builder().map(map).build();
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("map['k1']").getValue(bean)); // "v1"
- 通过索引访问字符串中的字符
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("'Hello, World!'[1]").getValue()); // "e"
4.3. 内联列表
SpEL表达式支持内联列表(Inline Lists),可以使用{}
声明。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("{1, 2, 3}").getValue()); // [1, 2, 3]
log.info("{}", parser.parseExpression("{ {'a', 'b'}, {'c', 'd'} }[1][0]").getValue()); // "c"
log.info("{}", parser.parseExpression("{}").getValue()); // []
4.4. 内联映射
SpEL表达式支持内联映射(Inline Maps),可以使用{:}
声明。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("{a: 'foo', b: 'bar'}").getValue()); // {"a": "foo", "b": "bar"}
log.info("{}", parser.parseExpression("{a: 'foo', b: 'bar'}['a']").getValue()); // "foo"
log.info("{}", parser.parseExpression("{:}").getValue()); // {}
4.5. 方法
SpEL表达式支持通过Java语法的方式调用方法(Methods)。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("'foo'.toUpperCase()").getValue()); // "FOO"
log.info("{}", parser.parseExpression("'foo'.substring(1, 3)").getValue()); // "oo"
4.6. 操作符
4.6.1. 关系操作符
SpEL表达式支持以下几种关系操作符(Relational Operators):
- 等于(
==
或eq
) - 不等于(
!=
或ne
) - 小于(
<
或lt
) - 小于等于(
<=
或le
) - 大于(
>
或gt
) - 大于等于(
>=
或ge
)
关系运算符适用于Number
类型和实现了Comparable
接口的类型。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("2 >= 1").getValue()); // true
log.info("{}", parser.parseExpression("1 gt 2").getValue()); // false
除了以上常规的关系运算符外,SpEl还支持between
,instanceof
,matchers
三种关系运算符。示例如下:
log.info("{}", parser.parseExpression("9 between {1, 10}").getValue()); // true
log.info("{}", parser.parseExpression("123 instanceof T(Integer)").getValue()); // true
log.info("{}", parser.parseExpression("'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue()); // true
对于
between
操作符,<input> between {<range_begin>, <range_end>}
等价于<input> >= <range_begin> && <input> <= <range_end>}
。
4.6.2. 逻辑操作符
SpEL表达式支持以下几种逻辑操作符(Logical Operators):
- 且(
and
或&&
) - 或(
or
或||
) - 非(
not
或!
)
以上文本操作符均不区分大小写。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("true and false").getValue()); // false
log.info("{}", parser.parseExpression("true AnD false").getValue()); // false
log.info("{}", parser.parseExpression("true || false").getValue()); // true
log.info("{}", parser.parseExpression("!true").getValue()); // false
4.6.3. 字符串操作符
SpEL表达式支持以下几种字符串操作符(String Operators):
- 字符串拼接(
+
)。 - 字符减(
-
)。 - 字符串重复(
*
)。
示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("'a' + 'b'").getValue()); // "ab"
log.info("{}", parser.parseExpression("'a' - 3").getValue()); // "^"
log.info("{}", parser.parseExpression("'a' * 3").getValue()); // "aaa"
4.6.4. 数学操作符
SpEL表达式支持以下几种数学操作符(Mathematical Operators):
- 加(
+
)。 - 减(
-
)。 - 乘(
*
)。 - 除(
/
或div
)。 - 递增(
++
)。 - 递减(
--
)。 - 取余(
%
或mod
)。 - 指数幂(
^
)。
以上文本操作符均不区分大小写。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("1 + 2").getValue()); // 3
log.info("{}", parser.parseExpression("1 - 2").getValue()); // -1
log.info("{}", parser.parseExpression("1 * 2").getValue()); // 2
log.info("{}", parser.parseExpression("1 / 2").getValue()); // 0
log.info("{}", parser.parseExpression("1 div 2").getValue()); // 0
JavaBean bean = JavaBean.builder().age(1).build();
log.info("{}", parser.parseExpression("++age").getValue(bean)); // 2
log.info("{}", parser.parseExpression("--age").getValue(bean)); // 1
log.info("{}", parser.parseExpression("1 % 2").getValue()); // 1
log.info("{}", parser.parseExpression("1 mod 2").getValue()); // 1
log.info("{}", parser.parseExpression("1 MOd 2").getValue()); // 1
log.info("{}", parser.parseExpression("2 ^ 3").getValue()); // 8
4.6.5. 赋值操作符
SpEL表达式支持赋值操作符(Assignment Operator),即=
。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
JavaBean bean = JavaBean.builder().age(1).build();
log.info("{}", parser.parseExpression("age = 2").getValue(bean)); // 2
4.7. 类型
SpEL表达式支持通过T(xxx)
获取Class
的实例。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("T(String).valueOf(123)").getValue()); // "123"
log.info("{}", parser.parseExpression("T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR").getValue()); // true
对于
java.lang
包下的类,可以省略包名。
4.8. 构造器
SpEL表达式支持通过new xxx(xxx)
调用构造器(Constructors)来创建对象实例。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("new String('123')").getValue()); // "123"
对于
java.lang
包下的类,可以省略包名。
4.9. 变量
SpEL表达式支持通过#variableName
引用变量(Variables),可以通过EvaluationContext#setVariable
方法设置变量。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("foo", "bar");
JavaBean bean = JavaBean.builder().name("tom").build();
log.info("{}", parser.parseExpression("name = #foo").getValue(context, bean)); // "bar"
SpEL内置了#this
和#root
变量:
#this
:执行当前评估的对象。#root
:指向引用根上下文对象。
示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("#root").getValue("foo")); // "foo"
log.info("{}", parser.parseExpression("{1, 2, 3}.![#this - #root]").getValue(1)); // [0, 1, 2]
4.10. 函数
SpEl表达式支持通过#functionName(...)
调用用户自定义的函数(Functions),可以通过EvaluationContext#setVariable
方法注册函数。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
try {
context.setVariable("max", Math.class.getMethod("max", int.class, int.class));
} catch (NoSuchMethodException ignore) {
}
log.info("{}", parser.parseExpression("#max(1, 2)").getValue(context)); // 2
4.11. 三目操作符
SpEL表达式支持三目操作符(Ternary Operator)。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("1 > 2 ? 'yes' : 'no'").getValue()); // "no"
4.12. Elvis操作符
SpEL表达式支持Elvis操作符(Elvis Operator),为null
设置默认值。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("name", null);
log.info("{}", parser.parseExpression("#name?:'default'").getValue()); // "default"
4.13. 空值安全访问操作符
SpEl表达式支持空值安全访问操作符(Safe navigation operator),避免NullPointerException
。
- 安全地对象属性访问(
?.
)。 - 安全地集合下标访问(
?.[]
)。 - 安全地集合过滤(
?.?
,?.^
,?.$
)。 - 安全地集合映射(
?.!
)。
示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("#name?.toUppercase()").getValue()); // null
4.14. 集合过滤
SpEL表达式支持对集合进行过滤(Collection Selection),以返回新的集合或元素。
- 对集合进行过滤并返回新的集合(
.?[selectionExpression]
)。 - 对集合进行过滤并返回第一个匹配的元素(
.^[selectionExpression]
)。 - 对集合进行过滤并返回最后一个匹配的元素(
.$[selectionExpression]
)。
示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("{1, 2, 3}.?[#this >= 2]").getValue()); // [2, 3]
log.info("{}", parser.parseExpression("{1, 2, 3}.^[#this >= 2]").getValue()); // 2
log.info("{}", parser.parseExpression("{1, 2, 3}.$[#this >= 2]").getValue()); // 3
4.15. 集合映射
SpEL表达式支持对集合进行映射(Collection Projection),以返回新的集合。
- 对集合进行映射并返回新的集合(
.![projectionExpression]
)。
示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
log.info("{}", parser.parseExpression("{1, 2, 3}.![#this * 2]").getValue()); // [2, 4, 6]
4.16. 表达式模板
表达式模板(Expression Templating)允许字面量文本和评估表达式混用。每个评估表达式需要用#{}
包裹。示例如下:
SpelExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("name", "foo");
log.info("{}", parser.parseExpression("Hello, #{ #name }", new TemplateParserContext()).getValue(context)); // Hello, foo
5. 实战
5.1. 使用SpEl封装业务日志打印切面
当某个方法被调用时,我们希望打印关键的业务日志进行留痕。
5.1.1. 业务日志注解定义
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface BizLog {
/**
* Log format
*
* @return /
*/
String format();
/**
* Spring expression
*
* @return /
*/
String[] expressions() default {};
}
5.1.2. 业务日志切面实现
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
@Aspect
public class BizLogAspect {
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
private final ExpressionParser expressionParser = new SpelExpressionParser();
@Pointcut("@annotation(bizLog)")
public void pointcut(BizLog bizLog) {
}
@Before(value = "pointcut(bizLog)", argNames = "joinPoint,bizLog")
public void doBizLog(JoinPoint joinPoint, BizLog bizLog) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] parameterNames = parameterNameDiscoverer.getParameterNames(signature.getMethod());
Object[] args = joinPoint.getArgs();
Map<String, Object> parameterNameToValueMap = buildParameterNameToValueMap(parameterNames, args);
Object[] arguments = buildArguments(parameterNameToValueMap, bizLog.expressions());
log.info(bizLog.format(), arguments);
}
private Object[] buildArguments(Map<String, Object> parameterNameToValueMap, String[] args) {
Object[] arguments = new String[args.length];
for (int i = 0; i < args.length; i++) {
EvaluationContext evaluationContext = new StandardEvaluationContext();
initEvaluationContext(evaluationContext, parameterNameToValueMap);
Object argument = expressionParser.parseExpression(args[i]).getValue(evaluationContext);
arguments[i] = argument;
}
return arguments;
}
private void initEvaluationContext(EvaluationContext evaluationContext, Map<String, Object> parameterNameToValueMap) {
for (Map.Entry<String, Object> entry : parameterNameToValueMap.entrySet()) {
evaluationContext.setVariable(entry.getKey(), entry.getValue());
}
try {
evaluationContext.setVariable("right", this.getClass().getMethod("right", Object.class, int.class));
} catch (NoSuchMethodException ignore) {
}
}
public static String right(Object source, int size) {
if (source == null) {
return "";
}
return StringUtils.right(source.toString(), size);
}
private static Map<String, Object> buildParameterNameToValueMap(String[] parameterNames, Object[] args) {
if (parameterNames == null) {
return new HashMap<>();
}
Map<String, Object> parameterNameToValueMap = new HashMap<>(parameterNames.length);
for (int i = 0; i < parameterNames.length; i++) {
parameterNameToValueMap.put(parameterNames[i], args[i]);
}
return parameterNameToValueMap;
}
}
5.1.3. 业务日志注解使用示例
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Slf4j
@Controller
@RequestMapping("/test")
public class TestController {
@BizLog(format = "Register with username={}, phone(last 4 numbers)={}", expressions = {"#username", "#right(#phone, 4)"})
@GetMapping("/register")
public void register(@RequestParam String username, @RequestParam String phone) {
// do something
}
}
GET http://localhost:8080/test/register?username=foo&phone=17812345678
Register with username=foo, phone(last 4 numbers)=5678