1 概述
Spring表达式语言全称为“Spring Expression Language”,缩写为“SpEL”。在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与Spring功能完美整合,如能用来配置Bean定义。
SpEL是单独模块,只依赖于core模块,不依赖于其他模块,可以单独使用。
2 使用场景
- Bean 的香瓜属性的配置
- 结合 AOP 完成业务系统的日志记录
3 功能概览
SpEL支持如下表达式:
一、基本表达式: 字面量表达式、关系,逻辑与算数运算表达式、字符串连接及截取表达式、三目运算及Elivis表达式、正则表达式、括号优先级表达式;
二、类相关表达式: 类类型表达式、类实例化、instanceof表达式、变量定义及引用、赋值表达式、自定义函数、对象属性存取及安全导航表达式、对象方法调用、Bean引用;
三、集合相关表达式: 内联List、内联数组、集合,字典访问、列表,字典,数组修改、集合投影、集合选择;不支持多维内联数组初始化;不支持内联字典定义;
四、其他表达式:模板表达式。
注:SpEL表达式中的关键字是不区分大小写的。
4 基本使用
4.1 使用步骤
1)创建解析器:SpEL使用ExpressionParser接口表示解析器,提供 SpelExpressionParser默认实现;
2)解析表达式:使用 ExpressionParser 的 parseExpression 来解析相应的表达式为Expression对象。
3)构造上下文:准备比如变量定义等等表达式需要的上下文数据。
4)求值:通过Expression接口的getValue方法根据上下文获得表达式值。
示例:
public class SpelTest {
@Test
public void test1() {
// 创建解析器
ExpressionParser parser = new SpelExpressionParser();
// 解析表达式
Expression expression = parser.parseExpression("('Hello' + ' World').concat(#end)");
// 构建上下文
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("end", "!");
// 获取 表达式值
System.out.println(expression.getValue(context));
}
}
4.2 原理
一、表达式: 表达式是表达式语言的核心,所以表达式语言都是围绕表达式进行的,从我们角度来看是“干什么”;
二、解析器: 用于将字符串表达式解析为表达式对象,从我们角度来看是“谁来干”;
三、上下文: 表达式对象执行的环境,该环境可能定义变量、定义自定义函数、提供类型转换等等,从我们角度看是“在哪干”;
四、根对象及活动上下文对象: 根对象是默认的活动上下文对象,活动上下文对象表示了当前表达式操作的对象,从我们角度看是“对谁干”
4.3 基本语法
4.3.1 字面量表达式
SpEL支持的字面量包括:字符串、数字类型(int、long、float、double)、布尔类型、null类型。
4.3.2 算数运算表达式
4.3.3 关系表达式
等于(==)、不等于(!=)、大于(>)、大于等于(>=)、小于(<)、小于等于(<=),区间(between)运算。
如parser.parseExpression("1>2").getValue(boolean.class);将返回false;
而parser.parseExpression("1 between {1, 2}").getValue(boolean.class);将返回true。
between运算符右边操作数必须是列表类型,且只能包含2个元素。第一个元素为开始,第二个元素为结束,区间运算是包含边界值的,即 xxx>=list.get(0) && xxx<=list.get(1)。
SpEL同样提供了等价的“EQ” 、“NE”、 “GT”、“GE”、 “LT” 、“LE”来表示等于、不等于、大于、大于等于、小于、小于等于,不区分大小写。
4.3.4 逻辑表达式
且(and或者&&)、或(or或者||)、非(!或NOT)。
public void test4() {
ExpressionParser parser = new SpelExpressionParser();
boolean result1 = parser.parseExpression("2>1 and (!true or !false)").getValue(boolean.class);
boolean result2 = parser.parseExpression("2>1 && (!true || !false)").getValue(boolean.class);
boolean result3 = parser.parseExpression("2>1 and (NOT true or NOT false)").getValue(boolean.class);
boolean result4 = parser.parseExpression("2>1 && (NOT true || NOT false)").getValue(boolean.class);
}
4.3.5 变量定义及引用
变量定义通过EvaluationContext接口的setVariable(variableName, value)方法定义;在表达式中使用"#variableName"引用;除了引用自定义变量,SpE还允许引用根对象及当前上下文对象,使用"#root"引用根对象,使用"#this"引用当前上下文对象;
@Test
public void testVariableExpression() {
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("name", "路人甲java");
context.setVariable("lesson", "Spring系列");
//获取name变量,lesson变量
String name = parser.parseExpression("#name").getValue(context, String.class);
System.out.println(name);
String lesson = parser.parseExpression("#lesson").getValue(context, String.class);
System.out.println(lesson);
//StandardEvaluationContext构造器传入root对象,可以通过#root来访问root对象
context = new StandardEvaluationContext("我是root对象");
String rootObj = parser.parseExpression("#root").getValue(context, String.class);
System.out.println(rootObj);
//#this用来访问当前上线文中的对象
String thisObj = parser.parseExpression("#this").getValue(context, String.class);
System.out.println(thisObj);
}
输出:
路人甲java
Spring系列
我是root对象
我是root对象
4.3.6 对象属性及安全导航表达式
对象属性获取非常简单,即使用如“a.property.property”这种点缀式获取,SpEL对于属性名首字母是不区分大小写的;SpEL还引入了Groovy语言中的安全导航运算符“(对象|属性)?.属性”,用来避免“?.”前边的表达式为null时抛出空指针异常,而是返回null;修改对象属性值则可以通过赋值表达式或Expression接口的setValue方法修改。
4.3.7 Bean 引用
SpEL支持使用“@”符号来引用Bean,在引用Bean时需要使用BeanResolver接口实现来查找Bean,Spring提供BeanFactoryResolver实现。
示例:
public void test6() {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
User user = new User();
Car car = new Car();
car.setName("保时捷");
user.setCar(car);
factory.registerSingleton("user", user);
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(factory));
ExpressionParser parser = new SpelExpressionParser();
User userBean = parser.parseExpression("@user").getValue(context, User.class);
System.out.println(userBean);
System.out.println(userBean == factory.getBean("user"));
}
4.3.8 集合相关表达式
1. 内联List
从Spring3.0.4开始支持内联List,使用{表达式,……}定义内联List,如“{1,2,3}”将返回一个整型的ArrayList,而“{}”将返回空的List,对于字面量表达式列表,SpEL会使用java.util.Collections.unmodifiableList方法将列表设置为不可修改。
public void test7() {
ExpressionParser parser = new SpelExpressionParser();
//将返回不可修改的空List
List<Integer> result2 = parser.parseExpression("{}").getValue(List.class);
//对于字面量列表也将返回不可修改的List
List<Integer> result1 = parser.parseExpression("{1,2,3}").getValue(List.class);
Assert.assertEquals(new Integer(1), result1.get(0));
try {
result1.set(0, 2);
} catch (Exception e) {
e.printStackTrace();
}
//对于列表中只要有一个不是字面量表达式,将只返回原始List,
//不会进行不可修改处理
String expression3 = "{{1+2,2+4},{3,4+4}}";
List<List<Integer>> result3 = parser.parseExpression(expression3).getValue(List.class);
result3.get(0).set(0, 1);
System.out.println(result3);
//声明二维数组并初始化
int[] result4 = parser.parseExpression("new int[2]{1,2}").getValue(int[].class);
System.out.println(result4[1]);
//定义一维数组并初始化
int[] result5 = parser.parseExpression("new int[1]").getValue(int[].class);
System.out.println(result5[0]);
}
输出:
java.lang.UnsupportedOperationException
at java.util.Collections$UnmodifiableList.set(Collections.java:1311)
at com.javacode2018.spel.SpelTest.test7(SpelTest.java:315)
[[1, 6], [3, 8]]
2
0
2. 集合,字典元素访问
SpEL目前支持所有集合类型和字典类型的元素访问,使用“集合[索引]”访问集合元素,使用“map[key]”访问字典元素;
//SpEL内联List访问
int result1 = parser.parseExpression("{1,2,3}[0]").getValue(int.class);
//SpEL目前支持所有集合类型的访问
Collection<Integer> collection = new HashSet<Integer>();
collection.add(1);
collection.add(2);
EvaluationContext context2 = new StandardEvaluationContext();
context2.setVariable("collection", collection);
int result2 = parser.parseExpression("#collection[1]").getValue(context2, int.class);
//SpEL对Map字典元素访问的支持
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("a", 1);
EvaluationContext context3 = new StandardEvaluationContext();
context3.setVariable("map", map);
int result3 = parser.parseExpression("#map['a']").getValue(context3, int.class);
3. 列表、字典、数组元素修改
可以使用赋值表达式或Expression接口的setValue方法修改;
@Test
public void test8() {
ExpressionParser parser = new SpelExpressionParser();
//修改list元素值
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
EvaluationContext context1 = new StandardEvaluationContext();
context1.setVariable("collection", list);
parser.parseExpression("#collection[1]").setValue(context1, 4);
int result1 = parser.parseExpression("#collection[1]").getValue(context1, int.class);
System.out.println(result1);
//修改map元素值
Map<String, Integer> map = new HashMap<String, Integer>();
map.put("a", 1);
EvaluationContext context2 = new StandardEvaluationContext();
context2.setVariable("map", map);
parser.parseExpression("#map['a']").setValue(context2, 4);
Integer result2 = parser.parseExpression("#map['a']").getValue(context2, int.class);
System.out.println(result2);
}
输出:
4
4
5 示例 -- 操作日志
1. controller
@TenantLog(module = "custer", description = "get ...", content = "parameter: id = #{ #customer?.id }, "
+ "accountId = #{ #customer?.accountId}, orderNumber = #{ #customer.orderNumber }, des = #{ #customer.description }")
@RequestMapping(value = "/create", method = RequestMethod.POST)
public CustomerTicket createCustomerTicket(@RequestBody CustomerTicket customer) {
log.info("customer is 888888 {}", customer.toString());
CustomerTicket customerTicket = new CustomerTicket();
customerTicket.setId(customer.getId());
customerTicket.setAccountId(customer.getAccountId());
customerTicket.setOrderNumber(customer.getOrderNumber());
customerTicket.setDescription(customer.getDescription());
customerTicket.setCreateTime(new Date());
return customerTicket;
}
@TenantLog(module = "custer", description = "get ...", content = "parameter: #{ #customer.toString() }")
@RequestMapping(value = "/create1", method = RequestMethod.POST)
public CustomerTicket createCustomerTicket1(@RequestBody CustomerTicket customer) {
log.info("customer is 888888 {}", customer.toString());
CustomerTicket customerTicket = new CustomerTicket();
customerTicket.setId(customer.getId());
customerTicket.setAccountId(customer.getAccountId());
customerTicket.setOrderNumber(customer.getOrderNumber());
customerTicket.setDescription(customer.getDescription());
customerTicket.setCreateTime(new Date());
return customerTicket;
}
2. AOP + SpEL
@Slf4j
@Aspect
@Component
public class TenantLogOperateAspect {
// 需要被SpEl解析的模板前缀和后缀 {{ expression }}
public static final TemplateParserContext TEMPLATE_PARSER_CONTEXT = new TemplateParserContext("#{", "}");
// SpEL解析器
public static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser();
@Pointcut("@annotation(com.chenjunjie.webpro.aop.TenantLog)")
public void tenantLogPointcut() {
}
@Around("tenantLogPointcut()")
public Object addAspect(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 参数
Object[] args = joinPoint.getArgs();
// 参数名称
String[] parameterNames = signature.getParameterNames();
// 目标方法
Method targetMethod = signature.getMethod();
TenantLog operationLog = targetMethod.getAnnotation(TenantLog.class);
// request
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
try {
/**
* SpEL解析的上下文,把 HandlerMethod 的形参都添加到上下文中,并且使用参数名称作为KEY
*/
EvaluationContext evaluationContext = new StandardEvaluationContext();
for (int i = 0; i < args.length; i ++) {
evaluationContext.setVariable(parameterNames[i], args[i]);
}
String logContent = EXPRESSION_PARSER.parseExpression(operationLog.content(), TEMPLATE_PARSER_CONTEXT).getValue(evaluationContext, String.class);
// TODO 异步存储日志
System.out.println("**************************");
log.info("operationLog={}", logContent);
System.out.println("**************************");
// 执行方法
Object proceed = joinPoint.proceed();
return proceed;
} catch (Exception e) {
log.error("操作日志SpEL表达式解析异常: {}", e.getMessage());
}
return null;
}
}