Spring表达式语言(SpEL)

原创
2017/08/24 19:02
阅读数 934

1 简介

Spring表达式语言(简称SpEL)是强大的表达式语言,它支持在运行时查询和操纵对象图表。语法与统一EL相似但是提供了额外的功能,最引人注目的是方法调用和基本字符串模版功能。

尽管有另外几种Java表达式语言可用,例如OGNL、MVEL和JBoss EL,Spring表达式语言被创建用于提供给Spring社区一个单一的支持良好的表达式语言,它可以用于Spring portfolio中的所用产品。它的语言特性被Spring portfolio中的项目的需求驱动,包括基于Eclipse的Spring Tool Suite中的代码补齐支持的工具需求。也就是说,SpEL是基于技术不可知的API,允许在需要时集成其他表达式语言实现。

尽管SpEL作为Spring portfolio中表达式求值的基础,它不直接与Spring绑定并且可以被独立使用。为了自持,本章中的许多例子使用SpEL,就像它是一种独立的表达式语言。这需要创建一些启动基础设施类例如解析器。大多数Spring用户不需要处理这种基础设施,而只会编写表达式字符串用于求值。这个典型应用的一个例子是集成SpEL创建基于XML或者注解的bean定义,这会在“创建bean定义的表达式支持”一节中展示。

本章覆盖了表达式语言的特性、API和语法。在一些地方,Inventor和Inventor的Society类被用于表达式求值的目标对象。这些类的声明和用来填充它们的数据在本章的最后被列出。

2 特性概览

表达式语言支持下面的功能:

  • 字面表达式;
  • Boolean和相关的运算符;
  • 正则表达式;
  • 类表达式;
  • 访问属性、数组、列表和映射;
  • 方法调用;
  • 关系运算符;
  • 赋值;
  • 调用构造函数;
  • Bean引用;
  • 构造数组;
  • 內联列表;
  • 內联映射;
  • 三元运算符;
  • 变量;
  • 用户定义的函数;
  • 列表投影;
  • 列表选择;
  • 模板表达式。

3 使用Spring的Expression接口对表达式求值

本节介绍SpEL接口和表达式语言的简单实用方法。完整的语言参考可以在“语言参考”一节中找到。

下面的代码介绍SpEL API如何对字面字符串表达式'Hello World'求值。

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

消息变量的值是'Hello World'。

最可能使用的SpEL类和接口位于org.springframework.expression包和它的子包以及spel.support包中。

ExpressionParser接口用于解析一个表达式字符串。在这个例子中,表达式字符串是包围着单引号的一个字符串文字。Expression接口用于对预先定义好的表达式字符串求值。调用parser.parseExpression和exp.getValue可能会分别抛出ParseException和EvaluationException。

SpEL支持许多特性,例如调用方法、访问属性和调用构造函数。

作为调用方法的一个例子,我们在字符串字面值上调用concat方法。

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

消息的值现在是"Hello World!"。

作为访问JavaBean属性的一个例子,字符串的bytes属性可以如下调用:

ExpressionParser parser = new SpelExpressionParser();

// 调用 '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)的用法。使用这个方法避免了转换表达式的值到期望类型的需要。如果值不能被转换为T或者不能使用注册的类型转换器转换,将抛出EvaluationException。

SpEL更普遍的用法是提供表达式字符串用于对特定的对象实体(称为根对象)求值。这里有两种方法,要选择哪个取决于表达式求值的对象是否会随每次调用而更改。在下面的例子中,我们获取Inventor类的对象的name属性。

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

// 构造函数的参数是name, birthday和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和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允许在同一个调用中指定两者。在这些情况下,调用时被传递的根对象会覆盖任何求值上下文指定的对象。(就是使用getValue(EvaluationContext context, Object rootObject)方法)

在独立使用SpEL时,需要创建解析器,解析表达式并且提供求值上下文和根上下文对象。然而,更普遍的使用场景是仅提供SpEL表达式字符串作为配置文件的一部分,例如用于Spring bean或Spring Web流定义。在这种情况下,求值上下文,根对象和任何预定义的变量被隐式创建,不需要用户指定除表达式外的任何东西。

作为最后一个例子,用上一个例子中的Inventor对象展示布尔运算符的使用。

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

3.1 EvaluationContext接口

当对一个表达式求值时使用EvaluationContext接口来解析属性、方法、字段和帮助执行类型转换。开箱即用的实现StandardEvaluationContext,使用反射来操作对象,通过缓存java.lang.reflect.Method、java.lang.reflect.Field和java.lang.reflect.Constructor接口来提高效率。

StandardEvaluationContext时通过setRootObject()方法或者传递给构造函数来参数来指定根对象的地方。也可以通过setVariable()和registerFunction()方法指定将会在表达式中使用的变量和函数。变量和函数的使用在语言参考的变量和函数一节讨论。也可以通过StandardEvaluationContext注册自定义的ConstructorResolver、MethodResolver和PropertyAccessor来扩展SpEL求值表达式的行为。参考这些类的javadoc来获取详细信息。

类型转换

默认的,SpEL使用Spring核心中可用的转换服务(org.springframework.core.convert.ConversionService)。这个转换服务包含许多内建的用于常见类型的转换器,但是也完全可扩展的以便添加两种类型间的自定义转换。另外它有感知范型的关键能力。这意味着当在表达式中使用范型类型,SpEL会尝试转换以维护遇到的任何对象的类型正确性。

在实践中这意味着什么?假设使用setValue()给一个List属性赋值。属性的类型实际上时List<Boolean>。SpEL将会识别到这个列表的元素需要在被添加到其中前转换为布尔类型。一个简单的例子:

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

Simple simple = new Simple();

simple.booleanList.add(true);

StandardEvaluationContext simpleContext = new StandardEvaluationContext(simple);

// 此处false作为字符串被传入。 

//SpEL和转换服务将会正确的识别到这里需要一个Boolean类型并且转换它。

parser.parseExpression("booleanList[0]").setValue(simpleContext, "false");

// b 将会为false
Boolean b = simple.booleanList.get(0);

3.2 解析器配置

可以使用一个解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration)配置SpEL表达式的解析器。配置对象控制一些表达式组件的行为呢。例如,如果在数组或集合中检索并且指定索引的元素为null,可以自动创建元素。当使用的表达式由属性引用的链构成,这是非常有用的。如果在数组或集合中检索并且指定索引超出了数组或集合现在的尺寸,可以自动增长尺寸来容纳索引。

class Demo {
    public List<String> list;
}

// 打开:
// - null应用自动初始化
// - 集合自动扩容
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现在是一个有四个元素的集合
// 每个元素是一个空字符串

也可以配置SpEL表达式编译器的行为。

3.3 SpEL 编译

Spring Framework 4.1包含了基础的表达式编译器。表达式通常被视为在求值过程中提供了大量的动态灵活性,但不能提供最佳性能。对于临时的表达式使用是良好的,但是当被像Spring Integration这样的其他组件使用,性能是非常重要的并且没有动态的实际需要。

新的SpEL编译器用于满足这种需求。编译器将会在求值期间即时生成一个真正的Java类,它体现了表达式的行文并使用它来实现更快的表达式求值。由于缺少表达式中的类型信息,编译器在执行编译时使用在表达式的解释性评估期间收集的信息。例如,它无法从表达式中获得一个属性引用的 类型,但是在第一次解释性求值过程中会发现它时什么。当然,如果各种表达式元素的类型随着时间的推移而变化,那么基于这些信息的编译可能在稍后导致麻烦。因此,编译最适合于重复糗事时类型信息不会改变的表达式。

对于一个如下的基本表达式:

someArray[0].someProperty.someOtherProperty < 0.1

包含了数组的方法,一些属性的引用和数值操作,性能的增益可以非常明显。在50000次迭代的微型基准运行实例中,使用解释器需要花费75ms而使用编译版本的表达式只需要3ms。

编译器配置

编译器默认不会被打开,但是有两种方法打开它。使用上面介绍的解析器配置处理或者当在另外一个组件中嵌入使用SpEL时通过系统属性。这一节讨论这两种方法。

明白编译器可以有几种运行模式非常重要,模式在一个枚举中(org.springframework.expression.spel.SpelCompilerMode)。模式如下:

  • OFF 编译器被关闭,这是默认的配置;
  • IMMEDIATE 在即时模式中表达式将尽快的被编译。一般是在第一次解释性求值之后。如果编译过的表达式失败(一般是由于类型改变,如上所述),那么表达式求值的调用会收到一个异常;
  • MIXED 在混合模式中,表达式在解释和编译模式之间静默的切换。在几次解析模式运行后会切换到编译模式,并且如果编译模式出现了错误(例如类型发生改变,如上所述)那么表达式会自动切换回解释模式。稍后,它可能生成另一个编译形式并切换到它。基本上,用户进入即时模式的异常被内部处理。

IMMEDIATE模式存在是因为MIXED模式的副作用可能导致表达式的问题。如果一个编译的表达式在部分成功后失败,完成的部分可能已经对系统状态产生影响。如果发生这种情况,调用者不希望它在解释模式下静默的重新运行,因为表达式的一部分可能会运行两次。

在选定一个模式后,使用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.complier.mode可以被设定为SpelCompilerMode枚举值中的一种(off,immediate或者mixed)。

编译器的局限

Spring Framework 4.1引入了基本编译框架。然而,框架还没有支持编译每一种表达式。最初的重点是可能在性能关键环境中使用的常见表达式。下面这些种类的表达式现在无法被编译:

  • 包含赋值的表达式;
  • 依赖转换服务的表达式;
  • 使用自定义解析器或者存取器的表达式;
  • 使用selection或projection的表达式(???)。

更多类型的表达式在将来会被编译。

4 bean定义的表达式支持

SpEL表达式可以与XML或基于注解的配置元数据一起使用来定义BeanDefinition。在这两种情况下,定义表达式的语法格式为#{<表达式字符串>}。

4.1 基于XML的配置

属性或者构造函数参数值可以使用表达式设置如下:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- 其他属性 -->
</bean>

systemProperties变量被预定义,所以像下面这样在表达式中使用。注意在此上下文中,不需要使用#符号作为预定义变量的前缀。

<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
    <property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>

    <!-- 其他属性 -->
</bean>

也可以使用name引用其他bean属性,例如:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- 其他属性 -->
</bean>

<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
    <property name="initialShapeSeed" value="#{ numberGuess.randomNumber }"/>

    <!-- 其他属性 -->
</bean>

4.2 基于注解的配置

@value注解可以注释字段、方法和方法/构造函数参数来指定默认值。

这里是给字段变量设置默认值的一个例子。

public static class FieldValueTestBean

    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;

    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }

}

等价的使用属性setter方法的设置如下:

public static class PropertyValueTestBean

    private String defaultLocale;

    @Value("#{ systemProperties['user.region'] }")
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    public String getDefaultLocale() {
        return this.defaultLocale;
    }

}

自动装配的方法和构造函数也可以使用@Value注解。

public class SimpleMovieLister {

    private MovieFinder movieFinder;
    private String defaultLocale;

    @Autowired
    public void configure(MovieFinder movieFinder,
            @Value("#{ systemProperties['user.region'] }") String defaultLocale) {
        this.movieFinder = movieFinder;
        this.defaultLocale = defaultLocale;
    }

    // ...
}
public class MovieRecommender {

    private String defaultLocale;

    private CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
            @Value("#{systemProperties['user.country']}") String defaultLocale) {
        this.customerPreferenceDao = customerPreferenceDao;
        this.defaultLocale = defaultLocale;
    }

    // ...
}

5 语言参考

5.1 字面表达式

字面表达式支持的类型有字符串、数值(整数、实数、十六进制)、布尔型和null。字符串由单引号分隔。要将一个单引号本身放在字符串中,使用两个单引号。

下面的列表展示字面表达式的简单使用方法。通常,它们不会像这样使用,而是作为更复杂表达式的一部分,例如在逻辑比较运算分的一侧使用文字。

ExpressionParser parser = new SpelExpressionParser();

// 求值为"Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();

double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue();

// 求值为 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();

boolean trueValue = (Boolean) parser.parseExpression("true").getValue();

Object nullValue = parser.parseExpression("null").getValue();

数值支持负号、指数符号和十进制点号的使用。默认的,使用Double.parseDouble()解析实数。

5.2 Properties、Arrays、Lists、Maps和Indexers

使用属性引用浏览是容易的:只需使用点好来指示嵌套的属性值。Inventor类的实例pupin和tesla,使用“例子中使用的类”一节列出的数据填充。为了“向下”浏览,获取Tesla的出生年份和Pupin的出生城市,使用一下的表达式。

// 求值为1856
int year = (Integer) parser.parseExpression("Birthdate.Year + 1900").getValue(context);

String city = (String) parser.parseExpression("placeOfBirth.City").getValue(context);

属性名的首字母不区分大小写。数组和列表的内容使用方括号符号获取。

ExpressionParser parser = new SpelExpressionParser();

// Inventions数组
StandardEvaluationContext teslaContext = new StandardEvaluationContext(tesla);

// 求值为 "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
        teslaContext, String.class);

// Members列表
StandardEvaluationContext societyContext = new StandardEvaluationContext(ieee);

// 求值为 "Nikola Tesla"
String name = parser.parseExpression("Members[0].Name").getValue(
        societyContext, String.class);

// 列表和数组 navigation
// 求值为"Wireless communication"
String invention = parser.parseExpression("Members[0].Inventions[6]").getValue(
        societyContext, String.class);

Map的内容通过在括号内指定文字的键值获得。在下面的例子中,因为Officers map的键是字符串,可以指定字符串字面值。

// Officer's Dictionary

Inventor pupin = parser.parseExpression("Officers['president']").getValue(
        societyContext, Inventor.class);

//  求值为 "Idvor"
String city = parser.parseExpression("Officers['president'].PlaceOfBirth.City").getValue(
        societyContext, String.class);

// 设置值
parser.parseExpression("Officers['advisors'][0].PlaceOfBirth.Country").setValue(
        societyContext, "Croatia");

5.3 內联列表

列表可以使用{}符号直接在表达式中表示。

// 求值为一个Java列表包含四个数值
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);

List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context);

{}自身表示一个空列表。出于性能的原因,如果列表本身完全由固定文字组成,则会创建一个常量列表来表示表达式,而不是在每次求值时构建一个新列表。

5.4 內联映射

映射也可以使用{key:value}符号直接在表达式中表示。

// 求值为一个包含两个键值对的Java映射
Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context);

Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context);

{:}自身表示一个空映射。出于性能的原因,如果映射本身完全由固定文字或其他关联的常熟结构(列表或映射)组成,则会创建一个常量列表来表示表达式,而不是在每次求值时构建一个新列表。引用映射键是可选的,上面的示例中没有使用引用的键。

5.5 数组构建

可以使用与Java相似的语法构建数组,可以选择提供初始化器以在构造的时间填充数组。

int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context);

// 使用初始化器构造数组
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);

// 多维数组
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);

现在在构造多维数组时不允许使用初始化器。

5.6 方法

使用典型的Java编程语法调用方法。也可以在字面值上调用方法,同时也支持变量。

// 字符串字面值, 求值为"bc"
String c = parser.parseExpression("'abc'.substring(2, 3)").getValue(String.class);

// 求值为 true
boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(
        societyContext, Boolean.class);

5.7 运算符

关系运算符

关系运算符:相等,不等,小于,小于等于,大于,大于等于支持使用标准运算符。

// 求值为 true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);

// 求值为 false
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);

// 求值为 true
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);

大于/小于与null比较遵循一个简单原则:null被认为是没有(即,不是0)。因此,任何其他值总是大于null(X > null总为true)并且没有任何其他值比null小(X < null总为false)。如果更倾向于使用数值比较,请避免基于数字的null比较而使用与0的比较(例如 X > 0 或 X < 0)。

除了标准关系操运算符SpEL支持instanceof和机遇正则表达式的matches运算符。

// 求值为false
boolean falseValue = parser.parseExpression(
        "'xyz' instanceof T(Integer)").getValue(Boolean.class);

// 求值为true
boolean trueValue = parser.parseExpression(
        "'5.00' matches '\^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

//求值为false
boolean falseValue = parser.parseExpression(
        "'5.0067' matches '\^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class);

小心使用基本类型,因为它们会立即被装箱为包裹类型,所以 1 instanceof T(int)求值为false而 1 instanceof T(Integer)求值为true。

每个符号运算符也可以被替换为纯粗的字母等价形式。这避免了使用的符号在嵌入表达式的文档类型中具有特殊含义的问题(例如,XML文档)。文本的等价形式如下:lt (<), gt (>), le (⇐), ge (>=), eq (==), ne (!=), div (/), mod (%), not (!)。它们是不区分大小写的。

逻辑运算符

逻辑运算符中支持与、或和非。它们的用法展示如下。

/ -- 与 --

// 求值为 false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);

// 求值为 true
String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- 或 --

// 求值为 true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);

// 求值为 true
String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')";
boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

// -- 非 --

// 求值为 false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);

// -- 与非 --
String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')";
boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class);

数学运算符

加运算符可以在数值和字符串使用。减法、乘法和除法只能在数值上使用。此外还支持求模(%)和指数幂(^)。执行标准运算符优先级。这些运算符展示如下:

// 加
int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2

String testString = parser.parseExpression(
        "'test' + ' ' + 'string'").getValue(String.class); // 'test string'

// 减
int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4

double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000

// 乘
int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6

double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0

// 除
int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2

double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0

// 模
int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3

int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1

// 运算符优先级
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21

5.8 赋值

使用赋值运算符设置属性。这通常通过调用setValue完成,但也可以在调用的getValue内部完成。

Inventor inventor = new Inventor();
StandardEvaluationContext inventorContext = new StandardEvaluationContext(inventor);

parser.parseExpression("Name").setValue(inventorContext, "Alexander Seovic2");

// alternatively

String aleks = parser.parseExpression(
        "Name = 'Alexandar Seovic'").getValue(inventorContext, String.class);

5.9 类型

特殊的T运算符可用于指定java.lang.Class(类型)的实例。也可以使用此运算符调用静态方法。StandardEvaluationContext使用TypeLocator查找类型,并且StandardTypeLocator(可以被替换)构建了对java.lang包的理解。这意味着T()指向java.lang包中的类型不需要使用完全限定符,但是所有其他类型的引用必须使用。

Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class);

Class stringClass = parser.parseExpression("T(String)").getValue(Class.class);

boolean trueValue = parser.parseExpression(
        "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR")
        .getValue(Boolean.class);

5.10 构造函数

使用new运算符可以调用构造函数。除了基本类型和字符串(int、float等)意外的其他类型必须使用完全限定的类名。

Inventor einstein = p.parseExpression(
        "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
        .getValue(Inventor.class);

//在列表的add方法中创建新的inventor实例
p.parseExpression(
        "Members.add(new org.spring.samples.spel.inventor.Inventor(
            'Albert Einstein', 'German'))").getValue(societyContext);

5.11 变量

使用#variableName语法在表达式中引用变量。使用StandardEvaluationContext上的setVairable方法设置变量。

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
StandardEvaluationContext context = new StandardEvaluationContext(tesla);
context.setVariable("newName", "Mike Tesla");

parser.parseExpression("Name = #newName").getValue(context);

System.out.println(tesla.getName()) // "Mike Tesla"

#this和#root变量

变量#this总是被定义并引用当前求值对象(针对哪个非限定引用被解析)。变量#root总是被定义并引用根上下文对象。尽管#this可能因为被求值的表达式组件而发生变化,但是#root总是引用根对象。

// 创建整数数组
List<Integer> primes = new ArrayList<Integer>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));

// 创建解析器并且设置变量primes为整型数组
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("primes",primes);

// list中的所有基本数值大于10 (u使用选择器 ?{...})
// 求值为 [11, 13, 17]
List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
        "#primes.?[#this>10]").getValue(context);

5.12 函数

可以通过注册可以在表达式字符串中调用的用户自定义函数扩展SpEL。使用StandardEvaluationContext的如下方法注册函数:

public void registerFunction(String name, Method m)

一个Java方法的引用提供函数的实现。例如,一个工具方法反转字符串的使用方法如下所示:

public abstract class StringUtils {

    public static String reverseString(String input) {
        StringBuilder backwards = new StringBuilder();
        for (int i = 0; i < input.length(); i++)
            backwards.append(input.charAt(input.length() - 1 - i));
        }
        return backwards.toString();
    }
}

然后使用求值上下文注册这个方法并且可以在表达式字符串中使用。

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();

context.registerFunction("reverseString",
    StringUtils.class.getDeclaredMethod("reverseString", new Class[] { String.class }));

String helloWorldReversed = parser.parseExpression(
    "#reverseString('hello')").getValue(context, String.class);

5.13 Bean引用

如果求值上下文被配置了一个bean极细气,可以在表达式中使用@符号查找bean。

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

//在求值过程中它会调用MyBeanResolver的resolve(context, "foo")方法
Object bean = parser.parseExpression("@foo").getValue(context);

为了得到工厂方法本身,bean名字需要添加&前缀而不是@。

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new MyBeanResolver());

// 在求值过程中它会调用MyBeanResolver的resolve(context, "foo")方法
Object bean = parser.parseExpression("&foo").getValue(context);

5.14 三元运算符(If-Then-Else)

可以在表达式中使用三元运算符表示if-then-else条件逻辑。一个小例子如下:

String falseString = parser.parseExpression(
        "false ? 'trueExp' : 'falseExp'").getValue(String.class);

在这种情况下,布尔值false会导致返回字符串值“falseExp“。一个更实际的例子如下:

parser.parseExpression("Name").setValue(societyContext, "IEEE");
societyContext.setVariable("queryName", "Nikola Tesla");

expression = "isMember(#queryName)? #queryName + ' is a member of the ' " +
        "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'";

String queryResultString = parser.parseExpression(expression)
        .getValue(societyContext, String.class);
// queryResultString = "Nikola Tesla is a member of the IEEE Society"

请查看下一节的Elvis运算符,它有更简短的三元运算符语法。

5.15 Elvis运算符

Elvis运算符缩短了三元操作符的语法,它在Groovy语言中使用。在三元运算符语法中,通常需要重复一个变量两次,例如:

String name = "Elvis Presley";
String displayName = name != null ? name : "Unknown";

作为替代可以使用Elvis运算符,因为与猫王的发型相似而得名。

ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("name?:'Unknown'").getValue(String.class);

System.out.println(name); // 'Unknown'

下面是一个更复杂的例子。

ExpressionParser parser = new SpelExpressionParser();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
StandardEvaluationContext context = new StandardEvaluationContext(tesla);

String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, String.class);

System.out.println(name); // Nikola Tesla

tesla.setName(null);

name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, String.class);

System.out.println(name); // Elvis Presley

5.16 安全的导航运算符

安全的导航运算符来自Groovy语言,用于避免NullPointerException。一般情况下,当你引用一个对象,你也许需要在访问对象的方法或者属性之前验证它是否为空。为了避免这个操作,安全导航运算符会简单的返回null替代抛出异常。

ExpressionParser parser = new SpelExpressionParser();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));

StandardEvaluationContext context = new StandardEvaluationContext(tesla);

String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, String.class);
System.out.println(city); // Smiljan

tesla.setPlaceOfBirth(null);

city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, String.class);

System.out.println(city); // null - 不会抛出NullPointerException!!!

Elvis运算符可以用于在表达式中设置默认值,例如一个一个@Value表达式中:@Value("#{systemProperties['pop3.port'] ?: 25}")。这将注入系统属性pop3.port(如果已定义)或25(如果不是)。

5.17 列表选择器

选择器是一个强大的表达式语言特性,它运行你通过选择源列表中的元素将源列表转换为另一个一个列表。

选择器使用语法.?[选择器表达式]。它将锅略列表并且返回一个包含原始元素子集的新列表。例如,选择器允许我们简单的获取一个塞尔维亚发明家列表:

List<Inventor> list = (List<Inventor>) parser.parseExpression(
        "Members.?[Nationality == 'Serbian']").getValue(societyContext);

列表和映射都可以使用选择器。在前一种情况下,根据每个单独列表元素求值选择标准;同时针对映射,根据每个映射键值对(Java Map.Entry类型)求值选择标准。

映射的键值对可以作为属性在选择器中访问。

下面的表达式会返回一个新的映射由原始映射中值小于27的键值对组成。

Map newMap = parser.parseExpression("map.?[value<27]").getValue();

除了返回所有选定的元素,还可以检索第一个或最后一个值。要获取与选择器匹配的第一个键值对语法为^[...];同时获取匹配的选择器的最后一条,语法为$[...]。

5.18 列表投影(???)

投影允许集合驱动子表达式的求值,结果是一个新的集合。投影的语法是![projectionExpression]。最容易理解的例子,假设我们有一个发明家的列表,但希望获取他们出生的城市的列表。高效的,我们对发明人列表中的每个条目的"placeOfBirth.city"求值。使用投影:

// 返回 ['Smiljan', 'Idvor' ]
List placesOfBirth = (List)parser.parseExpression("Members.![placeOfBirth.city]");

映射也可以被用于驱动投影并且在这种情况下会根据Map中的每个键值对对投影表达式求值。映射投影的结果是由对每个映射键值对的投影表达式的求值组成的列表。

5.19 表达式模板

表达式模板允许字面文本与一个或多个求值模块混合在一起。每个求值模块用你定义的前缀和后缀自负分隔,一个普遍的选择是使用#{ }作为分隔符。例如:

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

// 求值为 "random number is 0.7038186818312008"

字符串通过连接字面文本"random number is"与#{}分隔符中的表达式的求值结果进行求值,在这个例子中是调用random()方法的结果。传递给parseExpression()方法的第二个参数是ParserContext类型。ParserContext接口用于影响表达式如何解析以支持表达式模板功能。TemplateParserContext的定义如下:

public class TemplateParserContext implements ParserContext {

    public String getExpressionPrefix() {
        return "#{";
    }

    public String getExpressionSuffix() {
        return "}";
    }

    public boolean isTemplate() {
        return true;
    }
}

6 例子中使用的类

Inventor.java

package org.spring.samples.spel.inventor;

import java.util.Date;
import java.util.GregorianCalendar;

public class Inventor {

    private String name;
    private String nationality;
    private String[] inventions;
    private Date birthdate;
    private PlaceOfBirth placeOfBirth;

    public Inventor(String name, String nationality) {
        GregorianCalendar c= new GregorianCalendar();
        this.name = name;
        this.nationality = nationality;
        this.birthdate = c.getTime();
    }

    public Inventor(String name, Date birthdate, String nationality) {
        this.name = name;
        this.nationality = nationality;
        this.birthdate = birthdate;
    }

    public Inventor() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getNationality() {
        return nationality;
    }

    public void setNationality(String nationality) {
        this.nationality = nationality;
    }

    public Date getBirthdate() {
        return birthdate;
    }

    public void setBirthdate(Date birthdate) {
        this.birthdate = birthdate;
    }

    public PlaceOfBirth getPlaceOfBirth() {
        return placeOfBirth;
    }

    public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) {
        this.placeOfBirth = placeOfBirth;
    }

    public void setInventions(String[] inventions) {
        this.inventions = inventions;
    }

    public String[] getInventions() {
        return inventions;
    }
}

PlaceOfBirth.java

package org.spring.samples.spel.inventor;

public class PlaceOfBirth {

    private String city;
    private String country;

    public PlaceOfBirth(String city) {
        this.city=city;
    }

    public PlaceOfBirth(String city, String country) {
        this(city);
        this.country = country;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String s) {
        this.city = s;
    }

    public String getCountry() {
        return country;
    }

    public void setCountry(String country) {
        this.country = country;
    }

}

Society.java

package org.spring.samples.spel.inventor;

import java.util.*;

public class Society {

    private String name;

    public static String Advisors = "advisors";
    public static String President = "president";

    private List<Inventor> members = new ArrayList<Inventor>();
    private Map officers = new HashMap();

    public List getMembers() {
        return members;
    }

    public Map getOfficers() {
        return officers;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public boolean isMember(String name) {
        for (Inventor inventor : members) {
            if (inventor.getName().equals(name)) {
                return true;
            }
        }
        return false;
    }

}
展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部