1.初识spel
在spring项目中大家都用到过@Value注解,用来将application文件中的配置值绑定到field上,其中@Value中的字符串便是使用了spel表达式。
通过spel表达式,开发者无需对变量手动赋值,便可将配置文件中指定的配置项赋值给变量,降低了配置的复杂度,提高了灵活性。
2.简单语法
- 对象.属性 @Value注解中,开发最常用的就是通过${a.b}的方式,将a.b对应配置的值注入field。偶尔会碰到如果没有配置项,就赋默认值的情况,可以通过${a.b:helloworld}的语法,在 ":"后写入默认值。
"${}"的语法更多运用于配置项占位替换, 使用"#{表达式}"的语法可以解锁更多高阶应用。
先来看一段简单的代码。
与${}的语法看似没有什么不同,表达式的内容也一模一样。但spring的上下文中配置了一个名为"test"的map,且map有"username"和"password"两个key。显然"hello"和"world"被注入了"username"和"password"。
这种方式同时也适用于读取对象的field的值。通过这种方式,@Value的灵活性就被放大了,开发可以脱离application文件,自由通过spring上下文读取配置值。
- 字面量与运算符 spel支持字面量赋值,以及+-*/等算数运算符,><==等关系运算符,在此不多赘述。
- Elivis运算符 "${a.b:helloworld}"可以实现找不到配置赋默认值,在"#{}"语法中同样可以通过"?:"实现。
3.自由扩展
@Value只能用于赋值,对一个追求自由的程序员来说肯定是远远不够的。显然spel提供的语法可以用在模板替换中,取代String.replace和String.format。
来看一段简单的应用,实现了对表达式"#order.detail.detailId"的取值,"#"已经了解过了,即从上下文中获取对象,"#order.detail.detailId"可以解释为从上下文中获取order对象的detail字段的detailId字段。
这里我们主要用到了ExpressionParser,StandardEvaluationContext和Expression三个对象。 ExpressionParser顾名思义是表达式解析器,用于将表达式解释成代码实现。 StandardEvaluationContext是上下文,用于读写值。Expression则是表达式,通过getValue方法从上下文取值。
细心的读者已经发现了,此处的"#"后并没有紧跟"{}",表达式也不在"{}"内。其实spel本身语法就是用"#"取值,"#{}"的作用其实是提取表达式,需要结合ParserContext使用,那我们来看看ParserContext。
与上面的代码差别不大,只是parseExpression的参数多了一个ParseContext,表达式变成了#{order.detail.detailId}。但其实这段代码运行是会抛异常的。
通过阅读代码异常,我们可以推测,表达式执行过程中在null上取了order字段,显然是有问题的,我们的预期应该是从order对象取值,而此处order成了一个对象的字段了。
通过debug发现,表达式变成了order.detail.detailId,但根据上文提到的,"#"才是从上下文取对象。通过debug和观察StandardEvaluationContext发现有个setRootObject的方法,将代码修改为:
异常也变了
由此可以推断出使用"#{}"会将"{}"包裹的表达式提取出来,并从rootObject取值。其实反过来想,如果表达式写成"#{#order.detail.detailId}"不也能解决问题吗?但是一个优雅的开发一定不会这么做的,太丑了。那有没有别的方式解决呢?自然是有的。
将rootObject定义为一个map,再往root中塞值,那既不破坏现有规则,又能实现我们需求的优雅。需要注意的是StandardEvaluationContext.addPropertyAccessor方法,因为默认是从对象的field取值的,显然map没有order这个field,因此我们重新定义了map作为rootObject的取值方法,即通过map.get方法读取。
来看下最终实现的结果:
4.不足之处
由于笔者的需求需要在表达式上定义取出来的值的类型,如mybatis的xml写sql一样,#{username,jdbcType=VARCHAR}。但目前研究下来只能通过Expression.getValue方法硬编码指定读取类型。希望各位读者如有知晓的多多指教。