likes
comments
collection
share

SpEL快速上手及实践

作者站长头像
站长
· 阅读数 18

简介

Spring Expression Language(简称 SpEL,Sp:Spring,EL:Expression Language)是一个支持运行时查询和操作对象图的强大的表达式语言。

在Spring产品组合中与我们常见的Beans 模块、Core 核心模块、Context 上下文模块一起组成了Spring 的核心容器,是表达式计算的基础,支持在运行时查询和操作对象,可以与基于XML和基于注解的Spring配置还有bean定义一起使用。

SpEL快速上手及实践

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}’}

快速入门

  • Hello World 纯字面意义的字符串输出,实际场景中无人使用。
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动态组装异常信息的自定义的业务报警举例说明。

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 内部实现的简单梳理

SpEL 在 Spring 内部的实现可以简单理解如下:

SpEL快速上手及实践

语法分析

  1. 首先用户调用 ExpressionParser#parseExpression 方法触发表达式解析。
  2. 表达式解析器在内部先进行词法解析,将字符串形式的表达式拆分成不同的 Token,如 1 + 2 表达式会被拆分成 1、+、2 三部分。解析时同时会参考上下文 ParserContext,如上述示例中的 #{name} 表达式,解析器会先去掉前后缀#{},然后再进行解析。
  3. 随后 Token 将被转换为抽象语法树,在内部使用 SpelNode 表示,为了简化用户操作语法树被包装到 Expression。
  4. 用户使用 Expression#getValue 方法获取表达式的值,在内部也会参考评估上下文 EvaluationContext 进行解析。

总结

本文只是简单的介绍了如何使用,实际场景中SpEL随处可见,除了上文中示例的监控报警之外,动态创建Documet类的Index、动态加解锁、接口缓存等都有非常多的实践。

除此之外spring应用中常见的@cacheable、@Value,以及Spring Security框架中的@PreAuthorize、@PostAuthorize、@PreFilter、@PostFilter中都有SpEL的身影。

参考资料

[1] Spring Expression Language (SpEL) : docs.spring.io/spring-fram…

[2] SpEL你感兴趣的实现原理浅析: cloud.tencent.com/developer/a…


关于作者

钱曙光,转转C2B业务研发工程师。