Spring 5 中文解析核心篇-IoC容器之SpEL表达式

Spring表达式语言(简称SpEl)是非常强大的表达式语言,它支持在运行时查询和手动操作对象图。这个语言语法类似EL但是提供了额外的特性,最著名的是方法调用和基本字符串模板功能。

虽然还有其他几种Java表达式语言OGNLMVELJBoss EL可用,但创建Spring表达式语言是为了向Spring社区提供一种受良好支持的表达式语言,可以跨Spring组合中的所有产品使用。它的语言特性是由Spring组合中的项目需求驱动的,包括用于Eclipse的Spring工具中的代码完成支持的工具需求。也就是说,SpEL基于与技术无关的API,如果需要,可以将其他表达语言实现集成在一起。虽然SpEL是Sprin*品组合中表达评估的基础,但它并不直接与Spring绑定,可以独立使用。为了自我包含,本章中的许多例子使用SpEL,好像它是一种独立的表达语言。这需要创建一些引导基础设施类,比如解析器。大多数Spring用户不需要处理这个基础设施,相反,而只需编写表达式字符串进行评估。这种典型用法的一个示例是将SpEL集成到创建XML或基于注解的Bean定义中,如Expression支持中所示,用于定义bean定义。

本章介绍了表达语言,其API和语言语法的功能。在许多地方,InventorSociety类都用作表达评估的目标对象。这些类声明和用于填充它们的数据在本章末尾列出(末尾给出示例代码)。

表达式语言支持以下功能:

  • 文字表达式
  • 布尔运算符和关系运算符
  • 常用表达式
  • 类表达式
  • 访问属性,数组,列表和映射
  • 方法调用
  • 关系运算符
  • 分配
  • 调用构造函数
  • Bean引用
  • 数组构造
  • 内联列表
  • 内联Map
  • 三元运算符
  • 变量
  • 用户定义的功能
  • 集合投影
  • 集合选择
  • 模板表达式
4.1 评估

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

以下代码介绍了SpEL API,用于评估文字字符串表达式Hello World

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); //1
String message = (String) exp.getValue();
  1. 消息值变量使用'Hello World'

你最可能使用的SpEL类和接口位于org.springframework.expression包及其子包中,例如spel.support

ExpressionParser接口负责解析表达式字符串。在前面的示例中,表达式字符串是由周围的单引号表示的字符串文字。Expression接口负责评估先前定义的表达式字符串。分别调用parser.parseExpressionexp.getValue时,可以引发两个异常,ParseExceptionEvaluationException

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

在以下方法调用示例中,我们在字符串文字上调用concat方法:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); //1
String message = (String) exp.getValue();
  1. 消息值 'Hello World!'是 'Hello World!'。
ExpressionParser parser = new SpelExpressionParser();

// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); //1
byte[] bytes = (byte[]) exp.getValue();
  1. 此行将文字转换为字节数组。

SpEL还通过使用标准的点符号(例如prop1.prop2.prop3)以及相应的属性值设置值支持嵌套属性。也可以访问公共字段。

下面的示例演示如何使用点表示法获取文字的长度:

ExpressionParser parser = new SpelExpressionParser();

// invokes 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); //1
int length = (Integer) exp.getValue();
  1. “Hello World” .bytes.length给出文字的长度。

可以调用String的构造函数,而不是使用字符串文字,如以下示例所示:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); //1
String message = exp.getValue(String.class);
  1. 从文字构造一个新的String并将其变为大写。

注意通用方法的使用:

public <T> T getValue(Class<T> desiredResultType)。使用这个方法移除需要需要强制转换表达式值为期望结果类型。如果这个值不能转换为类型T或通过注册的类型转换器转换,会抛出一个EvaluationException异常。

SpEL的更常见用法是提供一个针对特定对象实例(称为根对象)进行评估的表达式字符串。以下示例显示如何从Inventor类的实例检索name属性或如何创建布尔条件:

// 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"); // Parse name as an expression
String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla"

exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
4.1.1 理解EvaluationContext

在评估表达式以解析属性、方法或字段并帮助执行类型转换时,使用EvaluationContext接口。Spring提供了两种实现。

  • SimpleEvaluationContext: 针对不需要完全使用SpEL语言语法并且应该有意义地加以限制的表达式类别,公开基本SpEL语言特性和配置选项的子集。示例包括但不限于数据绑定表达式和基于属性的过滤器。
  • StandardEvaluationContext: 公开SpEL语言功能和配置选项的全部集合。你可以使用它来指定默认的根对象并配置每个可用的评估相关策略。

SimpleEvaluationContext设计为仅支持SpEL语言语法的子集。它不包括Java类型引用、构造函数和Bean引用。它还要求你明确选择对表达式中的属性和方法的支持级别。默认情况下,create()静态工厂方法仅启用对属性的读取访问。你还可以获取构建器来配置所需的确切支持级别,并针对以下一种或某种组合:

  • 仅自定义PropertyAccessor(无反射)
  • 只读访问的数据绑定属性
  • 读写的数据绑定属性

类型转换

默认情况下,SpEL使用Spring核心中可用的转换服务(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);

EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

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

// b is false
Boolean b = simple.booleanList.get(0);
4.1.2 解析配置

可以使用解析器配置对象(org.springframework.expression.spel.SpelParserConfiguration)配置SpEL表达式解析器。配置对象控制某些表达式组件的行为。例如,如果你索引到数组或集合中并且指定索引处的元素为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
4.1.3 SpEL编译器

Spring Framework 4.1包含一个基本的表达式编译器。通常对表达式进行解释,这样可以在评估过程中提供很大的动态灵活性,但不能提供最佳性能。对于偶尔使用表达式,这很好,但是,当与其他组件(如Spring Integration)集成一起使用时,性能可能非常重要,并且不需要动态性。

SpEL编译器旨在满足这一需求。在评估过程中,编译器会生成一个Java类,该类体现了运行时的表达式行为,并使用该类来实现更快的表达式评估。由于表达式周围缺乏输入,编译器在执行编译时使用在解释表达式评估期间收集的信息。例如,它不仅仅从表达式中就知道属性引用的类型,而是在第一次解释评估时就知道它是什么。当然,如果各种表达元素的类型随时间变化,则基于此类派生信息进行编译会在以后引起麻烦。因此,编译最适合类型信息在重复评估时不会改变的表达式。

考虑下面简单表达式:

someArray[0].someProperty.someOtherProperty < 0.1

由于前面的表达式涉及数组访问,一些属性取消引用和数字运算,因此性能提升可能非常明显。在一个示例中,进行了50000次迭代的微基准测试,使用解释器评估需要75毫秒,而使用表达式的编译版本仅需要3毫秒。

编译器配置

默认情况下不打开编译器,但是你可以通过两种不同的方式之一来打开它。当SpEL用法嵌入到另一个组件中时,可以使用解析器配置处理(前面讨论过)或使用系统属性来打开它。本节讨论这两个选项。

编译器可以在org.springframework.expression.spel.SpelCompilerMode枚举中捕获的三种模式之一进行操作。模式如下:

  • OFF(default):编译器被关闭
  • IMMEDIATE:在立即模式下,表达式将尽快编译。通常是在第一次解释评估之后。如果编译的表达式失败(通常是由于类型更改,如前所述),则表达式评估的调用者将收到异常。
  • MIXED:在混合模式下,表达式会随着时间静默在解释模式和编译模式之间切换。经过一定数量的解释运行后,它们会切换到编译形式,如果编译形式出了问题(例如,如前面所述的类型更改),则表达式会自动再次切换回解释形式。一段时间后,它可能会生成另一个已编译的格式并切换到它。基本上,用户在IMMEDIATE模式下获得的异常是在内部处理的。

存在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 Framework 4.1开始,已经有了基本的编译框架。但是,该框架尚不支持编译每种表达式。最初的重点是可能在性能关键型上下文中使用的通用表达式。

目前无法编译以下类型的表达式:

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

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

4.2.1 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>
4.2.2 注解配置

若要指定默认值,可以将@Value注解放置在字段、方法以及方法或构造函数参数上。

下面的示例设置字段变量的默认值:

public class FieldValueTestBean {

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

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

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

以下示例显示了等效的但使用属性设置器方法的示例:

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

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

    // ...
}
4.3 语法手册

本节描述了Spring Expression Language的工作方式。它涵盖以下主题:

4.3.1 文字表达式

支持的文字表达式的类型为字符串、数值(int,实数,十六进制)、布尔和null。字符串由单引号引起来。要将单引号本身放在字符串中,请使用两个单引号字符。

以下清单显示了字符串的简单用法。通常,它们不是像这样孤立地使用,而是作为更复杂的表达式的一部分使用-例如,在逻辑比较运算符的一侧使用文字。

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()解析实数。

参考代码:com.liyong.ioccontainer.service.expression.LiteralExpr

4.3.2 属性、数组、列表、Map和索引器

使用属性引用进行导航很容易。为此,请使用句点来指示嵌套的属性值。Inventor类的实例pupintesla填充有示例部分使用的类中列出的数据。要向下导航并获取Tesla的出生年份和Pupin的出生城市,我们使用以下表达式:

// 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();
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();

// Inventions Array

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

// Members List

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

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

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

// 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");
4.3.3 内联列表

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

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

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

4.3.4 内联Map

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

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

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

4.3.5 数组构造

你可以使用熟悉的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);

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

4.3.6 方法

你可以使用典型的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);
4.3.7 操作符

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被视为无(不是零)。结果,任何其他值始终大于null(X> null始终为true),并且其他任何值都不小于零(X <null始终为false)。如果你更喜欢数字比较,请避免使用基于数字的空比较,而建议使用零进行比较(例如,X>0或X<0)。

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

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

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

参考代码:com.liyong.ioccontainer.service.expression.Operators

逻辑操作符

SpEL支持以下逻辑运算符:

  • and (&&)
  • or (||)
  • not (!)

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

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

数字运算符

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

// 加法
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

赋值运算符

要设置属性,请使用赋值运算符(=)。这通常在对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);
4.3.8 类型

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

你可以使用new运算符来调用构造函数。除基本类型(int,float等)和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);
4.3.10 变量

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

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

  • 字母:A到Z和a到z
  • 数组:0到9
  • 下划线:_
  • 美元符号:$

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

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);
4.3.11 函数

你可以通过注册可以在表达式字符串中调用的用户定义函数来扩展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);

参考代码:com.liyong.ioccontainer.service.expression.PropertiesArraysAndSoOn

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

参考代码:com.liyong.ioccontainer.service.expression.BeanRef

4.3.13 三元运算符

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

4.3.14 Elvis运算符

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

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

相反,你可以使用Elvis运算符。以下示例显示了如何使用Elvis运算符:

ExpressionParser parser = new SpelExpressionParser();

String name = parser.parseExpression("name?:'Unknown'").getValue(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

4.3.15 安全导航操作符

安全导航运算符用于避免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 - 不抛出 NullPointerException!!!

参考代码:com.liyong.ioccontainer.service.expression.SafeNavigation

4.3.16 集合选择

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

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

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

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

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

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

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

参考代码:com.liyong.ioccontainer.service.expression.CollectionSelected

4.3.17 集合投影

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

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

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

参考代码:com.liyong.ioccontainer.service.expression.CollectionProjection

4.3.18 表达式模版

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

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()方法的结果)相连接来评估字符串。parseExpression()方法的第二个参数的类型为ParserContextParserContext接口用于影响表达式的解析方式,以支持表达式模板功能。TemplateParserContext的定义如下:

public class TemplateParserContext implements ParserContext {

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

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

    public boolean isTemplate() {
        return true;
    }
}
4.4 示例中使用的类

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

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

作者

个人从事金融行业,就职过易极付、思建科技、某网约车平台等重庆一流技术团队,目前就职于某银行负责统一支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、响应式编程、人工智能等领域。同时也热衷于技术分享创立公众号和博客站点对知识体系进行分享。关注公众号订阅最新资讯!!!

博客地址: http://youngitman.tech

CSDN: https://blog.csdn.net/liyong1028826685

微信公众号:
Spring 5 中文解析核心篇-IoC容器之SpEL表达式

技术交流群:
Spring 5 中文解析核心篇-IoC容器之SpEL表达式

上一篇:Spring 5 中文解析核心篇-IoC容器之基于Java容器配置


下一篇:Spring 5 中文解析核心篇-IoC容器之BeanDefinition继承与容器拓展点