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引擎封装实现算术表达式计算工具类的更多相关文章

  1. Go/Python/Erlang编程语言对比分析及示例 基于RabbitMQ.Client组件实现RabbitMQ可复用的 ConnectionPool(连接池) 封装一个基于NLog+NLog.Mongo的日志记录工具类LogUtil 分享基于MemoryCache(内存缓存)的缓存工具类,C# B/S 、C/S项目均可以使用!

    Go/Python/Erlang编程语言对比分析及示例   本文主要是介绍Go,从语言对比分析的角度切入.之所以选择与Python.Erlang对比,是因为做为高级语言,它们语言特性上有较大的相似性, ...

  2. BigDecimal精确计算工具类

    前言 在实际开发中,遇到例如货币,统计等商业计算的时候,一般需要采用java.math.BigDecimal类来进行精确计算.而这类操作通常都是可预知的,也就是通用的.所以,写了个工具类来方便以后的工 ...

  3. 自己封装的poi操作Excel工具类

    自己封装的poi操作Excel工具类 在上一篇文章<使用poi读写Excel>中分享了一下poi操作Excel的简单示例,这次要分享一下我封装的一个Excel操作的工具类. 该工具类主要完 ...

  4. 为什么要用Hibernate框架? 把SessionFactory,Session,Transcational封装成包含crud的工具类并且处理了事务,那不是用不着spring了?

    既然用Hibernate框架访问管理持久层,那为何又提到用Spring来管理以及整合Hibernate呢?把SessionFactory,Session,Transcational封装成包含crud的 ...

  5. 分享基于MemoryCache(内存缓存)的缓存工具类,C# B/S 、C/S项目均可以使用!

    using System; using System.Collections.Generic; using System.Linq; using System.Runtime.Caching; usi ...

  6. 一、JDBC的概述 二、通过JDBC实现对数据的CRUD操作 三、封装JDBC访问数据的工具类 四、通过JDBC实现登陆和注册 五、防止SQL注入

    一.JDBC的概述###<1>概念 JDBC:java database connection ,java数据库连接技术 是java内部提供的一套操作数据库的接口(面向接口编程),实现对数 ...

  7. Java-精确计算工具类

    import java.math.BigDecimal; import java.math.RoundingMode; /** * 精确计算工具类(加,减,乘,除,返回较大值,返回较小值) */ pu ...

  8. java精确计算工具类

    java精确计算工具类 import java.math.BigDecimal; import java.math.RoundingMode; import java.math.BigDecimal; ...

  9. 基于数组阻塞队列 ArrayBlockingQueue 的一个队列工具类

    java语言基于ArrayBlockingQueue 开发的一个根据特定前缀和后缀的队列.每天自动循环生成. 1.定义队列基类 Cookie package com.bytter.util.queue ...

  10. JavaScript学习系列博客_31_JavaScript Math 工具类

    Math - Math属于一个工具类,它不需要我们创建对象(例如Date日期对象需要通过构造函数创建对象 var 变量=new Date(),Math不需要),它里边封装了属性运算相关的常量和方法 我 ...

随机推荐

  1. springboot项目pom文件设置<profile>读取不同的配置文件(网上瞎copy的大把,测试过真实可用)

    这篇文章主要介绍关于springboot如何通过多个properties实现数据库环境部署时自动切换配置的两种方式,部署时切换方式和打包时Maven控制方式.

  2. NSSCTF Round#13 web专项

    rank:3 flask?jwt? 简单的注册个账号,在/changePassword 下查看页面源代码发现密钥<!-- secretkey: th3f1askisfunny --> ,很 ...

  3. rem在手机移动端app中的兼容适配问题

    这是我之前一直使用的第一种rem方案.贴代码 1 <script> 2 // 适用于750的设计稿 3 var iScale = 1; 4 // 通过页面加载的时候去获取用户设备的物理像素 ...

  4. C#设计模式11——享元模式的写法

    1. 什么是享元模式? 享元模式是一种结构型设计模式,目的是通过共享对象来尽量减少内存使用和对象数量.它通过将对象分为可共享的和不可共享的来实现这一目的. 2. 为什么要使用享元模式? 使用享元模式可 ...

  5. 小白学标准库之 http

    1. 前言 标准库是工具,是手段,是拿来用的.一味的学标准库就忽视了语言的内核,关键.语言层面的特性,内存管理,垃圾回收.数据结构,设计模式.这些是程序的内核,要熟练,乃至精通它们,而不是精通标准库. ...

  6. 每天学五分钟 Liunx 011 | sudo

    回顾前两节,在 001 中介绍了怎么添加用户及用户组,在 010 中介绍了从 client 端 ssh 到 server 的详细过程,那么接下来要介绍的就是是登陆到 server 之后如何切换用户了. ...

  7. NCC Mocha v0.10 发布, .NET 开发的基于 OpenTelemetry 的 APM 系统

    目录 项目简介 项目进度 v0.10 发布内容 项目背景 平台功能 技术架构 v0.10 快速体验 启动项目 Trace 数据的发送 配置 Jaeger 数据源 Trace 数据的查询 项目简介 Mo ...

  8. AXI Channel

    AXI Channel axi与ahb不同就是分为不同的channel write address channel - 表明一个transaction基本的属性,包含本次传输的地址\类型\大小(多少字 ...

  9. CSS - 使图片自适应

    img {         height: 100%;         object-fit: cover; }

  10. MyBatis_问题解决:Invalid bound statement (not found)

    Invalid bound statement (not found)问题,即在mybatis中dao接口与mapper配置文件在做映射绑定的时候出现问题,简单说,就是接口与xml要么是找不到,要么是 ...