Spring 表达式语言

SpEL 是 Spring Expression Language 的简称,虽然目前已经有许多其它的 Java 表达式语言,例如 OGNL、MVEL、Jboss EL,但 SpEL 的诞生是为了给 Spring 社区提供一种能够与 Spring 生态系统所有产品无缝对接、提供一站式支持的表达式语言。它的语言特性由 Spring 生态系统的实际项目需求驱动而来,比如基于 Eclipse 的 Spring Tool Suite(Spring 开发工具集)中的代码补全工具需求。尽管如此、SpEL 本身基于一套与具体实现技术无关的 API,在需要的时候允许其它表达式语言实现集成进来。

SpEL 简介

SpEL 与 OGNL 很相似,有自己的执行上下文(context),context 中有一个 root-object(根对象),context 其实就是一个 key-value 结构的 map,而这个 root-object 就是其中的特殊 value,我们可以在表达式中直接访问 root-object 的 bean 属性,而不需要指定 key 名。来看一个简单的例子:

执行结果:

SpEL 拥有很多特性

  • 允许通过 bean 的 id 来引用 bean
  • 调用对象的方法和访问对象的属性
  • 对值进行算术、关系和逻辑等运算
  • regex 匹配,集合访问、集合投影

其实 EL 表达式也能进行类似的操作(除了正则表达式和集合投影这些),到后面你会发现,SpEL 的语法和 EL 表达式的语法很相似。

SpEL 语法

和 JSP EL 表达式一样,SpEL 表达式内可以使用 整数浮点数字符串(单引号或双引号包围)、true/false 以及 null。此外,SpEL 表达式的语法和 EL 表达式的语法也很相似,EL 表达式使用 ${expression} 包围,而 SpEL 表达式使用 #{expression} 包围。不对,你说 SpEL 表达式需要 #{} 包围?那为什么上面的例子中没有使用这种语法呢?别急,听我慢慢道来。

上面这种用法其实很少见,大多数情况下,我们都是在 Spring 程序中使用 SpEL 表达式,比如我们可以在 spring.xml 配置文件中使用 SpEL,也可以在 Java 程序中使用 @Value 注解来使用 SpEL,这两种方式的语法都是 #{expression},而不是上面这种语法,并且它们除了这一点不同外,还有很多不同的地方,比如上面这种方式如果要引用 Bean,必须使用 @beanId,而在 #{expression} 中,直接使用 beanId 来引用就可以了。

因为文章开头这种用法很少见,所以本文也不打算讲解,如果你需要使用这种方式的 SpEL,可以参考:Spring 表达式语言之 SpEL 语法

算术运算符+加、-减、*乘、/除、%求余、^乘方,/ 的等价运算符 div% 的等价运算符 mod(不区分大小写,下同)。

关系运算符==等于、!=不等于、>大于、>=大于等于、<小于、<=小于等于、between区间运算。所谓区间运算就是 1.5 between {1, 2},等价于 1.5 >= 1 && 1.5 <= 2,所以返回 true,记住是包含边界的。同样的,对于大于小于等于不等于这些,SpEL 也提供了英文关键字:eq等于、ne不等于、gt大于、ge大于等于、lt小于、le小于等于,不区分大小写。

逻辑运算符&&逻辑与、||逻辑或、!逻辑非,等价关键字为 andornot,也是不区分大小写。

字符串操作:与 Java 语法一样,字符串可以使用 + 进行连接,此外,SpEL 还可以直接使用 'hello'[0] 语法来取指定 index 上的字符。

三目操作符:语法同 Java 中的三目运算符,boolExpr ? valueIfTrue : valueIfFalse,除此之外,SpEL 还引入了 Groovy 中的 null 值运算符,语法为 expr ?: valueIfExprIsNull,当 expr 为 null 时,表达式的值为 valueIfExprIsNull,如果 expr 不为 null,则表达式的值仍然为 expr,可以看作是给 expr 提供一个默认值。

正则匹配符string matches regex,regex 为字符串,表达式的返回类型为 boolean,如 'hello' matches '^\\w++$' 的结果为 true。

括号优先级:和 Java 一样,可以使用 () 来改变表达式的优先级(或者提高可读性),比如 (5 + 3) * 5 的结果为 40,而不是 20。

属性访问符#{object.prop}#{object['prop']}#{object["prop"]}(单双引号没区别),它们的区别和 EL 表达式是一样的,第一种方式的 prop 必须符合 Java 标识符规范(以字母、下划线、美元符开头,后可接字母、数字、下划线、美元符),而第二、三种方式则没有此要求;第一种方式属于静态取值,而第二、三种方式属于动态取值,所谓动态取值就是这个 prop 可以在运行期间动态的计算出来,灵活性更大,但性能不如静态取值,而静态取值中的 prop 是编译期间写死的。

类型表达式T(Type) 表示一个 java.lang.Class 实例,其中 Type 为类的名称,除了 java.lang 包外,其它的类名必须为全限定类名,比如 T(String)T(java.util.Arrays)。使用类型表达式还可以访问对应类的静态字段和静态方法,语法和 Java 很相似,都是使用 . 操作符,如 T(Integer).MAX_VALUET(java.util.Arrays).toString(new int[] {1, 2, 3})

类的实例化new String('hello, world')new java.util.ArrayList(),同样的,除了 java.lang 包外,其它包下的类需要带上包名。

instanceof'hello' instanceof T(String),返回 true,语法和语义和 Java 中的 instanceof 是一样的。

变量的引用:在第一节中的例子中已经演示过,SpEL 中可以使用 #root 引用 context 中的 rootObject,使用 #emp01 引用 context 中的 "emp01" 对象,此外还有一个特殊的变量,#this,这个一般用在集合投影中,表示当前对象,其实和 Java 中的 this 语义差不多。

赋值表达式:如 #root = 1 + 2,执行后,rootObject 的值为 3。又如 #test = 100 + 100,执行后,会创建自定义变量 test,且值为 200。

安全访问符:前面已经介绍了 obj.propobj['prop']obj["prop"] 三种方式的 bean/pojo 对象取值,但其实我们还有一种方式,语法为 obj?.prop,当 obj 为 null 时,表达式不会继续执行,而是直接返回 null,避免了空指针异常的发生。如 #test.test 表示访问 test 变量的 test 属性(getTest() 方法),但是我们并没有在 context 中定义 test 变量,所以 SpEL 会返回 null,然后执行到 null.test 时,就会出现空指针异常,我们将其改为 #test?.test 之后,就没有问题了,表达式的结果为 null。此外,?. 安全访问符也可以用来调用对象的方法,比如 obj?.getValue(),作用和调用对象的属性是一样的,防止空指针异常。

方法调用符:访问对象的方法和访问类的静态方法的语法是一样的,如 'hello'.getBytes() 返回 hello 字符串的字节数组对象。

引用bean:在传统方式中,使用 @beanId 的形式来引用 bean,比如一个 bean 的 id 为 dataSource,则使用 @dataSource 来引用它,但是在 #{expression} 表达式中,直接使用 dataSource 来引用就可以了。在 Spring 环境中,定义了两个 bean,systemEnvironment(环境变量) 和 systemProperties(系统属性),环境变量很好理解,就是当前系统的环境变量,如 PATH 变量,可以使用 systemEnvironment.PATH 来访问,而系统属性就是传递给 jvm 的 -Dkey=value 参数,当然 jvm 也内设了很多 properties,而且我们也可以在 spring.xml 中引入外部的 properties 文件,都可以使用 systemProperties.prop 来访问它们。

引用prop:在 #{expression} 表达式中,我们可以使用 ${propertyName} 来引用 properties 中的属性(系统内置属性、命令行传递的属性、外部文件中定义的属性等),当然,在 spring.xml 和 @Value 注解中,也可直接使用 ${} 来引用 property,如 @Value("${jdbc.url}"),如果对应的属性不存在,我们也可以给它分配一个默认值,语法为 ${jdbc.database:test},如果 jdbc.database 这个属性不存在,则使用默认值 test 替代。

List定义{1, 2, 3} 返回一个 ArrayList,{} 返回一个空的 List,对于字面量表达式 List,SpEL 会将它设为不可修改的,比如 {}{1, 2, 3} 就是不可修改的,而 {1 + 2, 2 + 3, 3 + 4} 就是可以修改的。

数组定义new int[] {1, 2, 3} 静态初始化一个大小为 3 的 int 数组;new int[3] 分配一个大小为 3 的 int 数组,但是不进行初始化,如果不设置元素的值,则默认为 0;new Object[3] 分配一个大小为 3 的 object 数组,但不进行初始化,如果不设置元素的值,则默认为 null。

集合访问:对于 array、list、map,都可以使用 集合[索引]map[key] 来访问对应的 value。如 {1, 2, 3}[0] 返回 list 的第 0 个元素,也即 1。此外,对于可修改的集合对象,也可以修改元素的值,如 map['key'] = 'value',将 map 中的 'key' 对应的 value 改为 'value'

SpEL 使用

这里讨论的是在 Spring 环境中使用 SpEL 表达式,有两个地方可以使用 SpEL 表达式,一个是 spring.xml(Spring 配置文件),一个是在 Bean 的成员变量上使用 @Value 注解来注入 SpEL 表达式的值(注意不能在静态字段上使用 @Value 注解,因为 SpEL 表达式不会被执行,字段的值为 null)。先来看第一种,在 spring.xml 中使用 SpEL 表达式。配置 pom.xml,引入 spring-context 依赖,spring-context 内部会依赖 spring-expression:

spring.xml,注意 <context:property-placeholder/> 元素,不加这个,${} 属性表达式不会被执行,会直接显示 ${os.name} 这样的值。

XmlTest.java

Main.java

执行结果:

引入外部 properties 文件
除了访问系统定义的属性外,如 os.name、file.encoding 这些 jvm 自带的属性,我们也可以将外部的 properties 文件中的属性引入到 spring.xml,然后就可以在 spring.xml 中引用了,当然在 @Value 注解中也能访问这些属性,都是一样的。首先我们在 classpath 中放入我们的属性文件,因为我使用 maven 进行项目管理,所以直接在 src/main/resources 目录下创建一个 test.properties 属性文件,内容如下:

spring.xml

运行结果:

引入多个外部 properties 文件,只需要使用英文逗号隔开就行:

test01.properties

test02.properties

运行结果:

<context:property-placeholder/>PropertyPlaceholderConfigurer 是等价的

@Value 注解
除了直接在 spring.xml 配置文件中引用 properties 和使用 SpEL 表达式外,也可以直接在类的成员变量、成员方法、方法参数上使用 @Value 注解,语法和 spring.xml 里面的 properties 引用、SpEL 表达式是一样的,来看一个简单的例子(@Value 注解所在的类必须注册为 bean,否则不会被解析):

spring.xml

Main.java

运行结果: