• 基于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开发人员可以试试;

  • 相关阅读:
    【转载】零基础学Support Vector Machine(SVM)
    【转载】前向传播算法(Forward propagation)与反向传播算法(Back propagation)
    python 3.5 解决csv 读入中的'utf-8' codec can't decode办法
    pandas用法大全
    南阳理工OJ 题目168.房间安排问题与题目14.会场安排问题
    C++中IO设置数字精度问题
    C++中memset()函数笔记
    Java 编程命名规范
    数据类型内存分配--js基础
    对象--js基础
  • 原文地址:https://www.cnblogs.com/zuowj/p/15778534.html
Copyright © 2020-2023  润新知