修复低版本 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(具体执行节点))

20220928225610
这个维度是存放一些表达式解析后的具体逻辑执行,如:加减乘除,调用类的方法,调用 Spring Bean 对象的方法等。
例子:OpPlus 类
20220929001714
我标记处就是这个版本的 OpPlus 对类型 BigDecimal 计算有误的原因,其他的也是同理

Context(上下文)

20220929002028
这是一个扩展的上下文用的类,它内部包含对 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 这个比较方法,具体代码如下
20221007122112

Expression(执行器)

20220929001919
执行器是对 Spel 真正进行处理的类,经过解析器 SpelExpressionParser 处理后的 Spel 表达式会产生这种执行器,内置对应的 SpelNode(AST(具体执行节点)) 。解析器默认返回的是 SpelExpression 。

ExpressionParser(解释器)

结构图如下
uTools_1664373970335
其中的 SpelExpressionParser 是我们常用的“入口”,我一般使用方式如下

SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
SpelExpression spelExpression = spelExpressionParser.parseRaw("1+1");
Object value = spelExpression.getValue();
System.out.println(value);

可以看出先通过解释器(SpelExpressionParser)去处理表达式生成执行器(SpelExpression)

SpelExpressionParser 类代码

20221007124133
可以看出 SpelExpressionParser 与 InternalSpelExpressionParser 的区别是前者是线程安全的,后者不是,SpelExpressionParser 实现线程安全的本质是每个线程重新 new 了一个 InternalSpelExpressionParser 。返回的 Expression 执行器固定就是 SpelExpression。

InternalSpelExpressionParser 类代码

20221008215231
这个类的图上这个方法就是解析表达式的,其中框起来的就是为了 Expression(执行器) 装载正确的 SpelNode(AST(具体执行节点)) , eatExpression 方法会进行解析的。
部分代码如下,上面是大小比较,下面是加减,还有其他的等等...
20221008215043

支持 Bigdecimal 具体实现

根据前面架构可以看出,我应该重写的对象,我必须明白一件事 Spring 的 Spel 是一个可以独立存在运行的工具类,并不由 Spring 管理。

步骤

  1. 将源码中 InternalSpelExpressionParser 类复制一份到自己的工作目录中,复制这个类时可能会因为引用其他类的非公共方法导致报错,可以将报错的类也复制一份在自己的工作目录中,如:Token、Tokenizer、TokenKind 这三个类

这个目的是用于替换 InternalSpelExpressionParser 类解析出来的 Expression(执行器) 装载我们自己的支持 BigDecimal 的 SpelNode(AST(具体执行节点))

  1. 将源码中 SpelExpressionParser 类也复制一份到自己的工作目录中,需要注意这个类使用的 InternalSpelExpressionParser 应该是前面复制的

之后使用时的入口将是这个类

  1. 复制不支持 BigDecimal 的 SpelNode(AST(具体执行节点)) 类,如 OpPlus、OpMinus 等,再在 github的OpPlus 上扣取想要的逻辑部分放在对应复制的类里面
    举例:原来的结构
    20221009080354
    修改后
    20221009080624

  2. 其中比较节点如<、>、=<、>=会使用到 StandardEvaluationContext 上下文的比较器,也就是前文说的 getTypeComparator 这个比较方法
    20221009232515
    可以看到被之前的 <、>、=<、>= 等 SpelNode 节点和新重写类引用,所以要对这个类进行改动(直接去找最新的 github 上 spring-expression 模块相同类这处逻辑拷贝)
    20221009233541

  3. StandardEvaluationContext 这个类可以直接考一份

修改总结

整体修改完善思路如上文,修改后目录如下,使用的时候就使用这个 SpelExpressionParser 就好了
20221010081137

##总结
这次的笔记告诉了我,有些功能实现后,也许并没有我一开始想象的那么难以理解和畏惧,反复翻看源码能更加的理解它