快捷搜索:

五分钟学会Spring表达式语言SpEL,不但学会使用也知道底层原理?

 

#头条群星9月榜#

sprign 表达式

SpEL:Spring Expression Language,支持在运行时查询和操作对象图的一种强大的表达式语言。该语言的语法类似于Unified EL,但提供了额外的特性,最显著的是方法调用和基本的字符串模板功能。SpEL为Spring社区提供表达式语言的支持,但是并没有和Spring绑定,可以单独使用。

Spring 表达式中的几个接口类

几个重要接口类

接口ExpressionParser类,这个接口主要用来解析表达式字符串,并且返回一个表达式对象Expression,这个接口ExpressionParser有个安全并且可以重用的实现类SpelExpressionParser。

package org.springframework.expression; public interface ExpressionParser { /** * 解析表达式字符串并且返回一个可重复获取返回值的表达式对象 */ Expression parseExpression(String expressionString) throws ParseException; /** * 解析表达式字符串并且返回一个可重复获取返回值的表达式对象,这个方法传递一个Context对象 * Context对象为表达式对象提供输入 */ Expression parseExpression(String expressionString, ParserContext context) throws ParseException; }

通过一个例子了解解析字符串字面量(string literal),比如表达式'Hello World',程序执行完输出字符串Hello World,这个字符串需要使用单引号括起来,先看看测试程序,后续再了解字符串字面量的语法,代码如下:

ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("'Hello World'"); String message = (String) exp.getValue(); System.out.printf(message); //输出:Hello World

如果不使用单引号,程序则抛出SpelParseException异常,从异常信息看是解析完表达式后面还有数据导致,主要是字符串字面量中有空格导致,如果字符串改变成'Hello_World'则会正常解析,测试结果如下:

Expression exp = parser.parseExpression("Hello World"); //结果如下: Exception in thread "main" org.springframework.expression.spel.SpelParseException: EL1041E: After parsing a valid expression, there is still more data in the expression: 'World' at org.springframework.expression.spel.standard.InternalSpelExpressionParser.doParseExpression(InternalSpelExpressionParser.java:141) at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:61) at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:33) at org.springframework.expression.common.TemplateAwareExpressionParser.parseExpression(TemplateAwareExpressionParser.java:52) at org.springframework.expression.common.TemplateAwareExpressionParser.parseExpression(TemplateAwareExpressionParser.java:43) at antlr4.TestSPEL.main(TestSPEL.java:12)

再简单分析类Expression,这个类会根据上下文对自身求值,并封装了求值的公共方法。常用的方法是getValue(),这个方法有多个重载的版本,如果没有传参desiredResultType(预期的结果类型),怎需要对结果类型进行转换,如果类型不能转换则抛出SpelEvaluationException异常。

@Nullable Object getValue() throws EvaluationException; @Nullable <T> T getValue(@Nullable Class<T> desiredResultType) throws EvaluationException; @Nullable Object getValue(@Nullable Object rootObject) throws EvaluationException; @Nullable <T> T getValue(@Nullable Object rootObject, @Nullable Class<T> desiredResultType) throws EvaluationException; @Nullable Object getValue(EvaluationContext context) throws EvaluationException; @Nullable Object getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; @Nullable <T> T getValue(EvaluationContext context, @Nullable Class<T> desiredResultType) throws EvaluationException; @Nullable <T> T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class<T> desiredResultType) throws EvaluationException;

最后了解一下接口EvaluationContext,这个接口用来解析属性、方法、字段,并帮助执行类型转换。在类定义了很多get方法,例如获取rootObject,MethodResolvers,BeanResolver等,并且有个setVariable方法,用于设置变量。这个接口的实现类有三个StandardEvaluationContext、SimpleEvaluationContext和MethodBasedEvaluationContext。

使用Spring的表达式接口来表达式求值

表达式求值

1、字面量表达式,支类型包括字符串、数值(int、real、十六进制)、布尔值和null。字符串需要使用单引号括起来,如果字符中有单引号需要使用两个单引号。下面是几个例子:

// 这个Hello 'World中带有一个单引号 String helloWorld = (String) parser.parseExpression("'Hello ''World'").getValue(); // 输出:Hello 'World double avogadrosNumber = (Double) parser.parseExpression("3.0221415E+23").getValue(); // 输出:3.0221415E23 int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue(); // 输出:2147483647 boolean falseValue = (Boolean) parser.parseExpression("false").getValue(); // 输出:false Object nullValue = parser.parseExpression("null").getValue(); // 输出:null

2、获取对象属性,数组,列表,映射,索引:首先定义一个测试类,类中有数组、列表和映射(Map)等

class SpringTestObject { public String[] item = new String[]{"苹果", "香蕉", "梨", "西瓜"}; public List<String> list = Arrays.asList("矿泉水", "雪碧", "可乐", "牛奶"); public Map<String,String> map = new HashMap() { { put("man", "男"); put("woman", "女"); put("other", "中性"); } }; public String property = "测试类"; public String getListItem(int index){ return list.get(index); } }

首先测试获取对象的属性,测试前需要简单介绍下rootObject,从名字理解就是根对象,Spring会从这个对象中读取属性或者调用对象的方法。

ExpressionParser parser = new SpelExpressionParser(); SpringTestObject rootObj = new SpringTestObject(); //调用方法getListItem参数是1 Object value = parser.parseExpression("getListItem(1)").getValue(rootObj); System.out.println(""+value); // 输出结果:雪碧 String property = (String) parser.parseExpression("property").getValue(rootObj); System.out.println(property); // 输出结果:测试类

获取数组中的值和设置新的值,设置使用setValue()方法,第一个参数是rootObject,第二个参数是新的值。

ExpressionParser parser = new SpelExpressionParser(); SpringTestObject rootObj = new SpringTestObject(); // 获取item数组中的第二个值 String item = (String) parser.parseExpression("item[1]").getValue(rootObj); System.out.println(item); // 输出结果:香蕉 // 重新设置数组中第二个值为榴莲 parser.parseExpression("item[1]").setValue(rootObj,"榴莲"); System.out.println(rootObj.item[1]); // 输出:榴莲

列表,映射和数组是类似的就不单独解释了,示例如下:

ExpressionParser parser = new SpelExpressionParser(); SpringTestObject rootObj = new SpringTestObject(); // 获取item列表中的第二个值 String list = (String) parser.parseExpression("list[1]").getValue(rootObj); System.out.println(list); // 输出结果:雪碧 // 重新设置数组中第二个值为脉动 parser.parseExpression("list[1]").setValue(rootObj,"脉动"); System.out.println(""+rootObj.list.get(1)); // 输出结果:脉动 String map = (String) parser.parseExpression("map['man']").getValue(rootObj); System.out.println(map); // 输出结果:男 parser.parseExpression("map['man']").setValue(rootObj,"男人"); System.out.println(""+rootObj.map.get("man")); // 输出结果:男人

3、创建数组、映射和列表:创建映射和列表使用大括号开始和结尾,映射使用key:value形式,列表使用逗号分隔,他们都可以嵌套。

//Map对象 Map map = (Map) parser.parseExpression("{'key':'value'}").getValue(); //列表 List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(); //嵌套列表 List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(); //一维数组 int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(); //二维数组 int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue();

4、方法调用:对于字符串可以直接调用字符串的方法,比如length()、size()方法等,对于rootObject可以直接调用它的方法。

ExpressionParser parser = new SpelExpressionParser(); SpringTestObject rootObject = new SpringTestObject(); Integer length = (Integer) parser.parseExpression("'hello'.length()").getValue(); String property = (String) parser.parseExpression("getListItem(0)").getValue(rootObject);

另一种方法调用,注册一个方法到StandardEvaluationContext上,registerFunction接受两个参数,第一个是方法名,第二个是Method对象,这种方法调用支持静态方法,表达式需要再前面加个#。

//首先把getListItem改成静态方法 public static String getListItem(int index){ return list.get(index); } ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.registerFunction("getListItem", SpringTestObject.class.getDeclaredMethod("getListItem", new Class[] { int.class })); String item = parser.parseExpression("#getListItem(1)").getValue(context,String.class);

5、关系运算、逻辑运算和算术运算:关系运算符有lt ('<'), gt ('>'), le ('<='), ge ('>='), eq ('=='), ne ('!='), div ('/'), mod ('%'), not ('!');逻辑运算符有and, or和not;算术运算有加减乘除和求余等。

ExpressionParser parser = new SpelExpressionParser(); Boolean falseValue = parser.parseExpression("1 > 3").getValue(Boolean.class); Boolean trueValue1 = parser.parseExpression("10 gt null and 6 < 10").getValue(Boolean.class); Integer intValue = parser.parseExpression("10 - 2").getValue(Integer.class); //正则表达式 boolean trueValue = parser.parseExpression("'5.00' matches '^-?\\d+(\\.\\d{2})?#39;").getValue(Boolean.class);

对于和'null'比较,null代表设么也没有,其他和null大于比较总返回true。

6、获取类型和调用构造方法:获取类型使用T操作符,这个操作符也可以调用静态方法。对于在java.lang包中的类可以忽略包名,比如T(int),对于其他包需要使用全名T(java.util.Date)。

ExpressionParser parser = new SpelExpressionParser(); Class stringClass = parser.parseExpression("T(String)").getValue(Class.class); // 输出:class java.lang.String Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class); // 输出:class java.util.Date String strValue = parser.parseExpression("T(antlr4.SpringTestObject).getListItem(1)").getValue(String.class); //输出:雪碧 String hw = parser.parseExpression("new String('Hello World')").getValue(String.class); //输出:Hello World

7、Bean引用:需要在StandardEvaluationContext上设置BeanResolver的实例,BeanResolver意识就是从什么地方获取bean,在Spring项目上可以注入BeanFactory,在非Spring项目中可以自定义一个BeanResolver的实现。表达式中使用@字符获取bean。以下是一个Spring项目中获取bean的方式。

StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new BeanFactoryResolver(beanFactory)); List list = parser.parseExpression("@shoppingCart.getItems()").getValue(context,List.class);

自定义MyBeanResolver实现接口BeanResolver,并实现resolve方法。

class MyBeanResolver implements BeanResolver{ Map<String,Object> beans = new HashMap(){{ put("test",new SpringTestObject()); }}; @Override public Object resolve(EvaluationContext context, String beanName) throws AccessException { return beans.get(beanName); } } //使用自定义的MyBeanResolver StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new MyBeanResolver()); String invokeBeanMethod = parser.parseExpression("@test.getListItem(2)").getValue(context, String.class); // 输出:可乐

8、三元运算符和Elvis运算符:学过编程的都熟悉三元运算符,因为在很多语言中都存在,三元运算符使用类似if-then-else条件逻辑计算,在表达式中类似这样的"true ? 'trueExp' : 'falseExp'"。

Boolean value = parser.parseExpression("3 gt 1? 'true':'false'").getValue(Boolean.class); //输出:true

Elvis运算符在Groovy中使用,为了简化三元运算符,语法为'notnull'?:null

String name = parser.parseExpression("'notnull'?:'null'").getValue(String.class); //输出:notnull

9、安全的导航操作符:从一个对象中获取另一个对象并使用它属性的时候,为了防止NullPointerException会对每个对象判空,在Groovy语言中可以使用安全的导航避免空指针,语法如下:obj1?.obj2?.field。在Spring表达式中可以使用类似的语法。

StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new MyBeanResolver()); String invokeBeanMethod = parser.parseExpression("@notTest?.getListItem(2)").getValue(context, String.class); // notTest并不存在,结果为null,并不会抛出SpelEvaluationException

10、集合选择:从集合中选择一部分数据是比较常用的,在Java中可以循环对集合每个元素处理,在Spring 表达式中可以使用语法?[selectionExpression]获取全部符合条件的数据,使用^[selectionExpression]获取第一个元素,使用$[selectionExpression]获取最后一个元素。

ExpressionParser parser = new SpelExpressionParser(); List list = parser.parseExpression("{1,2,3,4,5,6,7,8,9}.?[#this>3]").getValue(List.class); // [4, 5, 6, 7, 8, 9] List firstlist = parser.parseExpression("{1,2,3,4,5,6,7,8,9}.^[#this>3]").getValue(List.class); // [4] List lasttlist = parser.parseExpression("{1,2,3,4,5,6,7,8,9}.$[#this>3]").getValue(List.class); // [9]

11、表达式模板:在一个字符串中可以使用#{ }在文本中使用表达式,在解析的方法中需要传递TemplateParserContext。

String randomPhrase = parser.parseExpression("random number is #{T(java.lang.Math).random()}", new TemplateParserContext()).getValue(String.class)

Spring SpEL的原理

在spring-expression-5.3.26.jar包中有个SpringExpresions.g文件,这个文件就是SpEL的语法文件,通过语法文件解析输入的表达式生成语法树,在语法正确的前提下计算表达式的值。

spring的语法文件

在类InternalSpelExpressionParser中处理输入的表达式,处理成TokenStream,最后生成一颗编译的语法树。

解析表达式

[注:本文部分图片来自互联网!未经授权,不得转载!每天跟着我们读更多的书]


互推传媒文章转载自第三方或本站原创生产,如需转载,请联系版权方授权,如有内容如侵犯了你的权益,请联系我们进行删除!

如若转载,请注明出处:http://www.hfwlcm.com/info/187405.html