基于javascript引擎封装实现算术表达式计算工具类
JAVA可动态计算表达式的框架非常多,比如:spEL、Aviator、MVEL、EasyRules、jsEL等,这些框架的编码上手程度、功能侧重点及执行性能各有优劣,网上也有大把的学习资料及示例代码,我这里也不在赘述了,本文要介绍的是直接借助于JDK中自带的ScriptEngineManager,使用javascript Engine来动态计算表达式,编码简单及执行性能接近原生JAVA,完全满足目前我公司的产品系统需求(通过配置计算公式模板,然后将实际的值带入公式中,最后计算获得结果),当然在实际的单元测试中发现,由于本质是使用的javascript 语法进行表达式计算,若有小数,则会出现精度不准确的情况(网上也有人反馈及给出了相应的解决方案),为了解决该问题,同时又不增加开发人员的使用复杂度,故我对计算过程进行了封装,计算方法内部会自动识别出表达式中的变量及数字部份,然后所有参与计算的值均通过乘以10000转换为整数后进行计算,计算的结果再除以10000以还原真实的结果,具体封装的工具类代码如下:
public class JsExpressionCalcUtils {
private static ScriptEngine getJsEngine() {
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
return scriptEngineManager.getEngineByName("javascript");
}
/**
* 普通计算,若有小数计算则可能会出现精度丢失问题,整数计算无问题
* @param jsExpr
* @param targetMap
* @return
* @throws ScriptException
*/
public static Double calculate(String jsExpr, Map<String, ? extends Number> targetMap) throws ScriptException {
ScriptEngine jsEngine = getJsEngine();
SimpleBindings bindings=new SimpleBindings();
bindings.putAll(targetMap);
return (Double) jsEngine.eval(jsExpr, bindings);
}
/**
* 精确计算,支持小数或整数的混合运算,不会存在精度问题
* @param jsExpr
* @param targetMap
* @return
* @throws ScriptException
*/
public static Double exactCalculate(String jsExpr, Map<String, ? extends Number> targetMap) throws ScriptException {
String[] numVars = jsExpr.split("[()*\\-+/]");
numVars = Arrays.stream(numVars).filter(StringUtils::isNotEmpty).toArray(String[]::new);
double fixedValue = 10000D;
StringBuilder stringBuilder = new StringBuilder();
for (String item : numVars) {
Number numValue = targetMap.get(item);
if (numValue == null) {
if (NumberUtils.isNumber(item)) {
jsExpr = jsExpr.replaceFirst("\\b" + item + "\\b", String.valueOf(Double.parseDouble(item) * fixedValue));
continue;
}
numValue = 0;
}
stringBuilder.append(String.format(",%s=%s",item, numValue.doubleValue() * fixedValue));
}
ScriptEngine jsEngine = getJsEngine();
String calcJsExpr = String.format("var %s;%s;", stringBuilder.substring(1), jsExpr);
double result = (double) jsEngine.eval(calcJsExpr);
System.out.println("calcJsExpr:" + calcJsExpr +",result:" + result);
return result / fixedValue;
}
}
如上代码所示,calculate方法是原生的js表达式计算,若有小数则可能会有精度问题,而exactCalculate方法是我进行封装转换为整数进行计算后再还原的方法,无论整数或小数进行计算都无精度问题,具体见如下单元测试的结果:
@Test
public void testJsExpr() throws ScriptException {
Map<String,Double> numMap=new HashMap<>();
numMap.put("a",0.3D);
numMap.put("b",0.1D);
numMap.put("c",0.2D);
//0.3-(0.1+0.2) 应该为 0.0,实际呢?
String expr="a-(b+c)";
Double result1= JsExpressionCalcUtils.calculate(expr,numMap);
System.out.println("result1:" + result1);
Double result2= JsExpressionCalcUtils.exactCalculate(expr,numMap);
System.out.println("result2:" + result2);
}
result1:-5.551115123125783E-17 ---这不符合预期结果
calcJsExpr:var a=3000.0,b=1000.0,c=2000.0;a-(b+c);,result:0.0
result2:0.0 ---符合预期结果
2021-01-19补充,经过实际多场景测试,发现上述JS表达式(取整再运算)并未达到实际效果,在除法运算时仍会产生小数导致不准确,故转而采用spEL表达式并进行改良后,以确保计算准确,代码如下:
private static Field typedValueField = null;
private static Field typedValueDescriptorField = null;
static {
typedValueField = ReflectionUtils.findField(TypedValue.class, "value");
typedValueDescriptorField = ReflectionUtils.findField(TypedValue.class, "typeDescriptor");
Assert.state(typedValueField != null && typedValueDescriptorField != null, "not found TypedValue field[value,typeDescriptor] !");
typedValueField.setAccessible(true);
typedValueDescriptorField.setAccessible(true);
}
/**
* 基于spring Expression【精确计算】算术表达式并获得结果,运算过程凡涉及数字均转换为BigDecimal类型,整个过程均以BigDecimal的高精度进行运算,确保精度正常(推荐使用)
*
* @param exprString
* @param targetMap
* @return
*/
public static Double exactCalculate(String exprString, Map<String, ?> targetMap) {
return exactCalculate(exprString, targetMap, false, null);
}
/**
* 基于spring Expression【精确计算】算术表达式并获得结果,运算过程凡涉及数字均转换为BigDecimal类型,整个过程均以BigDecimal的高精度进行运算,确保精度正常(推荐使用)
*
* @param exprString
* @param targetMap
* @return
*/
public static Double exactCalculate(String exprString, Map<String, ?> targetMap, boolean ignoreNonexistentKeys, Object defaultIfNull) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setOperatorOverloader(NumberOperatorOverloader.DEFAULT);
context.addPropertyAccessor(MapPropertyAccessor.DEFAULT.setOptions(ignoreNonexistentKeys, defaultIfNull));
//这里将目标入参MAP作为spring表达式的根对象,则表达式中可以直接使用属性即可
context.setRootObject(targetMap);
try {
SpelExpression spExpression = (SpelExpression) parser.parseExpression(exprString);
if (spExpression == null) {
throw new ApplicationException(Constants.DEFAULT_ERROR_CODE, "解析spring表达式失败:" + exprString);
}
if (spExpression.getAST() != null) {
numberLiteralToBigDecimal(spExpression.getAST());
}
BigDecimal result = spExpression.getValue(context, BigDecimal.class);
return result.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
} catch (Exception e) {
LOGGER.error("算术表达式语法执行错误:{},表达式:{},目标入参:{}", e.getMessage(), exprString, JsonUtils.deserializer(targetMap));
throw new ApplicationException(Constants.DEFAULT_ERROR_CODE, "算术表达式语法执行错误,原因:" + e.getMessage());
}
}
/**
* 内部辅助方法:将表达式解析后的树中包含有数值字面量的统一转换为BigDecimal,确保精度不丢失
*
* @param spelNode
*/
private static void numberLiteralToBigDecimal(SpelNode spelNode) {
if (spelNode == null) {
return;
}
if (spelNode instanceof Literal) {
TypedValue typedValue = ((Literal) spelNode).getLiteralValue();
if (typedValue != null && typedValue.getValue() instanceof Number) {
try {
//将表达式中数字字面量的值转换为BigDecimal,以便参与运算时精度不会丢失
typedValueField.set(typedValue, NumberUtils.createBigDecimal(typedValue.getValue().toString()));
typedValueDescriptorField.set(typedValue, null);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
if (spelNode.getChildCount() > 0) {
for (int i = 0; i < spelNode.getChildCount(); i++) {
numberLiteralToBigDecimal(spelNode.getChild(i));
}
}
}
/**
* 数字类型运算符重载操作类(目前实际并没有生效)
*/
private static class NumberOperatorOverloader implements OperatorOverloader {
private static final List<Operation> OVERRIDE_OPERATIONS = Arrays.asList(Operation.ADD, Operation.SUBTRACT, Operation.DIVIDE, Operation.MULTIPLY);
public static NumberOperatorOverloader DEFAULT = new NumberOperatorOverloader();
@Override
public boolean overridesOperation(Operation operation, Object o, Object o1) throws EvaluationException {
return OVERRIDE_OPERATIONS.contains(operation) && o instanceof Number && o1 instanceof Number;
}
@Override
public Object operate(Operation operation, Object o, Object o1) throws EvaluationException {
BigDecimal leftNumber = NumberUtils.createBigDecimal(o.toString());
BigDecimal rightNumber = NumberUtils.createBigDecimal(o1.toString());
switch (operation) {
case ADD: {
return leftNumber.add(rightNumber);
}
case SUBTRACT: {
return leftNumber.subtract(rightNumber);
}
case DIVIDE: {
return leftNumber.divide(rightNumber, RoundingMode.HALF_UP);
}
case MULTIPLY: {
return leftNumber.multiply(rightNumber);
}
default: {
return BigDecimal.ZERO;
}
}
}
}
/**
* MAP属性访问器,确保spring表达式中可以直接指明key,而无需使用['key']这种模式
*/
private static class MapPropertyAccessor implements PropertyAccessor {
public static final MapPropertyAccessor DEFAULT = new MapPropertyAccessor();
private boolean ignoreNonexistentKeys = false;
private Object defaultIfNull = null;
/**
* 设置选项
*
* @param ignoreNonexistentKeys 忽略不存在KEY
* @param defaultIfNull 如果没空时的默认值
*/
public MapPropertyAccessor setOptions(boolean ignoreNonexistentKeys, Object defaultIfNull) {
this.ignoreNonexistentKeys = ignoreNonexistentKeys;
this.defaultIfNull = defaultIfNull;
if (this.defaultIfNull != null && this.defaultIfNull instanceof Number) {
//如果指定的默认值不为空,且为数字类型,则直接转换为BigDecimal类型
this.defaultIfNull = NumberUtils.createBigDecimal(this.defaultIfNull.toString());
}
return this;
}
@Override
public Class<?>[] getSpecificTargetClasses() {
return new Class[]{Map.class};
}
@Override
public boolean canRead(EvaluationContext context, Object target, String name) throws AccessException {
return (target instanceof Map && (((Map<?, ?>) target).containsKey(name) || ignoreNonexistentKeys));
}
@Override
public TypedValue read(EvaluationContext context, Object target, String name) throws AccessException {
Assert.state(target instanceof Map, "参数不是Map类型");
Map<?, ?> map = (Map<?, ?>) target;
if (!map.containsKey(name) && !ignoreNonexistentKeys) {
throw new AccessException("Map中未包含该key: " + name);
}
Object value = map.get(name);
if (value == null) {
value = defaultIfNull;
} else if (value instanceof Number) {
//为保证精度不丢失,若为数字类型就转换为BigDecimal类型
value = NumberUtils.createBigDecimal(value.toString());
}
return new TypedValue(value);
}
@Override
public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException {
return false;
}
@Override
public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException {
}
}
核心就是将所有的涉及数字类型转为BigDecimal进行运算,sp EL内部操作符会对BigDecimal进行专门的运算处理,从而确保精度正常。
顺便说一下,.NET(C#)语言也是支持执行javascript表达式的哦,当然也可以实现上述的求值表达式工具类,实现思路相同,有兴趣的.NET开发人员可以试试;
基于javascript引擎封装实现算术表达式计算工具类的更多相关文章
- Go/Python/Erlang编程语言对比分析及示例 基于RabbitMQ.Client组件实现RabbitMQ可复用的 ConnectionPool(连接池) 封装一个基于NLog+NLog.Mongo的日志记录工具类LogUtil 分享基于MemoryCache(内存缓存)的缓存工具类,C# B/S 、C/S项目均可以使用!
Go/Python/Erlang编程语言对比分析及示例 本文主要是介绍Go,从语言对比分析的角度切入.之所以选择与Python.Erlang对比,是因为做为高级语言,它们语言特性上有较大的相似性, ...
- BigDecimal精确计算工具类
前言 在实际开发中,遇到例如货币,统计等商业计算的时候,一般需要采用java.math.BigDecimal类来进行精确计算.而这类操作通常都是可预知的,也就是通用的.所以,写了个工具类来方便以后的工 ...
- 自己封装的poi操作Excel工具类
自己封装的poi操作Excel工具类 在上一篇文章<使用poi读写Excel>中分享了一下poi操作Excel的简单示例,这次要分享一下我封装的一个Excel操作的工具类. 该工具类主要完 ...
- 为什么要用Hibernate框架? 把SessionFactory,Session,Transcational封装成包含crud的工具类并且处理了事务,那不是用不着spring了?
既然用Hibernate框架访问管理持久层,那为何又提到用Spring来管理以及整合Hibernate呢?把SessionFactory,Session,Transcational封装成包含crud的 ...
- 分享基于MemoryCache(内存缓存)的缓存工具类,C# B/S 、C/S项目均可以使用!
using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Caching; usi ...
- 一、JDBC的概述 二、通过JDBC实现对数据的CRUD操作 三、封装JDBC访问数据的工具类 四、通过JDBC实现登陆和注册 五、防止SQL注入
一.JDBC的概述###<1>概念 JDBC:java database connection ,java数据库连接技术 是java内部提供的一套操作数据库的接口(面向接口编程),实现对数 ...
- Java-精确计算工具类
import java.math.BigDecimal; import java.math.RoundingMode; /** * 精确计算工具类(加,减,乘,除,返回较大值,返回较小值) */ pu ...
- java精确计算工具类
java精确计算工具类 import java.math.BigDecimal; import java.math.RoundingMode; import java.math.BigDecimal; ...
- 基于数组阻塞队列 ArrayBlockingQueue 的一个队列工具类
java语言基于ArrayBlockingQueue 开发的一个根据特定前缀和后缀的队列.每天自动循环生成. 1.定义队列基类 Cookie package com.bytter.util.queue ...
- JavaScript学习系列博客_31_JavaScript Math 工具类
Math - Math属于一个工具类,它不需要我们创建对象(例如Date日期对象需要通过构造函数创建对象 var 变量=new Date(),Math不需要),它里边封装了属性运算相关的常量和方法 我 ...
随机推荐
- 企业如何利用 Serverless 快速扩展业务系统?
2022 年 9 月 24 日,阿里云用户组(AUG)第 12 期活动在厦门举办.活动现场,阿里云高级技术专家史明伟(花名:世如)向参会企业代表分享了<未来已来--从技术升级到降本提效>. ...
- BFS 广搜
HDU 2612 #include<stdio.h> #include<string.h> #include<iostream> #include<queue ...
- 3. Oracle数据库异常关闭,导致错误3. Oracle数据库异常关闭,导致错误ERROR: ORA-01034: ORACLE ngt available; ORA-27101: shared memory realm does not exist
之前由于电脑没电,强制关机,导致Oracle数据库异常关闭,再次启动电脑登陆数据库时,发生以下错误: 当我尝试重新启动数据库时,发生错误: 经过查阅资料后得知:缺少INITXE.ORA文件,需要从下图 ...
- 【scikit-learn基础】--『回归模型评估』之偏差分析
模型评估在统计学和机器学习中具有至关重要,它帮助我们主要目标是量化模型预测新数据的能力. 本篇主要介绍模型评估时,如何利用scikit-learn帮助我们快速进行各种偏差的分析. 1. **R² ** ...
- [转帖]nginx 剖析 request_time和upstream_response_time的误区、区别
https://cloud.tencent.com/developer/article/1767981 首先,澄清一个误区 upstream_response_time必须在upstream配置时才能 ...
- [转帖]十分钟掌握 Vim 编辑器核心功能
https://juejin.cn/post/6929248764746006535 前言 相信不论是前端还是后台多多少少都需要上到服务器上做一些操作,改改配置文件等,大多数 Linux 服务器默认都 ...
- [转帖]oracle 11g 分区表创建(自动按年、月、日分区)
https://www.cnblogs.com/yuxiaole/p/9809294.html 前言:工作中有一张表一年会增长100多万的数据,量虽然不大,可是表字段多,所以一年下来也会达到 1G ...
- [转帖]如何在一个Docker中同时运行多个程序进程?
https://cloud.tencent.com/developer/article/1683445 我们都知道Docker容器的哲学是一个Docker容器只运行一个进程,但是有时候我们就是需要在一 ...
- [转帖]SQL Server超过了每行的最大字节数(8060)的原因和解决办法
一.现象 出现这种错误都发生在SQL语句建表时,错误提示: "警告: 已创建表 'XXXX,但其最大行大小(10438)超过了每行的最大字节数(8060).如果结果行长度超过 806 ...
- 【转帖】71.常用的显示GC日志的参数、GC日志分析、日志分析工具的使用
目录 1.常用的显示GC日志的参数 2.图解垃圾`GC`日志(重要) 3.日志分析工具的使用 1.常用的显示GC日志的参数 解释: 日志中,GC和Full GC表示的是GC的类型.GC只在新生代进行, ...