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的几个概念:
- 表达式(“干什么”):SpEL的核心,所以表达式语言都是围绕表达式进行的。
- 解析器(“谁来干”):用于将字符串表达式解析为表达式对象。
- 上下文(“在哪干”):表达式对象执行的环境,该环境可能定义变量、定义自定义函数、提供类型转换等等。
- 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语法的。