CH22-SpEL表达式

简介

Spring表达式语言是一个支持查询和操作运行时对象图功能的强大的表达式语言。它的语法类似于传统 EL,但提供额外的功能,最出色的就是函数调用和字符串模板函数。

尽管有其他可选的 Java 表达式语言,如 OGNL, MVEL,JBoss EL 等等,但 Spel 创建的初衷是了给 Spring 社区提供一种简单而高效的表达式语言,一种可贯穿整个 Spring 产品组的语言。这种语言的特性应基于 Spring 产品的需求而设计。

应用过程

声明一个字符串值:

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

消息变量的值是简单的“hello world”。

接口ExpressionParser负责解析表达式字符串。这个字符串例子是通过单引号扩起来的一个字符串声明。

接口Expression负责解析之前被定义的字符串表达式。

调用字符串的“concat”方法:

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

获取 String 的“bytes”属性:

ExpressionParser parser = new SpelExpressionParser();

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

获取或设置嵌套属性:

ExpressionParser parser = new SpelExpressionParser();

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

构造字符串:

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比较常见的用途是针对一个特定的对象实例(称为root object)提供被解析的表达式字符串。

// Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// The constructor arguments are 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” 属性将被解析。如果root object不太可能改变. ,就可以简单地在评估上下文中设置一次。如果root object反复变化 ,它可以在每次调用getValue,如 接下来的例子说明:

/ Create and set a calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);

// The constructor arguments are 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提供不同的root object。 getValue允许既要在同一个调用中指定。在这些情况下对root object通过调用要考虑到覆盖任何(这可能为空)在解析范围内的指定。

在SpEL的独立使用的时候,需要创建parser,parse expressions, 同时可能需要提供解析的context和root context object。然而,更常见的 用法是只提供一个SpEL表达式字符串作为配置文件的一部分, 例如,对于Spring的bean或Spring Web Flow的定义。在这种情况下,解析器 求值的context,root object和所有预定义变量都设置了隐式, 没有什么要用户去指定了,除了声明表达式.

作为最后一个例子,使用了一个boolean运算符去调用 inventor object 在前面的例子中。

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

EvaluationContext 接口

当计算表达式解析properties, methods, fields,并帮助执行类型转换, 使用接口EvaluationContext 这是一个开箱即用的实现, StandardEvaluationContext,使用反射来操纵对象, 内部会缓存java.lang.reflectMethodField,和Constructor实例 提高性能。

StandardEvaluationContext是你可以指定root object通过使用 setRootObject()或传递root object到构造函数. 你也可以指定变量和函数使用方法的setVariable()registerFunction()的表达式。

StandardEvaluationContext也是在那里你可以自定义的注册 ConstructorResolvers, MethodResolvers, 和 PropertyAccessors 来扩展 SpEL 求值表达式的逻辑。

类型转换

默认情况下,SpEL使用Spring-core的转换服务( org.springframework.core.convert。ConversionService)。这种转换服务的许多转换器内置了常用的转换,但也完全可扩展类型之间特定转换。此外,它拥有的关键能力是泛型感知。这意味着,当与通用类型的工作表达式,SpEL将尝试转换他遇到的维持对任何对象类型的正确性

这做法是什么意思呢?假设分配,使用的 setValue(),正在使用 以设置一个 List属性。该属性的类型实际上是List<Boolean>。SpEL 将认识到,需要在列表中的元素之前,必须转换成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);

解析器配置

用一个parser configuration object去配置SpEL解析器是可能的, (org.springframework.expression.spel.SpelParserConfiguration)。配置对象控制的一些表达组件的行为。例如,如果数据为索引到指定索引处的数组或集合的元素是null 它可以自动地创建的元素。当用表达式组合一个链式属性引用时这将非常有用. 如果索引到一个数组或列表 并指定一个索引超出数组的当前大小或 自动增长的数组或队列去容纳

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表达式编译器的行为。

SpEl 编译

Spring Framework 4.1 包含了一个基本的表达式编译器. 表达式通常 解释其执行过程中提供了大量的动态灵活性,但 不提供最佳性能。对于偶尔使用的表达 这是好的,而是由其他组件,如Spring集成使用时, 性能是非常重要的,并没有为活力提供真正的需要。

新使用SpEL编译旨在解决这一需要。然后该 编译器将执行这体现了中动态生成一个真正的Java类 表达行为,并用它来实现更快的表达 式执行。由于缺乏各种表达式编译器 使用过程中的一个评估收集的评价的信息 当执行编译的表达。例如,它不知道的类型 参考表达,但在第一属性参考 解释执行会发现它是什么。当然,基于该 编译这些信息可能会造成的麻烦后,如果类型 各种表达元件随着时间而改变。出于这个原因汇编 是最适合返回执行不会改变其表达式类型的信息。

对于基本的表达是这样的:

someArray[0].someProperty.someOtherProperty < 0.1

其中涉及数组访问,部分属性引用和数字运算,性能 增益可以很明显的。在50000迭代一个例子微基准来看,它是 使用了75ms用来执行翻译,而仅仅3ms编译表达式的version。

编译器配置

编译器默认是并未开启的,但有两种方式打开 它。它被打开用parser configuration process 或者 通过系统属性将SpEL使用嵌入另一个组件中。本节 讨论这两个选项。

重要的是要明白,编译器可工作在几个模式下,查看详细可以用过一个enum (org.springframework.expression.spel.SpelCompilerMode). 模式如下:

  • OFF - 编译器被关闭;这是默认的。
  • IMMEDIATE - 在直接模式下,表达式尽快编制。 这是一个典型的首个编译选项。如果编译错误的表达式 (通常是由于一个类型变化,如上面所描述的)调用者将会得到一个异常。
  • MIXED - 在混合模式下,随着时间的推移,表达式默默地解释和编译之间切换。 经过解释运行的一些数字后,他们就会切换去编译源码 ,如果出现问题,编译形式(如一种变化,如 如上所述),那么表达式将自动切换回解释形式 。一段时间后,可能产生另一种形式的编制,并切换到它。基本上 相比用户IMMEDIATE模式,不同之处在于对于异常的处理,混合模式是隐式的(原话是: Basically the exception that the user gets in IMMEDIATE mode is instead handled internally。)。

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.compiler.mode可设置到SpelCompilerMode 枚举值(offimmediatemixed)之一。

编译器限制

随着Spring框架4.1的基本编制框架到位。然而,该框架 还没有支持编译每一种表情式。最初的重点一直是共同的表达 有可能在性能关键上下文中使用。这些种类的表达不能被编译 这些情况:

  • 涉及赋值表达式
  • 依托转换服务表达式
  • 使用自定义解析器或访问表达式
  • 使用选择或投影表达式

未来将支持越来越多类型的表达式。

定义bean的beandef表达支持

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

基于XML的配置

一个属性或构造带参数的值可以使用表达式如下所示进行设置。

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

    <!-- other properties -->
</bean>

变量systemProperties是预定义的,所以你可以在你的表达式使用 如下所示。请注意,您不必用``#前缀的预定义变量 符号于该上下文。

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

    <!-- other properties -->
</bean>

还可以参考其他bean属性的名字,例如。

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

    <!-- other properties -->
</bean>

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

    <!-- other properties -->
</bean>

基于注解的配置

@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;
    }

    // ...
}

字面量表达式

ExpressionParser parser = new SpelExpressionParser();

// evals to "Hello World"
String helloWorld = (String) parser.parseExpression("'Hello World'").getValue();

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

// evals to 2147483647
int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue();

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

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

数字支持使用负号,指数符号和小数点。 默认情况下,实数使用Double.parseDouble()。

属性/数组/列表/映射/索引器

用属性引用引导很简单:只要用一个.表示嵌套 属性值。实现Inventor类, pupin和tesla, 被添加 被添加。在章节classes 的例子. 使用表达式引导 “down” 同时获取 Tesla’s 出生年 和 Pupin’s 出生城市

// evals to 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 Array
StandardEvaluationContext teslaContext = new StandardEvaluationContext(tesla);

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

// Members List
StandardEvaluationContext societyContext = new StandardEvaluationContext(ieee);

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

// List and Array navigation
// evaluates to "Wireless communication"
String invention = parser.parseExpression("Members[0].Inventions[6]").getValue(
        societyContext, String.class);

通过在方括号内指定文字键值可以获取映射的内容。 在下面的示例中,由于 Officer 映射的键是字符串,因此我们可以指定字符串文字:

// Officer's Dictionary

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

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

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

内联列表

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

// evaluates to a Java list containing the four numbers
List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context);

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

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

内联映射

您也可以使用 {key:value} 表示法在表达式中直接表达映射。以下示例显示了如何执行此操作:

// evaluates to a Java map containing the two entries
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);

{:} 本身意味着一个空的映射。出于性能原因,如果映射图本身由固定的文字或其他嵌套的常量结构(列表或映射)组成,则会创建一个常量映射来表示该表达式(而不是在每次求值时都构建一个新的映射)。映射键的引号是可选的。上面的示例使用的是不带引号的键。

构建数组

您可以使用熟悉的 Java 语法来构建数组,可以选择提供一个初始化程序,以在构造时填充该数组。 以下示例显示了如何执行此操作:

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

// Array with initializer
int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context);

// Multi dimensional array
int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context);

构造多维数组时,当前无法提供初始化程序。

方法

您可以使用典型的 Java 编程语法来调用方法。 您还可以在文字上调用方法。 还支持变量参数。 下面的示例演示如何调用方法:

// string literal, evaluates to "bc"
String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class);

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

运算符

Spring 表达式语言支持以下几种运算符:

  • 关系运算符
  • 逻辑运算符
  • 数学运算符
  • 赋值运算符

关系运算符

使用标准运算符表示法支持关系运算符(等于,不等于,小于,小于或等于,大于和大于或等于)。 以下清单显示了一些运算符示例:

// evaluates to true
boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class);

// evaluates to false
boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class);

// evaluates to true
boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class);

null 的大于和小于比较遵循一个简单的规则:null 被视为无(不是零)。结果,任何其他值始终大于 nullX > null 始终为 true),并且其他任何值都不小于零(X < null 始终为 false)。

如果您需要数字比较,请避免使用基于数字的 null 比较,而建议使用零进行比较(例如,X > 0X < 0)。

除了标准的关系运算符外,SpEL 还支持 instanceof 和基于正则表达式的匹配运算符。 以下清单显示了两个示例:

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

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

//evaluates to 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 (!)

所有的文本运算符都不区分大小写。

逻辑运算符

SpEL 支持以下逻辑运算符:

  • 与(&&
  • 或(||
  • 非(!

下面的示例演示如何使用逻辑运算符

// -- AND --

// evaluates to false
boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class);

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

// -- OR --

// evaluates to true
boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class);

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

// -- NOT --

// evaluates to false
boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class);

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

数学运算符

您可以在数字和字符串上使用加法运算符。 您只能对数字使用减法,乘法和除法运算符。 您还可以使用模数(%)和指数幂(^)运算符。 强制执行标准运算符优先级。 以下示例显示了正在使用的数学运算符:

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

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

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

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

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

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

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

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

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

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

// Operator precedence
int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class);  // -21

赋值运算符

要设置属性,请使用赋值运算符(=)。 这通常在对 setValue 的调用内完成,但也可以在对 getValue 的调用内完成。 下面的清单显示了使用赋值运算符的两种方法:

Inventor inventor = new Inventor();
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();

parser.parseExpression("Name").setValue(context, inventor, "Aleksandar Seovic");

// alternatively
String aleks = parser.parseExpression(
        "Name = 'Aleksandar Seovic'").getValue(context, inventor, String.class);

类型

您可以使用特殊的 T 运算符来指定 java.lang.Class(类型)的实例。静态方法也可以通过使用此运算符来调用。StandardEvaluationContext 使用 TypeLocator 查找类型,而 StandardTypeLocator(可以替换)是在了解 java.lang 包的情况下构建的。 这意味着对 Java.lang 中的类型的 T() 引用不需要完全限定,但是所有其他类型引用都必须是完全限定的。下面的示例演示如何使用 T 运算符:

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);

构造器

您可以使用 new 运算符来调用构造函数。 除基本类型(intfloat 等)和 String 以外的所有其他类都应使用完全限定的类名。 下面的示例演示如何使用 new 运算符调用构造函数:

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

//create new inventor instance within add method of List
p.parseExpression(
        "Members.add(new org.spring.samples.spel.inventor.Inventor(
            'Albert Einstein', 'German'))").getValue(societyContext);

变量

您可以使用 #variableName 语法在表达式中引用变量。 通过在 EvaluationContext 实现上使用 setVariable 方法设置变量。

有效的变量名称必须由以下一个或多个受支持的字符组成。

  • 字母:AZaz
  • 数字:09
  • 下划线:_
  • 美元符号:$

以下示例显示了如何使用变量。

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

EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
context.setVariable("newName", "Mike Tesla");

parser.parseExpression("Name = #newName").getValue(context, tesla);
System.out.println(tesla.getName())  // "Mike Tesla"

#this#root 变量

#this 变量始终是定义的,并且引用当前的评估对象(反对解决不合格的引用)。#root 变量也是始终定义,并引用根上下文对象。尽管 #this可能随表达式的组成部分的求值而变化,但 #root 始终引用根。 以下示例说明如何使用 #this#root 变量:

// create an array of integers
List<Integer> primes = new ArrayList<Integer>();
primes.addAll(Arrays.asList(2,3,5,7,11,13,17));

// create parser and set variable 'primes' as the array of integers
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess();
context.setVariable("primes", primes);

// all prime numbers > 10 from the list (using selection ?{...})
// evaluates to [11, 13, 17]
List<Integer> primesGreaterThanTen = (List<Integer>) parser.parseExpression(
        "#primes.?[#this>10]").getValue(context);

函数

您可以通过注册可以在表达式字符串中调用的用户定义函数来扩展 SpEL。该函数通过 EvaluationContext 注册。下面的示例显示如何注册用户定义的函数:

Method method = ...;

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("myFunction", method);

例如,考虑以下用于反转字符串的实用程序方法:

public abstract class StringUtils {

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

然后,您可以注册并使用前面的方法,如以下示例所示:

ExpressionParser parser = new SpelExpressionParser();

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
context.setVariable("reverseString",
        StringUtils.class.getDeclaredMethod("reverseString", String.class));

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

Bean 引用

如果评估上下文已使用 bean 解析器配置,则可以使用@符号从表达式中查找 bean。 以下示例显示了如何执行此操作:

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

// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("@something").getValue(context);

要访问工厂 bean 本身,您应该在 bean 名称前加上&符号。 以下示例显示了如何执行此操作:

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

// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
Object bean = parser.parseExpression("&foo").getValue(context);

三元表达式

您可以使用三元运算符在表达式内部执行 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 运算符的下一部分。

Elvis 运算符

Elvis 运算符是三元运算符语法的简化,并且在 Groovy 语言中使用。 使用三元运算符语法,通常必须将变量重复两次,如以下示例所示:

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

相反,您可以使用 Elvis 运算符(其命名类似于 Elvis 的发型)。 以下示例显示了如何使用 Elvis 运算符:

ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class);
System.out.println(name);  // 'Unknown'

以下显示了一个更复杂的示例:

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Nikola Tesla

tesla.setName(null);
name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name);  // Elvis Presley

您可以使用 Elvis 运算符在表达式中应用默认值。 以下示例显示了如何在 @Value 表达式中使用 Elvis 运算符:

> @Value("#{systemProperties['pop3.port'] ?: 25}")
>

上例将注入系统属性 pop3.port,为空将注入 25。

安全导航运算符

安全导航运算符用于避免 NullPointerException,它来自 Groovy 语言。通常,当您引用一个对象时,可能需要在访问该对象的方法或属性之前验证其是否为 null。为了避免这种情况,安全导航运算符返回 null 而不是引发异常。 下面的示例演示如何使用安全导航操作符:

ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

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

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

tesla.setPlaceOfBirth(null);
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city);  // null - does not throw NullPointerException!!!

集合选择

选择是一种强大的表达语言功能,可让您通过从源集合中进行选择来将其转换为另一个集合。

选择使用 .?[selectionExpression] 的语法。 它过滤集合并返回一个包含原始元素子集的新集合。例如,通过选择,我们可以轻松地获得 Serbian inventors 的列表,如以下示例所示:

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

在列表和映射上都可以选择。对于列表,将针对每个单独的列表元素评估选择标准。针对映射,针对每个映射条目(Java 类型 Map.Entry 的对象)评估选择标准。每个映射条目都有其键和值,可作为属性访问以供选择。

以下表达式返回一个新映射,该映射由原始映射中条目值小于 27 的那些元素组成:

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

除了返回所有选定的元素外,您只能检索第一个或最后一个值。为了获得与选择匹配的第一个条目,语法为 .^[selectionExpression]。要获取最后一个匹配选择,语法为 .$[selectionExpression]

集合投影

投影使集合可以驱动子表达式的求值,结果是一个新的集合。投影的语法为 .![projectionExpression]。例如,假设我们有一个 inventor 列表,但是想要他们出生的城市列表。实际上,我们希望为 inventor 列表中的每个条目计算“placeOfBirth.city”。 下面的示例使用投影来做到这一点:

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

您还可以使用映射来驱动投影,在这种情况下,将针对映射中的每个条目(表示为 Java Map.Entry)对投影表达式进行评估。 跨映射的投影结果是一个列表,其中包含针对每个映射条目的投影表达式的评估。

表达式模板

表达式模板允许将文字文本与一个或多个评估块混合。每个评估块均以您可以定义的前缀和后缀字符分隔。常见的选择是使用 #{ } 作为分隔符,如以下示例所示:

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

// evaluates to "random number is 0.7038186818312008"

通过将文字文本 'random number is ' 与评估 #{ } 分隔符内的表达式的结果(在本例中为调用 random() 方法的结果)相连接来评估字符串。parseExpression() 方法的第二个参数的类型为 ParserContextParserContext 接口用于影响表达式的解析方式,以支持表达式模板功能。TemplateParserContext 的定义如下:

public class TemplateParserContext implements ParserContext {

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

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

    public boolean isTemplate() {
        return true;
    }
}

示例类

本节列出了本章示例中使用的类。

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;
    }
}