写点什么

SpEL 快速上手及实践

  • 2022 年 7 月 08 日
  • 本文字数:4271 字

    阅读完需:约 14 分钟

SpEL快速上手及实践
  • 简介

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

快速入门

  • 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 动态组装异常信息的自定义的业务报警举例说明。

流程简述

具体方式:

  • 定义一个注解:

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 内部的实现可以简单理解如下:

语法分析

  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) : 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),更多干货实践,欢迎交流分享~

用户头像

还未添加个人签名 2019.04.30 加入

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」,各种干货实践,欢迎交流分享~

评论

发布
暂无评论
SpEL快速上手及实践_Java_转转技术团队_InfoQ写作社区