6.3 使用SpEL的接口进行表达式求值

优质
小牛编辑
123浏览
2023-12-01

6.3 使用SpEL的接口进行表达式求值

本节介绍SpEL接口及其表达式语言的简单使用方法。完整的语言文档见:Language Reference

下面代码介绍了使用SpEL API来解析字符串表达式’Hello World’的示例

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'");
String message = (String) exp.getValue();

最常用的SpEL类和接口都放在包org.springframework.expression及其子包和spel.support下

接口ExpressionParser用来解析一个字符串表达式。在这个例子中字符串表达式即用单引号括起来的字符串.接口Expression用于对上面定义的字符串表达式求值。 调用parser.parseExpression和exp.getValue分别可能抛出ParseException和EvaluationException。

SpEL支持一系列广泛的特性,例如方法调用,访问属性,调用构造函数等。

下面举一个方法调用的例子,在String文本后面调用concat方法。

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();

message的值现在就是 ‘Hello World!’.

接下去是一个访问JavaBean属性的例子,String类的Bytes属性通过以下的方法调用

// 调用'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes");
byte[] bytes = (byte[]) exp.getValue();

SpEL同时也支持级联属性调用、和标准的prop1.prop2.prop3方式是一样的;同样属性值设置也是类似的方式.

公共方法也可以被访问到:

ExpressionParser parser = new SpelExpressionParser();

// 调用 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length");
int length = (Integer) exp.getValue();

除了使用字符串表达式、也可以调用String的构造函数:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()");
String message = exp.getValue(String.class);

针对泛型方法的使用,例如:public <T> T getValue(Class<T> desiredResultType) 使用这样的方法不需要将表达式的值转换成具体的结果类型。如果具体的值类型或者使用类型转换器都无法转成对应的类型、会抛EvaluationException的异常

SpEL中更常见的用途是提供一个针对特定对象实例(叫做根对象)求值的表达式字符串。使用方法有两种,
具体用哪一种要看每次调用表达式求值时相应的对象实例是否每次都会变化。接下来我们分别举两个例子说明,
第一个例子我们要做的是从Inventor类的实例中解析name属性:

// 创建并设置一个calendar实例
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// 构造器参数有: name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");

EvaluationContext context = new StandardEvaluationContext(tesla);
String name = (String) exp.getValue(context);

最后一行,字符串变量name将会被设置成”Nikola Tesla”. 通过StandardEvaluationContext类你能指定哪个对象的”name”会被求值这种机制用于当根对象不会被改变的场景,在求值上下文中只会被设置一次。相反,如果根对象会经常改变,则需要在每次调用getValue的时候被设置,就像如下的示例代码:

// 创建并设置一个calendar实例
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// 构造器参数有: name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");
String name = (String) exp.getValue(tesla);

在上面这个例子中inventor类实例tesla直接在getValue中设置,表达式解析器底层框架会自动创建和管理一个默认的求值上下文-不需要被显式声明

StandardEvaluationContext创建相对比较耗资源,在重复多次使用的场景下内部会缓存部分中间状态加快后续的表达式求值效率。因此建议在使用过程中尽可能被缓存和重用,而不是每次在表达式求值时都重新创建一个对象。

在很多使用场景下理想的方式是事先配置好求值上下文,但是在实际调用中仍然可以给getValue设置一个不同的根对象。getValue允许在同一个调用中同时指定两者,在这种场景下运行时传入的根对象会覆盖在求值上下文中事先指定的根对象。

Note:在单独使用SpEL时需要创建解析器、解析表达式、以及求值上下文和对应的根对象。但是在实际使用过程中、更常用的使用方式是只需要在配置文件里面配置SpEL字符串表达式即可,例如针对Spring Bean或者Spring Web Flow的定义。在这种场景下解析器,求值上下文,根对象和任何事先定义的变量都会被容器默认创建好,用户除了写表达式不需要做任何其他事情。

作为最后一个介绍性的例子,我们沿用上面的例子写一个使用布尔操作符的求值例子

Expression exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(context, Boolean.class); // 求值结果是True

6.3.1 EvaluationContext接口

EvaluationContext接口在求值表达式中需要解析属性,方法,字段的值以及类型转换中会用到。其默认实现类StandardEvaluationContext使用反射机制来操作对象。为获得更好的性能缓存了java.lang.reflect.Method,java.lang.reflect.Field, 和java.lang.reflect.Constructor实例

StandardEvaluationContext中你可以使用setRootObject()方法显式设置根对象,或通过构造器直接传入根对象。你还可以通过调用setVariable()和registerFunction()方法指定在表达式中用到的变量和函数。变量和函数的使用在语言参考的 Variables和Functions两章节有详细说明。使用StandardEvaluationContext你还可以注册自定义的构造器解析器(ConstructorResolvers),方法解析器(MethodResolvers),和属性存取器(PropertyAccessor)来扩展SpEL如何计算表达式,详见具体类的JavaDoc文档。

类型转换

SpEL默认使用Spring核心代码中的conversion service来做类型转换(org.springframework.core.convert.ConversionService)。这个类本身内置了很多常用的转换器,同时也可以扩展使用自定义的类型转换器。另外一个核心功能是它可以识别泛型。这意味着当在表达式中使用泛型类型时、SpEL会确保任何处理的对象的类型正确性。

实际应用中这意味着什么?这里举一个例子、拿赋值来说,比如使用setValue来设置List属性。属性的类型实际上是List<Boolean>,SpEL可以识别List中的元素类型并转换成Boolean类型。下面是示例代码:

class Simple {
    public List<Boolean> booleanList = new ArrayList<Boolean>();
}

Simple simple = new Simple();

simple.booleanList.add(true);

StandardEvaluationContext simpleContext = new StandardEvaluationContext(simple);

// false is passed in here as a string. SpEL and the conversion service will
// correctly recognize that it needs to be a Boolean and convert it
parser.parseExpression("booleanList[0]").setValue(simpleContext, "false");

// b will be false
Boolean b = simple.booleanList.get(0);

6.3.2 解析器配置

可以通过使用一个解析器配置对象 (org.springframework.expression.spel.SpelParserConfiguration)来配置SpEL表达式解析器。这个配置对象可以控制一些表达式组件的行为。例如:数组或者集合元素查找的时候如果当前位置对应的对象是Null,可以通过事先配置来自动创建元素。这个在表达式是由多次属性链式引用的时候比较重要。如果设置的数组或者List位置越界时可以自动增加数组或者List长度来兼容.

class Demo {
    public List<String> list;
}

// Turn on:
// - auto null reference initialization
// - auto collection growing
SpelParserConfiguration config = new SpelParserConfiguration(true,true);

ExpressionParser parser = new SpelExpressionParser(config);

Expression expression = parser.parseExpression("list[3]");

Demo demo = new Demo();

Object o = expression.getValue(demo);

// demo.list will now be a real collection of 4 entries
// Each entry is a new empty String

SpEL表达式编译器的行为也可以通过配置来实现

6.3.3 SpEL编译

Spring4.1 包括了一个基本的表达式编译器。表达式求值过程中往往可以利用动态解释功能来提供很多灵活性,但是却达不到理想的性能。当表达式不是被经常使用时是可以接受的,但是当被其他组件像Spring Integration使用时,性能往往更重要,这时动态性没有太多的需求。

新的SpEL编译器针对这样的需求进行了定制。编译器会在求值时同时创建一个真实的Java类包含表达式的行为,利用这种方式来达到更快的表达式求值速度。在编译过程中、编译器刚开始不知道表达式具体的类型、但它会在表达式求值过程中收集相应的信息来确定最终的类型。例如,编译器无法仅仅从表达式自身来分析出某个属性引用的类型,但是在首次进行解释求值时就可以确定了。当然,如果不同的表达式元素类型后续发生变化、基于这种信息来编译结果就不准确了。因此编译最适合的场景是表达式在多次求值过程中类型信息不会变化。

例如一个简单的表达式如下:

someArray[0].someProperty.someOtherProperty < 0.1

这个表达式包含数组访问,属性引用和数值运算,其性能开销不可忽视。
在一次50000次循环的性能基准评测中,如果只用解析器需要耗时75毫秒,但如果使用已编译的版本则只需要3毫秒

编译器配置

编译器模式不是默认打开的,有两种方法可以打开。一种是前面已经提到过的解析器配置的时候,另一种是当SpEL集成到其他组件时通过设置系统属性的方式打开。本节会同时介绍两种方式.

首先比较重要的是编译器本身有几种操作模式,都定义在枚举类(org.springframework.expression.spel.SpelCompilerMode)中。所有的模式如下:

OFF-编译器关闭;默认是关闭的
IMMEDIATE-即时生效模式,表达式会尽快的被编译。基本是在第一次求值后马上就会执行。如果编译表达式出错(往往都是因为上面提到的类型发生改变的情况)则表达式求值的调用点会抛出异常。
MIXED-混合模式,在混合模式中表达式会自动在解释器模式和编译器模式之间切换。在发生了几次解释处理后会切换到编译模式,如果编译模式哪里出错了(像上面提到的类型发生变化)则表达式会自动切换回解释器模式。过一段时间如果运用正常又会切换回编译模式。基本上像在IMMEDIATE模式下会抛出的那些异常都会被内部处理掉。

即时生效模式之所以会存在是因为混合模式会带来副作用。如果一个已编译的表达式在部分执行成功后发生错误的话,有可能已经导致系统的状态发生变化。这种场景下调用方并不希望表达式在解释模式下重新执行一遍、因为这意味着部分表达式会被执行两遍。

在选择了一个模式后,使用SpelParserConfiguration 来配置解释器:

SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
    this.getClass().getClassLoader());

SpelExpressionParser parser = new SpelExpressionParser(config);

Expression expr = parser.parseExpression("payload");

MyMessage message = new MyMessage();

Object payload = expr.getValue(message);

指定编译器类型时也可以同时指定类加载器(不指定传入Null也是允许的)。已编译的表达式将会被定义在所有被创建的子类加载器中。比较重要的一点是一旦指定了一个类加载器、需要确保表达式求值过程中所有涉及的类型对它都是可见的。如果没有明确指定则会使用缺省的类加载器(一般是当前正在执行表达式求值的线程所关联的上下文类加载器)

第二种方法是当SpEL内嵌到其他组件时仅通过配置对象不太容易配置实现的时候使用。在这种场景下往往使用系统属性来设置。属性spring.expression.compiler.mode可以被设置成SpelCompilerMode枚举值的其中一种(off, immediate, 或者 mixed)。

编译器的局限性

在Srping4.1中基础的编译框架就已经有了。但框架还不支持编译所有类型的表达式。最初的设计初衷主要在于先集中优化比较耗性能且又经常使用的表达式。以下类型的表达式目前还不能被编译:

涉及到赋值的表达式
依赖于转换服务的表达式
使用到自定义解析器或者存取器的表达式
使用到选择器或者投影的表达式

更多类型的表达式将来都会被支持编译