读者可以根据下面这个例子自行进行调试:

  1. @Test
  2. public void testSpel() {
  3. String expressionStr = "1 + 2 * (3 + 6)";
  4. ExpressionParser parpser = new SpelExpressionParser(); //SpelExpressionParser是Spring内部对ExpressionParser的唯一最终实现类
  5. Expression exp = parpser.parseExpression(expressionStr); //把该表达式,解析成一个Expression对象:SpelExpression
  6. System.out.println(exp.getValue());
  7. }

根据上面这个例子我们可以看到解析的大概过程,首先就是先将一个表达式放到ExpressionParser(表达式解析器)进行解析得到一个Expression(表达式对象),然后就可以通过这个Expression对象获取结果等等操作。我们来看看上面这个公式最后面解析出来的结构,如下图所示,表达式会被解析成为一个标准二叉树,父节点为操作符,最底左右节点为操作数。整个计算过程是通过自左向右,自底向上进行,先递归计算获取左操作数,然后再递归计算获取右操作数,最后进行根节点计算,比如这里的加操作。
image.pngimage.png


ok..接下来我们来看看代码层面的逻辑,一开始看可能会比较头晕,因为各种递归和判断。
我们先来介绍核心类及作用

  • TemplateAwareExpressionParser : 核心解析器,SpelExpressionParser每次调用都会创建一个TemplateAwareExpressionParser,因此我们也能看到SpelExpressionParser官方的注释线程安全的。
  • Tokenizer:语法分词器,将表达式分成一个个Token记录起来
  • Token:表达式最小单元,用于记录表达式分词之后的表达式,主要有该单元的数据、数据类型,以及位于表达式的哪个位置
  • SpelNodeImpl:AST(抽象语法树)的节点,从这个节点上延伸处理多种类型节点,用于支持各种操作,比如OpPlus类型就是为了支持加法运算的节点,StringLiteral用于存放字符串的节点等等。

image.png

  • SpelExpression:Spel表达式,实际表达式运算入口

Tokenizer分词逻辑

读者在阅读Tokenizer的时候可能会被吓到或者无从下手,其实逻辑并不复杂,其实就是遍历整个表达式,然后以穷举的方式为表达式中的每个元素赋予类型,比如我们看到下面这个第一个,如果发现当前这个字符为’+’,则会去判断下一个字符是不是也是’+’,如果是则将这两个字符放一起,归为自增操作符,如果只有单’+’,则判断为加法操作符,以此类推穷举出所有可能的操作符。
image.png
然后我们再来比如下面这个字符串操作数的获取,先判断是不是单引号或者双引号,如果是的话就去找到下一个下一个对应的单号或者双引号,如果找到的话就将这中间的数据取出来设置为字符串操作数,若没找到则抛出语法异常。
image.png
image.png

InternalSpelExpressionParser表达式解析器

上面的Tokenizer通过遍历的方式获取到整个表达式的各个操作数/操作符,然后接下来就是将这些元素根据一定的逻辑(比如数学计算逻辑),将表达式变为一颗合法正确的抽象语法树,具体实现逻辑位于eatExpression方法中,可以参考下面这个图基本上越往下优先级就越高,比如最开始的or逻辑优先级要比and的优先级低,再比如eatSumExpression方法用于处理加减法,而eatProductExpression处理乘除取余,则eatProductExpression优先级更高。
image.png
具体实现逻辑实际可以参考官方使用说明文档中的具体用法进行反向逻辑解析,下面是官方文档说明的spel支持特性,包括基本数学运算符、方法调用、Bean调用、逻辑运算、列表集合操作等。
image.png
这里我们拿其中的一个方法进行解析,其他的读者可以自行调试看看,逻辑可能比较绕但是并不深奥,也是用的穷举的方式来实现功能。
image.png
上图是加减法的实现,其他的运算符也差不多这种结构,两个eatProductExpression组装成操作符的左右两个节点,然后拼装成一个新节点就如文章开头那样
image.png
3和6对应了两个eatProductExpression,new OpPlus对应新的父节点。

实现细节

  • 入栈(push)/出栈(pop) : 解析器使用Deque(双端队列)来实现堆栈效果,尽量使用Deque来实现堆栈,Stack属于比较早期遗留的类。

至此整个语法树构建完成,最复杂部分实际上到这里已经结束,接下来就是遍历整个树将整个树自左向右,自底向上进行对应操作即可。

Spel表达式官方说明文档:https://docs.spring.io/spring-framework/docs/3.0.x/reference/expressions.html