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
小于等于,不区分大小写。
逻辑运算符:&&
逻辑与、||
逻辑或、!
逻辑非,等价关键字为 and
、or
、not
,也是不区分大小写。
字符串操作:与 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_VALUE
、T(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.prop
、obj['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
运行结果: