简介
SpEL 常见用法
快速入门
实际应用
SpEL 内部实现的简单梳理
总结
简介
Spring Expression Language(简称 SpEL,Sp:Spring,EL:Expression Language)是一个支持运行时查询和操作对象图的强大的表达式语言。
在 Spring 产品组合中与我们常见的 Beans 模块、Core 核心模块、Context 上下文模块一起组成了 Spring 的核心容器,是表达式计算的基础,支持在运行时查询和操作对象,可以与基于 XML 和基于注解的 Spring 配置还有 bean 定义一起使用。
Spring 的体系结构
SpEL 常见用法
SpEL 的语法类似于 JSP 中 EL 表达式,使用 #{…} 作为定界符,所有在大框号中的字符都将被认为是 SpEL。
SpEL 支持如下表达式:
SpEL 字面量:
整数:#{8}
小数:#{8.8}
科学计数法:#{1e4}
String:#{'string'}
Boolean:#{true}
SpEL 引用 bean,属性和方法:
引用其他对象:#{car}
引用其他对象的属性:#{car.brand}
调用其它方法 , 还可以链式操作:#{car.toString()}
调用静态方法静态属性:#{T(java.lang.Math).PI}
SpEL 支持的运算符号:
算术运算符:+,-,*,/,%,^(加号还可以用作字符串连接)
比较运算符:< , > , == , >= , <= , lt , gt , eg , le , ge
逻辑运算符:and , or , not , |
if-else 运算符(类似三目运算符):?:(temary), ?:(Elvis)
正则表达式:#{admin.email matches ‘[a-zA-Z0-9._%±]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,4}’}
快速入门
public void demo() {
// 1 定义解析器
SpelExpressionParser parser = new SpelExpressionParser();
// 2 使用解析器解析表达式
Expression exp = parser.parseExpression("'xxx'");
// 3 获取解析结果
String value = (String) exp.getValue();
System.out.println(value);//xxx
}
复制代码
public void demo() {
// 1 定义解析器
SpelExpressionParser parser = new SpelExpressionParser();
// 2 使用解析器解析表达式
Expression exp = parser.parseExpression("'xxx'.concat('yyy')");
// 3 获取解析结果
String value = (String) exp.getValue();
System.out.println(value);//xxxyyy
exp = parser.parseExpression("'xxx'.bytes");
byte[] bytes = (byte[]) exp.getValue();
exp = parser.parseExpression("'xxx'.bytes.length");
int length = (Integer) exp.getValue();
System.out.println("length: " + length);//length: 3
}
复制代码
public void demo() {
User user = new User();
user.setName("xxx");
User user2 = new User();
user2.setName(user.getName());
// 1 定义解析器
ExpressionParser parser = new SpelExpressionParser();
// 指定表达式
Expression exp = parser.parseExpression("name");
// 2 使用解析器解析表达式,获取对象的属性值
String name = (String) exp.getValue(user2);
// 3 获取解析结果
System.out.println(name);//xxx
// 2.1 使用解析器解析表达式,获取对象的属性值并进行运算
Expression exp2 = parser.parseExpression("name == 'xxx'");
// 3.1 获取解析结果
boolean result = exp2.getValue(user2, Boolean.class);
System.out.println(result);//true
}
复制代码
实际应用
实际应用中我们很少会使用字面量或者运算符,更多的还是解析对象或者对象属性来实现自己的功能。下面以使用 AOP+SpEL 动态组装异常信息的自定义的业务报警举例说明。
流程简述
具体方式:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Monitor {
/**
* 类型
* @return
*/
MonitorScenesTypeEnum scenes() default MonitorScenesTypeEnum.DEFAULT;
/**
* 表达式
* @return
*/
String monitorSpEL() default "";
}
复制代码
public void process(Monitor monitor, JoinPoint joinPoint, Object result, Throwable ex) throws ClassNotFoundException {
MonitorScenesTypeEnum scenes = monitor.scenes();
Integer code = scenes.getCode();
//获取Apollo配置
Map<Integer, MonitorConfig> monitorConfigMap = apolloConfigService.getMonitorConfigMap();
if (MapUtils.isEmpty(monitorConfigMap) || !monitorConfigMap.containsKey(code)) {
return;
}
MonitorConfig monitorConfig = monitorConfigMap.get(code);
//当有返回值时需要校验一下结果是否符合预期
String resultType = monitorConfig.getResultType();
if (checkResult(result, resultType)) {
return;
}
//获取入参的SpEL表达式进行解析
String monitorSpEL = monitor.monitorSpEL();
String monitorTrace = String uuid = StringUtils.isNotBlank(monitorSpEL) ?
SpelParseUtil.generateKeyBySpEL(monitorSpEL, joinPoint) : StringUtils.EMPTY_STRING;
//截取一下异常信息
String otherParamsJson = "";
if (Objects.nonNull(ex)) {
Map<String, String> otherParams = Maps.newHashMap();
String stackTraceAsString = Throwables.getStackTraceAsString(ex);
String errMsg = stackTraceAsString;
if (stackTraceAsString.length() > NumberConstant.NUMBER_512) {
errMsg = stackTraceAsString.substring(0,NumberConstant.NUMBER_512) + "...";
}
otherParams.put("异常信息", errMsg);
otherParamsJson = JsonUtil.silentObject2String(otherParams);
}
// 异步发送报警信息
asyncSendMonitor(scenes, monitorTrace, otherParamsJson);
}
}
......
private static SpelExpressionParser parser = new SpelExpressionParser();
private static DefaultParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
public static String generateKeyBySpEL(String spelString, JoinPoint joinPoint) {
// 通过joinPoint获取被注解方法
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
// 使用spring的DefaultParameterNameDiscoverer获取方法形参名数组
String[] paramNames = nameDiscoverer.getParameterNames(method);
// 解析过后的Spring表达式对象
Expression expression = parser.parseExpression(spelString);
// spring的表达式上下文对象
EvaluationContext context = new StandardEvaluationContext();
// 通过joinPoint获取被注解方法的形参
Object[] args = joinPoint.getArgs();
// 给上下文赋值
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
return Objects.requireNonNull(expression.getValue(context)).toString();
}
复制代码
@Monitor(scenes = MonitorScenesTypeEnum.CHANGE_PRICE_FAIL, spelStr = "#request?.orderId")
public ChangePriceResponse changePrice(ChangePriceRequest request) {
BizOrderContext<ChangePriceRequest, ChangePriceResponse> bizOrderContext = BizOrderContext.create(OrderEventEnum.C1_CHANGE_JM_PRICE, request);
ZzAssert.isTrue(stateMachine.isCanFire(bizOrderContext), BizErrorCode.ORDER_STATUS_CHANGED);
stateMachine.fire(bizOrderContext);
return bizOrderContext.getResponse();
}
复制代码
SpEL 内部实现的简单梳理
SpEL 在 Spring 内部的实现可以简单理解如下:
语法分析
首先用户调用 ExpressionParser#parseExpression 方法触发表达式解析。
表达式解析器在内部先进行词法解析,将字符串形式的表达式拆分成不同的 Token,如 1 + 2 表达式会被拆分成 1、+、2 三部分。解析时同时会参考上下文 ParserContext,如上述示例中的 #{name} 表达式,解析器会先去掉前后缀 #{},然后再进行解析。
随后 Token 将被转换为抽象语法树,在内部使用 SpelNode 表示,为了简化用户操作语法树被包装到 Expression。
用户使用 Expression#getValue 方法获取表达式的值,在内部也会参考评估上下文 EvaluationContext 进行解析。
总结
本文只是简单的介绍了如何使用,实际场景中 SpEL 随处可见,除了上文中示例的监控报警之外,动态创建 Documet 类的 Index、动态加解锁、接口缓存等都有非常多的实践。
除此之外 spring 应用中常见的 @cacheable、@Value,以及 Spring Security 框架中的 @PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter 中都有 SpEL 的身影。
参考资料
[1] Spring Expression Language (SpEL) : https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions
[2] SpEL 你感兴趣的实现原理浅析: https://cloud.tencent.com/developer/article/1497676
转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。
关注公众号「转转技术」(综合性)、「大转转FE」(专注于FE)、「转转QA」(专注于QA),更多干货实践,欢迎交流分享~
评论