20220507 Core - 4. Spring Expression Language (SpEL)

前言

文档地址

Spring 表达式语言(简称 SpEL )是一种强大的表达式语言,支持在运行时查询和操作对象图。语法类似于统一 EL,但提供了额外的功能,最显著的是方法调用和基本的字符串模板功能。

虽然还有其他几种可用的 Java 表达式语言——OGNL、MVEL 和 JBoss EL , 但创建 Spring 表达式语言是为了向 Spring 社区提供一种单一的、支持良好的表达式语言,可以在所有 Spring 产品中使用 。它的语言特性由 Spring 产品组合中的项目需求驱动。也就是说,SpEL 基于与技术无关的 API,可以在需要时集成其他表达式语言实现。

SpEL 作为 Spring 产品组合中表达式求值的基础,但它不直接与 Spring 相关联,可以独立使用。为了做到自包含,需要创建一些引导基础结构类,例如解析器。大多数 Spring 用户不需要处理此基础结构,而只需编写用于计算的表达式字符串。这种典型用法的一个例子是将 SpEL 集成到创建基于 XML 或注解的 bean 定义中,如用于定义 bean 定义的 Expression 支持中所示

示例中使用的类

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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class Society {

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

    private String name;
    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;
    }
}

评估(Evaluation)

SpEL 类和接口位于 org.springframework.expression 包及其子包中

SpEL 的简单使用:

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

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

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

// 在字符串文字上调用 concat 方法
Expression exp = parser.parseExpression("'Hello World'.concat('!')");
String message = (String) exp.getValue();
// 调用 JavaBean 属性的示例,调用 String 属性 Bytes
// 注意 String 中有 getBytes 方法
// invokes 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes");
byte[] bytes = (byte[]) exp.getValue();
// 通过使用标准点符号(例如prop1.prop2.prop3)以及属性值的相应设置来支持嵌套属性。也可以访问公共字段。
// invokes 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length");
// 调用 String 的构造函数而不是使用字符串字面值
// 构造一个新的 String,并将其设置为大写
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); 
String message = exp.getValue(String.class);

注意泛型方法的使用:public T getValue(Class 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);
// result == true

理解 EvaluationContext

在计算表达式时, EvaluationContext 接口用于解析属性、方法或字段并帮助执行类型转换。

Spring 提供了两种实现。

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

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

  • 仅自定义 PropertyAccessor(无反射)
  • 只读访问的数据绑定属性
  • 用于读写的数据绑定属性
类型转换

默认情况下,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);

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

Parser 配置( SpelParserConfiguration

可以使用解析器配置对象 ( org.springframework.expression.spel.SpelParserConfiguration )来配置 SpEL 表达式解析器。配置对象控制一些表达式组件的行为。例如,如果您对数组或集合进行索引,并且指定索引处的元素是 null ,SpEL 可以自动创建元素。这在使用由属性引用链组成的表达式时很有用。如果您索引数组或列表并指定超出数组或列表当前大小末尾的索引,SpEL 可以自动增长数组或列表以容纳该索引。

为了在指定索引处添加元素,SpEL 将尝试在设置指定值之前使用元素类型的默认构造函数创建元素。如果元素类型没有默认构造函数,null 将被添加到数组或列表中。如果没有知道如何设置值的内置或自定义转换器,则将 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 编译(Compilation)

Spring Framework 4.1 包含一个基本的表达式编译器。表达式通常是解释性的,这在计算期间提供了很多动态灵活性,但不能提供最佳性能。对于偶尔使用的表达式很有好处,但是,当被其他组件(例如 Spring Integration)使用时,并且不需要真正的动态性,性能可能非常重要。

SpEL 编译器旨在满足这一需求。在求值期间,编译器生成一个 Java 类,该类包含运行时的表达式行为,并使用该类来实现更快的表达式求值。由于缺少表达式周围的类型,编译器在执行编译时使用在表达式的解释评估期间收集的信息。例如,它不完全从表达式中知道属性引用的类型,但在第一次解释评估期间,它会找出它是什么。当然,如果各种表达式元素的类型随时间发生变化,则基于这些导出信息进行编译可能会在以后造成麻烦。出于这个原因,编译最适合类型信息不会因重复评估而改变的表达式。

考虑以下基本表达式:

someArray[0].someProperty.someOtherProperty < 0.1

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

Compiler 配置( SpelCompilerMode

默认情况下,编译器不会打开,但您可以通过两种方式打开它。您可以通过使用解析器配置过程或在 SpEL 用法嵌入到另一个组件中时使用 Spring 属性来打开它。

编译器可以在 org.springframework.expression.spel.SpelCompilerMode 枚举中定义的三种模式运行 。模式如下:

  • OFF (默认):编译器关闭。
  • 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 嵌入在其他组件中时使用,并且可能无法通过配置对象对其进行配置。在这些情况下,可以通过 JVM 系统属性(或通过属性 SpringProperties 机制)设置 spring.expression.compiler.modeSpelCompilerMode 枚举值( offimmediate ,或 mixed

Compiler 限制

从 Spring Framework 4.1 开始,基本的编译框架已经可用。但是,该框架尚不支持编译所有类型的表达式。最初的重点是可能在性能关键上下文中使用的常用表达式。目前无法编译以下类型的表达式:

  • 涉及赋值的表达式
  • 依赖类型转换服务的表达式
  • 使用自定义解析器或访问器的表达式
  • 使用选择或投影的表达式( Expressions using selection or projection )

将来可以编译更多类型的表达式。

Bean 定义中的表达式

可以使用带有基于 XML 或基于注解的配置元数据的 SpEL 表达式来定义 BeanDefinition 实例。在这两种情况下,定义表达式的语法都是 #{ <expression string> }

XML 配置

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

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

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

应用上下文中的所有 bean 都可用作具有公共 bean 名称的预定义变量。这包括用于访问运行时环境的标准上下文 bean,例如 environmentorg.springframework.core.env.Environment 类型)以及 systemPropertiessystemEnvironmentMap 类型)。

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

    // ...
}

表达式参考

字面值

支持的文字表达式类型是字符串、数值(整数、实数、十六进制)、布尔值和空值。

字符串由单引号分隔。要将单引号本身放在字符串中,请使用两个单引号字符。

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

属性、数组、列表、映射和索引器

使用句点表示嵌套的属性值

// evals to 1856
int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context);

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

属性名称的第一个字母允许不区分大小写。因此,上面例子中的表达式可以分别写成 Birthdate.Year + 1900PlaceOfBirth.City 。此外,属性可以选择性地通过方法调用来访问——例如,getPlaceOfBirth().getCity()

数组和列表的内容使用方括号表示法获取:

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

map 的内容可以通过在方括号内指定键值来获得

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

{} 本身意味着一个空列表。

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

内联 Map

可以使用 {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);

{:} 本身就意味着一张空 Map

出于性能原因,如果映射本身由固定文字或其他嵌套常量结构(列表或映射)组成,则会创建一个常量 Map 来表示表达式(而不是在每次评估时构建一个新 Map )。

Map 键的引号是可选的(除非键包含句点 ( . ))

数组

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

运算符

关系运算符

使用标准运算符表示法支持关系运算符(等于、不等于、小于、小于或等于、大于和大于或等于)。

// 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 总是 trueX < null 总是 false )。

如果您更喜欢数字比较,则应避免基于数字的 null 比较,以利于与零进行比较(例如,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 ( ! )

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

逻辑运算符

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);
数学运算符

可以对数字和字符串使用加法运算符 ( + ) 。只能对数字使用减法 ( - )、乘法 ( * ) 和除法 ( / ) 运算符。还可以对数字使用模数 ( % ) 和指数幂 ( ^ ) 运算符。强制执行标准运算符优先级。

以下示例显示了正在使用的数学运算符:

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

类型(Types)

可以使用特殊运算符 T 来指定 java.lang.Class 的实例。静态方法也可以使用此运算符调用。 StandardEvaluationContext 使用 TypeLocator 查找类型以及 StandardTypeLocator(可替换)查找 java.lang 包。这意味着 T()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);

构造函数

您可以使用 new 运算符调用构造函数。除了位于 java.lang 包中的类型( IntegerFloatString 等),应该对所有类型使用完全限定的类名。以下示例显示了如何使用 new 运算符来调用构造函数:

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

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

变量

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

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

  • 字母:A - Za - 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);

函数(Functions)

可以通过注册可在表达式字符串中调用的用户定义函数来扩展 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
// 调用 MyBeanResolver 的 resolve(context,"something") 方法
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
// 调用 MyBeanResolver 的 resolve(context,"&foo") 方法
Object bean = parser.parseExpression("&foo").getValue(context);

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

String falseString = parser.parseExpression("false ? 'trueExp' : 'falseExp'").getValue(String.class);
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"

猫王运算符(The Elvis Operator)

Elvis 运算符(与 Elvis 的发型相似而命名)

使用三元运算符语法,您通常必须将变量重复两次,猫王运算符可以简化一下

ExpressionParser parser = new SpelExpressionParser();

// 等同于 name != null ? name : "Unknown"
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("#{systemProperties['pop3.port'] ?: 25}")

这将注入系统属性 pop3.port (如果已定义),否则将注入25。

安全导航运算符

?.

安全导航操作符用于避免 NullPointerException 。通常,当您有一个对象的引用时,您可能需要在访问该对象的方法或属性之前验证它是否为空。为了避免这种情况,安全导航运算符返回 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!!!

集合选择

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

选择使用 .?[selectionExpression] 。 它过滤集合并返回一个包含原始元素子集的新集合

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

数组和任何实现 java.lang.Iterablejava.util.Map 的对象都支持选择。对于列表或数组,将针对每个单独的元素评估选择标准。针对 Map ,针对每个 Map 条目( Map.Entry 类型的对象)评估选择标准。每个 Map 条目都有 keyvalue 可选择访问。

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

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

集合投影

投影(Projection)让集合驱动子表达式的计算,结果是一个新集合。投影的语法是 .![projectionExpression]

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

数组和任何实现 java.lang.Iterablejava.util.Map 的对象都支持投影。使用 Map 驱动投影时,将针对 Map 中的每个条目( Map.Entry )评估投影表达式。Map 上的投影结果是一个列表,其中包含对每个 Map 条目的投影表达式的评估。

表达式模板

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

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;
    }
}
posted @ 2022-06-09 21:15  流星<。)#)))≦  阅读(63)  评论(0编辑  收藏  举报