天天看點

基于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 ---符合預期結果

順便說一下,.NET(C#)語言也是支援執行javascript表達式的哦,當然也可以實作上述的求值表達式工具類,實作思路相同,有興趣的.NET開發人員可以試試;

繼續閱讀