Spring 之 Spel 表达式

Spring 之 Spel 表达式

 

 

1、简介
官网: https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions
Spring Expression Language(简称 "SpEL")是一种功能强大的表达式语言,支持在运行时查询和操作对象图。其中最显著的是:方法调用和基本的字符串模板功能。
虽然SpEL是Spring产品组合中表达式评估的基础,但它与Spring没有直接联系,可以独立使用。

表达式语言支持以下功能:
  • 文字表达式: 字符串使用单引号'字符串',同时还支持double、int、boolean、Object类型,这些类型都不需要引号。
  • 布尔和关系运算符: 支持的逻辑运算符 and, or, and not。
  • 类表达式: T(全类名),java.lang类型不需要是 完全限定,使用方式:T(String),T(java.util.Date),也可以通过这种方式来调用方法。
  • 访问 properties, arrays, lists, maps: 只要用一个.表示嵌套 属性值,属性名称的第一个字母不区分大小写。
  • 数组:inventions[3]。
  • List当中存储的对象,获取对象属性:Members[0].Name,属性值是个数组的情况:Members[0].Inventions[6]。
  • properties和maps,获取指定key值:Officers['president'],key值假如是个对象,获取对象当中属性的值,Officers['president'].PlaceOfBirth.City,假如是数组Officers['advisors'][0].PlaceOfBirth.Country。
  • 方法调用: 'abc'.substring(2, 3),isMember('Mihajlo Pupin'),这两种都是可以的,一种是通过某个对象调用方法,一种是直接调用当前类的方法。
  • 关系运算符: 'black' < 'block',2 < -5.0,2 == 2,除了标准的关系运算符SpEL支持instanceof和 增则表达式的matches操作。'xyz' instanceof T(int),'5.00' matches '^-?\\d+(\\.\\d{2})?$'。每个符号操作者也可以被指定为一个纯字母变量。这个 避免了在使用的符号有特殊含义的文档类型的问题 其表达被嵌入(例如,XML文档)。
  • 文本是等值 比如: lt (<), gt (>), le (<=), ge (>=), eq (==), ne (!=), div (/), mod (%), not (!). 这些都是不区分大小写。
  • 逻辑运算符: 支持的逻辑运算符 and, or, and not,示例:isMember('Nikola Tesla') or isMember('Albert Einstein')。
  • 调用构造函数: 构造函数可以使用new运算符调用。 new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')。
  • Bean引用: 如果解析上下文已经配置,那么bean解析器能够 从表达式使用(@)符号查找bean类。例如:@foo。
  • 构造Array: new int[]{1,2,3},@Value("#{new int[]{1,2,3}}")
  • 内嵌lists: {1,2,3,4}代表List,在spring项目当中使用的话就得再嵌套一层:
  • @Value("#{{1,2,3,4}}"),list的属性假如也是list可以使用{{'a','b'},{'x','y'}}。
  • @Value("#{{{'a','b'},{'x','y'}}}") 层次关系一定要屡明白!
  • 内嵌maps: {name:'Nikola',dob:'10-July-1856'}, {name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}。
  • 三元运算符: false ? 'trueExp' : 'falseExp',Elvis操作符使三元运算符语法的缩短,并用于在 Groovy语言。 Java当中的三元:String displayName = name != null ? name : "Unknown";,
  • SpEL当中使用Elvis操作符:null?:'Unknown',@Value("#{systemProperties['pop3.port'] ?: 25}") 如果它不存在,那么将定义为25
  • 变量: 变量可以在使用语法#变量名表达引用。变量使用在StandardEvaluationContext方法的setVariable设置。变量#this 始终定义和指向的是当前的执行对象,
  • 变量#root总是 定义和指向root context object。虽然#this可能作为表达式的一些组件被执行 ,但#root总是指 root。
  • bean引用: 如果解析上下文已经配置,那么bean解析器能够 从表达式使用(@)符号查找bean类。
  • 安全导航运算符: 安全导航操作符是用来避免NullPointerException,用法:PlaceOfBirth?.City,代表的是获取PlaceOfBirth对象的City属性。假如PlaceOfBirth为null正常会报空指针,该 安全航行运算符将简单地返回空代替抛出的异常。
  • 用户定义的函数: 支持自定义函数,函数就是方法。
  • 集合投影: 投影允许集合驱动子表达式和解析 生成一个新的集合。 Members.![placeOfBirth.city] 一个map也可以用于驱动投影。
  • 集合筛选: 选择是一个强大的表达式语言功能,他允许你转换一些 源集合到另一个通过其条目选择。Members.?[Nationality == 'Serbian'],map.?[value<27]说白了就是过滤功能
  • 模板表达式: 表达式模板允许文字文本与一个或多个解析块的混合。 你可以每个解析块分隔前缀和后缀的字符, 当然,常见的选择是使用#{}作为分隔符。

2、依赖
在 spring-context 包中已经引入 spring-expression 包, 在其他非Spring的项目中,可以单独引入:
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-expression</artifactId>
    <version>5.0.0.RELEASE</version>
    <scope>compile</scope>
</dependency>


3、案例
任何语言都需要有自己的语法,SpEL当然也不例外。所以我们应该能够想到,给一个字符串最终解析成一个值,这中间至少得经历:
字符串 -> 语法分析 -> 生成表达式对象 -> (添加执行上下文) -> 执行此表达式对象 -> 返回结果

关于SpEL的几个概念:
  1. 表达式(“干什么”):SpEL的核心,所以表达式语言都是围绕表达式进行的。
  2. 解析器(“谁来干”):用于将字符串表达式解析为表达式对象。
  3. 上下文(“在哪干”):表达式对象执行的环境,该环境可能定义变量、定义自定义函数、提供类型转换等等。
  4. root根对象及活动上下文对象(“对谁干”):root根对象是默认的活动上下文对象,活动上下文对象表示了当前表达式操作的对象。
import com.dw.study.model.MyUser;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.util.ArrayList;
import java.util.HashMap;

/**
 * @Author dw
 * @ClassName SpelTest
 * @Description
 * @Date 2023/7/13 14:48
 * @Version 1.0
 */
public class SpelTest {

    public static void main(String[] args) {
        // 1.创建表达式解析器
        ExpressionParser parser = new SpelExpressionParser();
        // 2.创建变量上下文,设置变量
        StandardEvaluationContext ctx = new StandardEvaluationContext();
        //把list和map都放进环境变量里面去
        ctx.setVariable("myPerson", new MyUser("fsx", 30));
        ctx.setVariable("myList", new ArrayList<String>() {{
            add("fsx");
            add("周杰伦");
        }});
        ctx.setVariable("myMap", new HashMap<String, Integer>(2) {{
            put("fsx", 18);
            put("周杰伦", 40);
        }});

        // 3.根据表达式计算结果
        // Person{name='fsx', age=30}
        System.out.println(parser.parseExpression("#myPerson").getValue(ctx));
        // fsx
        System.out.println(parser.parseExpression("#myPerson.name").getValue(ctx));
        // 安全导航运算符 "?",安全导航操作符是用来避免'NullPointerException`
        System.out.println(parser.parseExpression("#myPerson?.name").getValue(ctx));
        // setVariable方式取值不能像root一样,前缀不可省略~~显然找不到这个key就返回null呗~~~
        System.out.println(parser.parseExpression("#name").getValue(ctx));
        // [fsx, 周杰伦]
        System.out.println(parser.parseExpression("#myList").getValue(ctx));
        // 周杰伦
        System.out.println(parser.parseExpression("#myList[1]").getValue(ctx));

        // 请注意对Map取值两者的区别:中文作为key必须用''包起来   当然['fsx']也是没有问题的
        // 18
        System.out.println(parser.parseExpression("#myMap[fsx]").getValue(ctx));
        // 40
        System.out.println(parser.parseExpression("#myMap['周杰伦']").getValue(ctx));

        // =========若采用#key引用的变量不存在,返回的是null,并不会报错哦==============
        System.out.println(parser.parseExpression("#map").getValue(ctx));

        // 黑科技:SpEL内直接可以使用new方式创建实例  能创建数组、List、对象
        System.out.println(parser.parseExpression("new String[]{'java','spring'}").getValue());
        System.out.println(parser.parseExpression("{'java','c语言','PHP'}").getValue());
        System.out.println(parser.parseExpression("new com.dw.study.model.MyUser('dw', 30)").getValue());

        // 静态方法方法调用 T(类的全路径限定名).方法名(), 不要使用${}包裹。此方法一般用来引用常量或静态方法
        System.out.println(parser.parseExpression("T(java.lang.Math).random()").getValue(String.class));
        // 实例方法调用
        System.out.println(parser.parseExpression("#myPerson.getName()").getValue(ctx));

        /**
         * 调用Bean 中的方法
         * 如果解析上下文已经配置,那么bean解析器能够 从表达式使用(@)符号查找bean类。
         */
        // 此处用DefaultListableBeanFactory做测试,系统运行时可传入ApplicationContext
        DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
        beanFactory.registerSingleton("user", new MyUser("成龙", 30));
        ctx.setBeanResolver(new BeanFactoryResolver(beanFactory));
        // 3. spel解析器执行表达式取得结果
        System.out.println(parser.parseExpression("@user.getName()").getValue(ctx, String.class));
    }

}


4、spel 注入漏洞解决
最直接的防御方法就是使用SimpleEvaluationContext替换StandardEvaluationContext。
    private static void test() {
        //执行shell脚本
        String spel = "T(java.lang.Runtime).getRuntime().maxMemory()";
        ExpressionParser parser = new SpelExpressionParser();
        //只读属性
        EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
        Expression expression = parser.parseExpression(spel);
        System.out.println(expression.getValue(context));
    }

SimpleEvaluationContext和StandardEvaluationContext是SpEL提供的两个EvaluationContext:
SimpleEvaluationContext- 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。
StandardEvaluationContext- 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
SimpleEvaluationContext旨在仅支持SpEL语言语法的一个子集,不包括 Java类型引用、构造函数和bean引用;而StandardEvaluationContext是支持全部SpEL语法的。

 

posted @ 2023-07-13 18:07  邓维-java  阅读(3996)  评论(0)    收藏  举报