SpringBoot SpEL语法扫盲与查询手册的实现
Spring 表达式语言简称为 SpEL,一种类似 Ognl 的对象图导航语言(对于 ognl 不熟悉的同学可以参考一下: Ognl 系列博文)
SeEL 为 Spring 提供了丰富的想象空间,除了一些基本的表达式操作之外,还支持
访问 bean 对象 调用方法,访问(修改)类(对象)属性 计算表达式 正则匹配 ...I. 语法百科
以下内容均来自官方文档: https://docs.spring.io/spring-framework/docs/5.2.1.RELEASE/spring-framework-reference/core.html#expressions
1. 字面表达式
Spel 支持strings, numeric values (int, real, hex), boolean, and null等基本类型,实例如下
ExpressionParser parser = new SpelExpressionParser();// evals to 'Hello World'String helloWorld = (String) parser.parseExpression('’Hello World’').getValue();// double 类型double avogadrosNumber = (Double) parser.parseExpression('6.0221415E+23').getValue();// evals to 2147483647int maxValue = (Integer) parser.parseExpression('0x7FFFFFFF').getValue();boolean trueValue = (Boolean) parser.parseExpression('true').getValue();Object nullValue = parser.parseExpression('null').getValue();
请注意,字符串需要用单引号包括,浮点数默认为 double 类型,用null表示null object
输出结果
str: Hello Worlddouble: 6.0221415E23int: 2147483647bool: truenull: null
2. Inline List
通过{}来表明 List 表达式,一个空的列表直接用{}表示
ExpressionParser parser = new SpelExpressionParser();// Integer列表List numbers = (List) parser.parseExpression('{1,2,3,4}').getValue();System.out.println('list: ' + numbers);// List的元素为ListList<List> listlOfLists = (List) parser.parseExpression('{{’a’,’b’},{’x’,’y’}}').getValue();System.out.println('List<List> : ' + listlOfLists);
输出结果
list: [1, 2, 3, 4]List<List> : [[a, b], [x, y]]
3. Inline map
{key:value}来表示 map 表达式,空 Map 直接用{:}表示
private void map() { ExpressionParser parser = new SpelExpressionParser(); Map map = (Map) parser.parseExpression('{txt:’Nikola’,dob:’10-July-1856’}').getValue(); System.out.println('map: ' + map); Map mapOfMaps = (Map) parser.parseExpression('{txt:{first:’Nikola’,last:’Tesla’},dob:{day:10,month:’July’,year:1856}}') .getValue(); System.out.println('Map<Map>: ' + mapOfMaps);}
输出结果
map: {txt=Nikola, dob=10-July-1856}Map<Map>: {txt={first=Nikola, last=Tesla}, dob={day=10, month=July, year=1856}}
4. 数组
数组可以借助new构造方法来实现,通过下标ary[index]的方式访问数组中的元素
private void array() { ExpressionParser parser = new SpelExpressionParser(); int[] numbers1 = (int[]) parser.parseExpression('new int[4]').getValue(); System.out.println('array: ' + JSON.toJSONString(numbers1)); // Array with initializer int[] numbers2 = (int[]) parser.parseExpression('new int[]{1,2,3}').getValue(); System.out.println('array: ' + JSON.toJSONString(numbers2)); // Multi dimensional array int[][] numbers3 = (int[][]) parser.parseExpression('new int[4][5]').getValue(); System.out.println('array: ' + JSON.toJSONString(numbers3)); int[] nums = new int[]{1, 3, 5}; EvaluationContext context = new StandardEvaluationContext(); context.setVariable('num', nums); // 通过下标访问数组中的元素 Integer numVal = parser.parseExpression('#num[1]').getValue(context, Integer.class); System.out.println('numVal in array: ' + numVal);}
输出如下
array: [0,0,0,0]array: [1,2,3]array: [[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0]]numVal in array: 3
5. 表达式
Spel 支持一些 Java 语法中常规的比较判断,算数运算,三元表达式,类型判断,matches正则匹配等基表表达式
下面给出一些简单的实例
public void expression() { ExpressionParser parser = new SpelExpressionParser(); // 运算 System.out.println('1+2= ' + parser.parseExpression('1+2').getValue()); // 比较 System.out.println('1<2= ' + parser.parseExpression('1<2').getValue()); System.out.println('true ? hello : false > ' + parser.parseExpression('3 > 2 ? ’hello’: ’false’ ').getValue()); // instanceof 判断,请注意静态类,用T进行包装 System.out.println('instance : ' + parser.parseExpression('’a’ instanceof T(String)').getValue()); //正则表达式 System.out.println('22 是否为两位数字 :' + parser.parseExpression('22 matches ’d{2}’').getValue());}
输出结果
1+2= 31<2= truetrue ? hello : false > helloinstance : true22 是否为两位数字 :true
6. Type 与静态类
如果想获取 Class 对象,或者访问静态成员/方法,可以借助T()语法来实现
比如我们有一个静态类
public static class StaClz { public static String txt = '静态属性'; public static String hello(String tag) { return txt + ' : ' + tag; }}
如果希望访问静态属性txt, 表达式可以写成T(com.git.hui.boot.spel.demo.BasicSpelDemo.StaClz).txt,请注意圆括号中的是完整签名;访问静态方法方式类似
public void type() { // class,静态类 ExpressionParser parser = new SpelExpressionParser(); String name = parser.parseExpression('T(com.git.hui.boot.spel.demo.BasicSpelDemo.StaClz).txt').getValue(String.class); System.out.println('txt: ' + name); String methodReturn = parser.parseExpression('T(com.git.hui.boot.spel.demo.BasicSpelDemo.StaClz).hello' + '(’一灰灰blog’)') .getValue(String.class); System.out.println('static method return: ' + methodReturn); // class类获取 Class stringClass = parser.parseExpression('T(String)').getValue(Class.class); System.out.println('class: ' + stringClass.getName());}
输出结果如下
txt: 静态属性static method return: 静态属性 : 一灰灰blogclass: java.lang.String
上面的写法,请重点看一下T(String),这里的 String 没有用完整的包路径,即直接位于java.lang包下的类,是可以省略掉完整包名的,就像我们平时写代码时,也不需要显示的加一个import java.lang.*
7. 构造方法
上面介绍 array 的时候,就介绍了使用new来创建数组对象,当然也可以直接构造其他的普通对象, 如我们新建一个测试类
public static class Person { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public int getAge() { return age; } @Override public String toString() { return 'Person{' + 'txt=’' + name + ’’’ + ', age=' + age + ’}’; }}
通过 SpEl 创建一个对象的实例
public void construct() { ExpressionParser parser = new SpelExpressionParser(); Person person = parser.parseExpression('new com.git.hui.boot.spel.demo.BasicSpelDemo.Person(’一灰灰’, 20)') .getValue(Person.class); System.out.println('person: ' + person);}
输出结果如下:
person: Person{txt=’一灰灰’, age=20}
请注意,构造方法中类的完整签名
8. 变量引用
细心的小伙伴,在上面介绍数组的成员演示的实例中,写法如'#num[1]',这个 num 前面有一个#,这是一个语法定义,有#修饰的表示变量访问
要理解这一小节,首先得理解EvaluationContext, 在我们的 SpEL 表达式的解析中,getValue有一个参数就是这个 Context,你可以将他简单理解为包含一些对象的上下文,我们可以通过 SpEL 的语法,来访问操作 Context 中的某些成员、成员方法属性等
一般的操作过程如下:
context.setVariable('person', person); 向EvaluationContext中塞入成员变量 parser.parseExpression(xxx).getValue(context) 解析 SpEL 表达式,context 必须作为传参丢进去哦一个简单的实例
public void variable() { ExpressionParser parser = new SpelExpressionParser(); Person person = new Person('一灰灰blog', 18); EvaluationContext context = new StandardEvaluationContext(); context.setVariable('person', person); String name = parser.parseExpression('#person.getName()').getValue(context, String.class); System.out.println('variable name: ' + name); Integer age = parser.parseExpression('#person.age').getValue(context, Integer.class); System.out.println('variable age: ' + age);}
输出结果如下
variable name: 一灰灰blogvariable age: 18
友情提示,如果访问对象的私有 Field/method,会抛异常
9. 函数
Context 中的变量,除了是我们常见的基本类型,普通的对象之外,还可以是方法,在setVariable时,设置的成员类型为method即可
public void function() { try { ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); // 注册一个方法变量,参数为method类型 context.setVariable('hello', StaClz.class.getDeclaredMethod('hello', String.class)); String ans = parser.parseExpression('#hello(’一灰灰’)').getValue(context, String.class); System.out.println('function call: ' + ans); } catch (Exception e) { e.printStackTrace(); }}
输出结果如下
function call: 静态属性 : 一灰灰
10. bean 访问
在 Spring 中,什么对象最常见?当然是 bean, 那么我们可以直接通过 SpEL 访问 bean 的属性、调用方法么?
要访问 bean 对象,所以我们的EvaluationContext中需要包含 bean 对象才行
借助BeanResolver来实现,如context.setBeanResolver(new BeanFactoryResolver(applicationContext));其次访问 bean 的前缀修饰为@符号为了演示这种场景,首先创建一个普通的 Bean 对象
@Data@Componentpublic class BeanDemo { private String blog = 'https://spring.hhui.top'; private Integer num = 8; public String hello(String name) { return 'hello ' + name + ', welcome to my blog ' + blog + ', now person: ' + num; }}
接着我们需要获取ApplicationContext,所以可以稍微改一下我们的测试类,让它继承自ApplicationContextAware
private ApplicationContext applicationContext;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext;}public void bean() { ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new BeanFactoryResolver(applicationContext)); // 获取bean对象 BeanDemo beanDemo = parser.parseExpression('@beanDemo').getValue(context, BeanDemo.class); System.out.println('bean: ' + beanDemo); // 访问bean方法 String ans = parser.parseExpression('@beanDemo.hello(’一灰灰blog’)').getValue(context, String.class); System.out.println('bean method return: ' + ans);}
上面的写法和之前的并没有太大的区别,实际输出结果如下
bean: BeanDemo(blog=https://spring.hhui.top, num=8)bean method return: hello 一灰灰blog, welcome to my blog https://spring.hhui.top, now person: 8
11. ifElse
SpEL 支持三元表达式,在上述的表达式中也给出了实例
public void ifThenElse() { // 三元表达式,? : ExpressionParser parser = new SpelExpressionParser(); String ans = parser.parseExpression('true ? ’正确’: ’错误’').getValue(String.class); System.out.println('ifTheElse: ' + ans);}
输出结果如下
ifTheElse: 正确
12. elvis
xx != null ? xx : yy => xx?:yy
这个也属于我们经常遇到的一种场景,如果 xx 为 null,则返回 yy;否则直接返回 xx;简化写法为 elvis 写法: xx?:yy
public void elvis() { // xx != null ? xx : yy => xx?:yy ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = new StandardEvaluationContext(); context.setVariable('name', null); String name = parser.parseExpression('#name?:’Unknown’').getValue(context, String.class); System.out.println('elvis-before ' + name); context.setVariable('name', 'Exists!'); name = parser.parseExpression('#name?:’Unknown’').getValue(context, String.class); System.out.println('elvis-after ' + name);}
输出结果如下
elvis-before Unknownelvis-after Exists!
13. 安全表达式
在 java 中,最常见最讨厌的是一个就是 NPE 的问题,SpEL 中当然也可能出现这种情况,但是若在 SpEL 中进行非空判断,那就很不优雅了,SpEL 提供了xx?.yy的写法来避免 npe,即
xx == null ? null : xx.yy => xx?.yy
举例说明
public void safeOperate() { // 防npe写法, xx == null ? null : xx.yy => xx?.yy ExpressionParser parser = new SpelExpressionParser(); Person person = new Person(null, 18); String name = parser.parseExpression('name?.length()').getValue(person, String.class); System.out.println('safeOperate-before: ' + name); person.name = '一灰灰blog'; name = parser.parseExpression('name?.length()').getValue(person, String.class); System.out.println('safeOperate-after: ' + name);}
输出结果如下
safeOperate-before: nullsafeOperate-after: 7
14. 容器截取
遍历容器,获取子集,相当于 jdk8 Stream 中 filter 用法,语法格式如下
xx.?[expression], 请注意中括弧中的表达式必须返回 boolean
举例说明
public void collectionSelection() { // 容器截取,返回满足条件的子集 // xx.?[expression] , 将满足expression的子元素保留,返回一个新的集合,类似容器的 filter List<Integer> list = new ArrayList<>(Arrays.asList(1, 3, 4, 6, 7, 8, 9)); ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); context.setVariable('list', list); // 用 #this 来指代列表中的迭代元素 List<Integer> subList = (List<Integer>) parser.parseExpression('#list.?[#this>5]').getValue(context); System.out.println('subList: ' + subList); Map<String, Integer> map = new HashMap<>(); map.put('a', 1); map.put('b', 10); map.put('c', 4); map.put('d', 7); context.setVariable('map', map); // 表达式内部用key, value 来指代map的k,v Map subMap = parser.parseExpression('#map.?[value < 5]').getValue(context, Map.class); System.out.println('subMap: ' + subMap); subMap = parser.parseExpression('#map.?[key == ’a’]').getValue(context, Map.class); System.out.println('subMap: ' + subMap);}
输出结果如下
subList: [6, 7, 8, 9]subMap: {a=1, c=4}subMap: {a=1}
注意
在列表表达式中,可以通过#this来指代列表中的每一个元素 在 map 表达式中,通过key, value来分别指代 map 中的k,v15. 容器映射
将一个集合通过某种规则,映射为另一种集合,相当于 jdk8 Stream 中的 map 用法,语法如下
xx.![expression], 将表达式计算的结果作为输出容器中的成员
举例如下
public void collectionProjection() { // 容器操作之后,生成另一个容器, 类似lambda中的map方法 // xx.![expression] List<Integer> list = new ArrayList<>(Arrays.asList(1, 3, 4, 6, 7, 8, 9)); ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); context.setVariable('list', list); // 用 #this 来指代列表中的迭代元素 List newList = parser.parseExpression('#list.![#this * 2]').getValue(context, List.class); System.out.println('newList: ' + newList); Map<String, Integer> map = new HashMap<>(); map.put('a', 1); map.put('b', 10); map.put('c', 4); map.put('d', 7); context.setVariable('map', map); List newListByMap = parser.parseExpression('#map.![value * 2]').getValue(context, List.class); System.out.println('newListByMap: ' + newListByMap);}
输出结果如下:
newList: [2, 6, 8, 12, 14, 16, 18]newListByMap: [2, 20, 8, 14]
16. 表达式模板
SpEL 还提供了一种自定义表达式模板的方式,将字面量和表达式放在一起使用,比如下面这一条语句
'random number is #{T(java.lang.Math).random()}'
其中#{T(java.lang.Math).random()}是一个 SpEL 表达式,左边的是普通字符串,这种写法也常见于@Value注解中的属性写法,当然直接通过上面的写法执行这个语句会报错,这个时候需要指定ParserContext
举例说明
public void template() { // 模板,混合字面文本与表达式,使用 #{} 将表达式包裹起来 ExpressionParser parser = new SpelExpressionParser(); String randomPhrase = parser.parseExpression('random number is #{T(java.lang.Math).random()}', ParserContext.TEMPLATE_EXPRESSION).getValue(String.class); System.out.println('template: ' + randomPhrase);}
输出结果如下
template: random number is 0.10438946298113871
17. 小结
SpEL 属于非常强大的表达式语言了,就我个人的感觉而言,它和 OGNL 有些像,当它们的上下文中包含了 Spring 的上下文时,可以访问任何的 bean,而你可以借助它们的语法规范,做各种事情
推荐我之前的一个项目,https://github.com/liuyueyi/quick-fix,利用 ognl 结合ApplicationContext,可以随心所欲的访问控制应用中的任何 bean 对象
II. 其他
0. 项目
工程:https://github.com/liuyueyi/spring-boot-demo
源码:https://github.com/liuyueyi/spring-boot-demo/spring-boot/013-spel
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持好吧啦网。
相关文章: