0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

基于SpEL可以实现的功能

科技绿洲 来源:Java技术指北 作者:Java技术指北 2023-09-30 10:37 次阅读

Spring Expression Language

概念

Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。该语言的语法类似于Unified EL,但提供了额外的特性, 最显著的是方法调用和基本的字符串模板功能。

虽然还有其他几种Java表达式语言可用——OGNL、MVEL和JBoss EL等,SpEL是为了向Spring社区提供一种受良好支持的表达式语言,可以跨Spring产品组合中的所有产品使用。SpEL基于一种与技术无关的API,在需要时可以集成其他表达式语言实现。

作用

基于SpEL我们可以实现下面这些功能,当然是基于表达式,SpEL提供了表达式运行环境,而且不依赖于Spring,这样我可以基于其强大的执行器实现各种扩展。

  • 支持功能
    • 字面表达式
    • 布尔和关系运算符
    • 正则表达式
    • 类表达式
    • 访问属性、数组、列表和映射
    • 方法调用
    • 关系运算符
    • 申明
    • 调用构造函数
    • bean引用
    • 数组构造
    • 内联的list
    • 内联的map
    • 三元运算符
    • 变量
    • 用户自定义函数
    • 集合投影
    • 集合选择
    • 模板化表达式

关键接口

将表达式字符串解析为可求值的编译表达式。支持解析模板以及标准表达式字符串。

public interface ExpressionParser {

  /**
   * 解析字符串表达式为Expression对象
   */
  Expression parseExpression(String expressionString) throws ParseException;

  /**
   * 解析字符串表达式为Expression对象,基于ParserContext解析字符串,比如常见的#{exrp}
   */
  Expression parseExpression(String expressionString, ParserContext context) throws ParseException;

}

表达式在求值上下文中执行。正是在这个上下文中,表达式求值期间遇到的引用才会被解析。EvaluationContext接口有一个默认的实现StandardBeanExpressionResolver,可以通过继承该类进行扩展。

public interface EvaluationContext {

 /**
  * 获取Root上下文对象
  */
 TypedValue getRootObject();

 /**
  * 返回属性读写访问器
  */
 List< PropertyAccessor > getPropertyAccessors();

 /**
  * 返回构造器解析器
  */
 List< ConstructorResolver > getConstructorResolvers();

 /**
  * 返回方法解析器
  */
 List< MethodResolver > getMethodResolvers();

 /**
  * 返回Bean解析器,用于Bean的查找
  */
 @Nullable
 BeanResolver getBeanResolver();

 /**
  * 根据类名(一般为全限名)返回一个类型定位器,比如T(java.lang.Math)
  */
 TypeLocator getTypeLocator();

 /**
  * 返回可以将值从一种类型转换(或强制转换)为另一种类型的类型转换器。
  */
 TypeConverter getTypeConverter();

 /**
  * 返回一个类型比较器,用于比较对象对是否相等。
  */
 TypeComparator getTypeComparator();

 /**
  * 返回一个运算符重载器,该重载器可以支持多个标准类型集之间的数学运算。
  */
 OperatorOverloader getOperatorOverloader();

 /**
     * 为变量设值
  */
 void setVariable(String name, @Nullable Object value);

 /**
  * 从变量取值
  */
 @Nullable
 Object lookupVariable(String name);

}

适用场景

  • 扩展变量
    正如Spring中使用的那样,我们可以基于SpEL实现基于Spring容器、运行环境上下文等动态取值。
  • 数据审计
    通过从复杂的数据结构中进行数据运算,一般针对数据汇总或者复杂的结构化数据时,通过自定义特定表达式(类似于DSL)来实现数据运算。

Spring的使用

在Spring中有着大量使用SpEL的场景,在平时的开发中,可能会看到如下的这些配置,比如通过${}、#{}这些包裹的表达式,主要都是基于SpEL实现。

  1. @Value("#{systemProperties['pop3.port'] ?: 25}")
    简单的看下源吗,可以看到针对@Value标记的类属性,是如何为其注入属性:

AutowiredAnnotationBeanPostProcessor -> AutowiredFieldElement#injectvalue = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter)DefaultListableBeanFactory#doResolveDependency

public class DefaultListableBeanFactory {
    
  public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
                                    @Nullable Set< String > autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {

    InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);
    try {
      Class< ? > type = descriptor.getDependencyType();
      Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
      if (value != null) {
        if (value instanceof String) {
          // 处理 ${} 占位替换
          String strVal = resolveEmbeddedValue((String) value);
          BeanDefinition bd = (beanName != null && containsBean(beanName) ?
                  getMergedBeanDefinition(beanName) : null);
          // SpEL处理
          value = evaluateBeanDefinitionString(strVal, bd);
        }
      }
      // 省略...
      return result;
    }catch (Exception e){
        // ...
    }
  }
}
  1. @Cacheable(value="users", key="#p0")
    具体实现可以阅读源码:org.springframework.cache.interceptor.CacheOperationExpressionEvaluator
  2. @KafkaListener(topics = "#{'${topics}'.split(',')}")
    具体实现可以阅读源码:org.springframework.kafka.annotation.KafkaListenerAnnotationBeanPostProcessor

示例

下面通过一些测试示例了解SpEL的基本用法,对应上面的一些功能实现,通过这些简单的例子,可以大概了解其语法与使用方式:

public class Tests {
  @Test
  public void runParser() throws NoSuchMethodException {
    SpelExpressionParser parser = new SpelExpressionParser();
    Map map = new HashMap();
    map.put("name", "SpEL");
    StandardEvaluationContext context = new StandardEvaluationContext(map);
    context.setVariable("var1", 1);
    context.setVariable("func1", StringUtils.class.getMethod("hasText", String.class));
    context.setVariable("func2", new Root());
    context.setBeanResolver(new MyBeanResolver());

    log.info("字面量:{}", parser.parseExpression("'hello'").getValue(context));
    log.info("对象属性:{}", parser.parseExpression("'hello'.bytes").getValue(context));
    log.info("变量:{}", parser.parseExpression("#var1").getValue(context));
    log.info("调用方法:{}", parser.parseExpression("'hello'.concat(' world')").getValue(context));
    log.info("静态方法:{}", parser.parseExpression("T(java.lang.System).currentTimeMillis()").getValue(context));
    log.info("方法:{}", parser.parseExpression("#func1('1')").getValue(context));
    log.info("实例方法:{}", parser.parseExpression("#func2.print('2')").getValue(context));

    Root root = new Root();
    root.list.add("0");
    parser.parseExpression("list[0]").setValue(context, root, "1");
    log.info("设值:{}", root);

    log.info("ROOT: {}", parser.parseExpression("#root").getValue(context));
    log.info("ROOT 取值: {}", parser.parseExpression("#root[name]").getValue(context));
    log.info("ROOT 取值: {}", parser.parseExpression("[name]").getValue(context));

    log.info("THIS: {}", parser.parseExpression("#this").getValue(context));

    log.info("运算符: {}", parser.parseExpression("1+1").getValue(context));
    log.info("操作符: {}", parser.parseExpression("1==1").getValue(context));
    log.info("逻辑运算: {}", parser.parseExpression("true && false").getValue(context));

    ParserContext parserContext = new ParserContext() {
      @Override
      public boolean isTemplate() {
        return true;
      }

      @Override
      public String getExpressionPrefix() {
        return "#{";
      }

      @Override
      public String getExpressionSuffix() {
        return "}";
      }
    };
    log.info("#{表达式}: {}", parser.parseExpression("#{1+1}", parserContext).getValue(context));

    log.info("Map: {}", parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context));
    log.info("List: {}", parser.parseExpression("{1,2,3,4}").getValue(context));
    log.info("Array: {}", parser.parseExpression("new int[]{1,2,3}").getValue(context));

    log.info("instanceof: {}", parser.parseExpression("'hello' instanceof T(Integer)").getValue(context, Boolean.class));
    log.info("regex: {}", parser.parseExpression("'5.00' matches '^-?d+(.d{2})?$'").getValue(context, Boolean.class));
    log.info("三目运算: {}", parser.parseExpression("false ? 'trueExp' : 'falseExp'").getValue(context, String.class));

    log.info("Bean: {}", parser.parseExpression("@bean1").getValue(context));
  }
}

特殊处理

  • T
    通过T(CLASS)指定类型,可以用来类型判断或者调用类静态方法sec.setTypeLocator(new StandardTypeLocator(evalContext.getBeanFactory().getBeanClassLoader()));
  • @
    通过@Name获取beansec.setBeanResolver(new BeanFactoryResolver(evalContext.getBeanFactory()));
  • &
    通过&Name获取beanFactory

sec.setBeanResolver(new BeanFactoryResolver(evalContext.getBeanFactory()));

  • 通过#{}标识表达式,主要在springBeanExpressionResolver resolver = beanFactory.getBeanExpressionResolver(); BeanExpressionContext context = new BeanExpressionContext(beanFactory, null);


从root取值:#root.name 或者 name(#root可以忽略) 从variables取值或方法:#var

扩展

下面以一个示例看下SpEL在我们项目中的具体应用:

public class Tests{
  /**
   * 执行测试
   */
  @Test
    public void runCalc(){
        SpelExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.setVariable("helper",new StudentHelper());
        context.setVariable("students", genStudents(100));

        log.info("max score: {}", parser.parseExpression("#helper.max(#students)").getValue(context, Double.class));
        log.info("min score: {}", parser.parseExpression("#helper.min(#students)").getValue(context, Double.class));
        log.info("avg score: {}", parser.parseExpression("#helper.avg(#students)").getValue(context, Double.class));
    }

  /**
   * 生成测试数据
   * @param count
   * @return
   */
  private List< Student > genStudents(int count){
        List< Student > students = new ArrayList<  >();
        Faker faker = new Faker(Locale.CHINA);
        Name name = faker.name();
        Number number = faker.number();
        IntStream.range(0, count).forEach(i- >{
            students.add(new Student(name.name(), number.randomDouble(3, 60, 100)));
        });
        return students;
    }

    @Data
    @AllArgsConstructor
    class Student{
        private String name;
        private double score;
    }

  /**
   * 工具类
   */
  class StudentHelper{

        public double max(List< Student > students){
            if(CollectionUtils.isEmpty(students)){
                return 0;
            }
            return students.stream().mapToDouble(Student::getScore).max().getAsDouble();
        }

        public double min(List< Student > students){
            if(CollectionUtils.isEmpty(students)){
                return 0;
            }
            return students.stream().mapToDouble(Student::getScore).min().getAsDouble();
        }

        public double avg(List< Student > students){
            if(CollectionUtils.isEmpty(students)){
                return 0;
            }
            return students.stream().mapToDouble(Student::getScore).average().getAsDouble();
        }
    }
}

在这个示例中,我们主要通过SpEL获取获取学生分值的最大值、最小值、平均值等方式,在实际的项目中使用时,绝非如此简单,比如我们在做数据统计时,对各项指标数值的计算就是通过SpEL实现, 因为具体功能的实现是由我们自己定义,因此在业务扩展上会非常的方便。

结束语

SpEL是一个功能非常强大的基于Java的解释型语言解析器,如果你想基于表达式的形式,对复杂结构数据计算或审计的需求时,不妨试试这个轻量级工具。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • JAVA
    +关注

    关注

    19

    文章

    2904

    浏览量

    103004
  • 字符串
    +关注

    关注

    1

    文章

    552

    浏览量

    20147
  • 执行器
    +关注

    关注

    5

    文章

    337

    浏览量

    19205
  • SPEL
    +关注

    关注

    0

    文章

    3

    浏览量

    6099
收藏 人收藏

    评论

    相关推荐

    FPGA可以实现DSP的功能吗?

    一般涉及到数字处理和逻辑控制都用DSP加FPGA实现,最近想用FPGA实现数字处理和逻辑控制,听搞通信的说多加几个门就可以了,数字处理时钟要求25MHZ,请高手指点一下。
    发表于 04-05 10:01

    用什么电子元件可以实现触点开关的功能

    如题:用什么电子元件可以实现触点开关(电键)的功能(按下电路接通,松手后电路断开)?请教各位大神!能说详细点最好!谢谢!!!!
    发表于 12-03 08:52

    耳机孔可以实现OTG功能吗。。。。??????

    普通的手机耳机孔可以实现OTG功能吗???比如说链接外置的摄像头。。。。。。
    发表于 08-01 16:05

    请问ADF4106可以实现分频器的功能吗?

    您们好:最近做的项目中需要使用到分频比很大的分频器,故计划使用ADF4106作为分频器使用,请问ADF4106可以实现分频器的功能吗,可以的话相噪性能怎么样呢?谢谢!
    发表于 10-08 10:34

    使用Azure软件包连接IoT中心可以实现哪些功能

    使用Azure软件包连接IoT中心可以实现什么功能?Azure IoT中心的架构图及功能
    发表于 03-30 08:07

    FPGA与ARM核结合可以实现功能互补吗?

    RISC和FPGA结合成发展趋势如何?FPGA与ARM核结合可以实现功能互补吗?
    发表于 06-18 07:47

    SysTick定时器的计时功能可以实现精准延时吗

    什么是定时器?stm32定时器分为哪几类?SysTick定时器的计时功能可以实现精准延时吗?
    发表于 08-12 06:02

    有什么方法可以实现stm32的计数功能

    stm32的外部时钟源模式2和外部时钟源模式1的区别是什么呢?有什么方法可以实现stm32的计数功能呢?
    发表于 11-23 07:59

    USB Low Speed 设备可以实现CDC虚拟串口功能吗?

    USB Low Speed 设备可以实现CDC虚拟串口功能吗?
    发表于 06-07 08:44

    并行eeprom是否可以实现gal芯片功能

    如果不考虑速度的话,他俩能否实现相同的功能,比如通过地址输入信号,数据数据,是否可以实现
    发表于 10-07 07:15

    如何使用Jlink实现虚拟串口功能

    串口调试是单片机开发过程必不可少的一个功能,一般是使用一个UART-TTL的串口模块来实现串口的功能,其实下载调试使用的Jlink仿真器也可以实现
    发表于 06-04 17:52 11次下载
    如何使用Jlink<b class='flag-5'>实现</b>虚拟串口<b class='flag-5'>功能</b>

    如何使用VPSS实现旋转功能

    海思芯片里面VI,VPSS以及VGS都可以实现图片旋转功能。VI实现旋转的话,会导致后期保存的图片也实现了旋转;VGS
    发表于 01-26 18:31 1次下载
    如何使用VPSS<b class='flag-5'>实现</b>旋转<b class='flag-5'>功能</b>

    技术解读 | SpEL表达式注入漏洞分析、检查与防御

    在安全角度来看外部来源的数据,均应视为不可信数据,对外部数据,其包含的所有信息都须经过校验或者过滤,再向下游服务进行传递。若无防护手段,攻击者可以通过构造恶意输入,对服务进行攻击。程序中如果使用未经
    的头像 发表于 03-25 07:55 1267次阅读

    5G工作记录仪都可以实现哪些功能

    5G工作记录仪是指具备5G网络连接功能的工作记录设备。它与传统的执法记录仪相比,能够利用5G网络提供更快的数据传输速度和更稳定的连接,从而增强其性能和功能扩展性。瑞尼品牌5G执法记录仪可以实现
    的头像 发表于 06-05 15:50 417次阅读
    5G工作记录仪都<b class='flag-5'>可以</b><b class='flag-5'>实现</b>哪些<b class='flag-5'>功能</b>?

    Redis可以实现消息中间件MQ的功能

    是一种通信模式:发送者(PUBLISH)发送消息,订阅者(SUBSCRIBE)接收消息,可以实现进程间的消息传递   Redis可以实现消息中间件MQ的
    的头像 发表于 01-25 14:48 331次阅读
    Redis<b class='flag-5'>可以</b><b class='flag-5'>实现</b>消息中间件MQ的<b class='flag-5'>功能</b>