# 修复低版本 Spel 语句 BigDecimal 计算的问题
在使用低版本的 spring-expression 模块时就会导致在计算 BigDecimal 对象时转 int 导致结果错误
因为内部并未支持 BigDecimal 类型判断,导致走了 else 使用了 int 类型做计算
# 起因
因为一系列事情导致某个功能需要迁移到另一个系统,但因为迁移进去的系统 Spring 版本偏低,在测试的时发现使用的 Spel 语句处理数据有时会计算错误,经排查确认在这个低版本 spring-expression:3.2.8.RELEASE 版本不能支持 BigDecimal 表达式的计算,它将 BigDecimal 类型转换为 int 类型进行 加减乘除,大小比对等计算。
考虑到这个功能比较重要,也比较大而复杂,重构的成本太大了,在多方面分析下觉得仅修复 Spel 语句计算问题对业务逻辑影响是最小的,其次 Spring 的 Spel 是一个可以独立存在运行的工具类,对它的改动影响也是最小的。
# 分析 Spel 的设计与架构
我认为 Spel 设计上可以简单可以拆分四个维度,分别是:SpelNode (AST (具体执行节点))、Context (上下文)、Expression (执行器)、ExpressionParser (解释器)。
# SpelNode (AST (具体执行节点))
这个维度是存放一些表达式解析后的具体逻辑执行,如:加减乘除,调用类的方法,调用 Spring Bean 对象的方法等。
例子:OpPlus 类
我标记处就是这个版本的 OpPlus 对类型 BigDecimal 计算有误的原因,其他的也是同理
# Context (上下文)
这是一个扩展的上下文用的类,它内部包含对 Spel 字符串中的属性,方法,构造函数转换的方法。
如下:
public interface EvaluationContext { | |
/** | |
* 获取这个根对象的处理 | |
*/ | |
TypedValue getRootObject(); | |
/** | |
* 构造函数处理 | |
*/ | |
List<ConstructorResolver> getConstructorResolvers(); | |
/** | |
* 方法处理 | |
*/ | |
List<MethodResolver> getMethodResolvers(); | |
/** | |
* 属性处理 | |
*/ | |
List<PropertyAccessor> getPropertyAccessors(); | |
/** | |
* 定位数据类型的处理 | |
*/ | |
TypeLocator getTypeLocator(); | |
/** | |
* 类型转换的处理 | |
*/ | |
TypeConverter getTypeConverter(); | |
/** | |
* 类型比较器的处理 | |
*/ | |
TypeComparator getTypeComparator(); | |
/** | |
* 运算符的处理 | |
*/ | |
OperatorOverloader getOperatorOverloader(); | |
/** | |
* Spring Bean 的处理 | |
*/ | |
BeanResolver getBeanResolver(); | |
/** | |
* 变量的设置处理 | |
*/ | |
void setVariable(String name, Object value); | |
/** | |
* 查找变量的处理 | |
*/ | |
Object lookupVariable(String name); | |
} |
其中和类型有关的是 getTypeLocator、getTypeConverter、getTypeComparator 这三个方法,但不能处理 BigDecimal 类型的只有 getTypeComparator 这个比较方法,具体代码如下
# Expression (执行器)
执行器是对 Spel 真正进行处理的类,经过解析器 SpelExpressionParser 处理后的 Spel 表达式会产生这种执行器,内置对应的 SpelNode (AST (具体执行节点)) 。解析器默认返回的是 SpelExpression 。
# ExpressionParser (解释器)
结构图如下
其中的 SpelExpressionParser 是我们常用的 “入口”, 我一般使用方式如下
SpelExpressionParser spelExpressionParser = new SpelExpressionParser(); | |
SpelExpression spelExpression = spelExpressionParser.parseRaw("1+1"); | |
Object value = spelExpression.getValue(); | |
System.out.println(value); |
可以看出先通过解释器(SpelExpressionParser)去处理表达式生成执行器(SpelExpression)
# SpelExpressionParser 类代码
可以看出 SpelExpressionParser 与 InternalSpelExpressionParser 的区别是前者是线程安全的,后者不是,SpelExpressionParser 实现线程安全的本质是每个线程重新 new 了一个 InternalSpelExpressionParser 。返回的 Expression 执行器固定就是 SpelExpression。
# InternalSpelExpressionParser 类代码
这个类的图上这个方法就是解析表达式的,其中框起来的就是为了 Expression (执行器) 装载正确的 SpelNode (AST (具体执行节点)) , eatExpression 方法会进行解析的。
部分代码如下,上面是大小比较,下面是加减,还有其他的等等…
# 支持 Bigdecimal 具体实现
根据前面架构可以看出,我应该重写的对象,我必须明白一件事 Spring 的 Spel 是一个可以独立存在运行的工具类,并不由 Spring 管理。
# 步骤
- 将源码中 InternalSpelExpressionParser 类复制一份到自己的工作目录中,复制这个类时可能会因为引用其他类的非公共方法导致报错,可以将报错的类也复制一份在自己的工作目录中,如:Token、Tokenizer、TokenKind 这三个类
这个目的是用于替换 InternalSpelExpressionParser 类解析出来的 Expression (执行器) 装载我们自己的支持 BigDecimal 的 SpelNode (AST (具体执行节点))
- 将源码中 SpelExpressionParser 类也复制一份到自己的工作目录中,需要注意这个类使用的 InternalSpelExpressionParser 应该是前面复制的
之后使用时的入口将是这个类
-
复制不支持 BigDecimal 的 SpelNode (AST (具体执行节点)) 类,如 OpPlus、OpMinus 等,再在 github 的 OpPlus 上扣取想要的逻辑部分放在对应复制的类里面
举例:原来的结构
修改后
-
其中比较节点如 <、>、=<、>= 会使用到 StandardEvaluationContext 上下文的比较器,也就是前文说的 getTypeComparator 这个比较方法
可以看到被之前的 <、>、=<、>= 等 SpelNode 节点和新重写类引用,所以要对这个类进行改动(直接去找最新的 github 上 spring-expression 模块相同类这处逻辑拷贝)
-
StandardEvaluationContext 这个类可以直接考一份
# 修改总结
整体修改完善思路如上文,修改后目录如下,使用的时候就使用这个 SpelExpressionParser 就好了
## 总结
这次的笔记告诉了我,有些功能实现后,也许并没有我一开始想象的那么难以理解和畏惧,反复翻看源码能更加的理解它