使用ScriptEngineManager实现公式灵活计算

214 阅读1分钟

【开发需求】

  • 数据源配置公式,根据公式计算。
例如:($HLXX1_1_2.P+$HLXX1_1_3.P)*($HLXX1_1_4.P+$HLXX1_1_5.P+$HLXX1_1_6.P)

解决方式及代码实现

使用 ScriptEngineManager 获取一个 JavaScript 引擎,并且传入计算公式进行计算。通过调用 engine.eval(formula) 方法,可以得到表达式的计算结果并输出。 ScriptEngineManager 是 Java 中用于执行脚本的类,其中的 JavaScript 引擎可以用来解析和执行简单的数学表达式。

【代码实现】

配置ScriptEngineConfig

@Configuration  
public class ScriptEngineConfig {  
  
    private ScriptEngineManager scriptEngineManager = new ScriptEngineManager();  
    private ScriptEngine javaScript = scriptEngineManager.getEngineByName("JavaScript");  

    public ScriptEngine getScriptEngine() {  
        return javaScript;  
    }  
  
}

编写测试代码

@Slf4j  
public class ScriptEngineManagerTest {  
  
    @Autowired  
    private ScriptEngineConfig scriptEngineConfig;  

    @Test  
    public void scriptEngineManagerTest() {  
    String formula = "($HLXX1_1_2.P+$HLXX1_1_3.P)*($HLXX1_1_4.P+$HLXX1_1_5.P+$HLXX1_1_6.P)";  
    String formulaName = "ManagerTest";  
    // 获取公式不重复测点列表  
    List<String> pointList = getDistinctPointListFromFormula(formula);  
    Double value = calculate(formula, formulaName, pointList);  
    System.out.println("公式计算结果为:" + value);  
    }  

    /**  
    * 根据公式计算  
    *  
    * @param formula 公式  
    * @param formulaName 公式名称  
    * @param pointList 测点  
    */  
    private Double calculate(String formula, String formulaName, List<String> pointList) {  
        // 加载公式  
        String formulaScript = "function " + formulaName + "(ruleData){\n" +  
            " return " + formula.replaceAll("\\$", "ruleData.") + ";\n" +  
        "}";  
        log.info("公式脚本: {}", formulaScript);  
        //将公式脚本添加/更新到脚本引擎  
        ScriptEngine scriptEngine = scriptEngineConfig.getScriptEngine();  
        try {  
            scriptEngine.eval(formulaScript);  
        } catch (ScriptException e) {  
            e.printStackTrace();  
        }  
        Invocable invocable = (Invocable) scriptEngine;  
        // 自定义模拟数据 <仪表ID、测点ID、值>  
        Map<String, Map<String, Double>> dataTable = getSimulationData(pointList);  
        Double formulaResult = null;  
        try {  
            //传入公式名称、数据对象,执行  
            log.info("传入数据对象: {}", dataTable);  
            Object result = invocable.invokeFunction(formulaName, dataTable);  
            if (result == null) {  
                log.info("当前公式脚本执行结果为 NULL, 脚本: {}, 传入的值: {}", scriptEngine.get(formulaName), dataTable);  
            } else {  
                log.info("当前公式脚本执行结果为: {},结果类型为: {}", result, result.getClass());  
                if (!(result instanceof Double)) {  
                    log.info("当前公式脚本执行结果为 {}, 结果类型为 {}, 不是 Number 类型, 脚本: {}, 传入的值: {}", result, result.getClass(), scriptEngine.get(formulaName), dataTable);  
                } else {  
                    Double resultVal = (Double) result;  
                    if (!resultVal.isInfinite()) {  
                        formulaResult = MathUtil.round(resultVal, 3);  
                    }  
                }  
            }  
        } catch (Exception e) {  
            log.error("当前公式脚本执行异常", e);  
        }  
        return formulaResult;  
    }  

    /**  
    * 获取模拟数据  
    *  
    * @param pointList 测点列表  
    */  
    private Map<String, Map<String, Double>> getSimulationData(List<String> pointList) {  
        Table<String, String, Double> table = HashBasedTable.create();  
        Random random = new Random();  
        for (String point : pointList) {  
            String[] pArray = point.split("\\.");  
            String meterId = pArray[0];  
            String pointId = pArray[1];  
            table.put(meterId, pointId, random.nextDouble());  
        }  
        return table.rowMap();  
    }  

    /**  
    * 从公式中解析出测点列表并去重  
    *  
    * @param formula 公式,示例:($HLXX1_1_2.P+$HLXX1_1_3.P)*($HLXX1_1_4.P+$HLXX1_1_5.P+$HLXX1_1_6.P)  
    * @return 结果为去重后的测点列表,示例:[HLXX1_1_2.P, HLXX1_1_3.P, HLXX1_1_4.P, HLXX1_1_5.P, HLXX1_1_6.P]  
    */
    private List<String> getDistinctPointListFromFormula(String formula) {  
        //以$开头,后面跟字母、数字、下划线、点,以非字母、数字、下划线、点结束  
        String regEx = "\\$([?=A-Za-z0-9_.]*(?![A-Za-z0-9_.]))";  
        Pattern pattern = Pattern.compile(regEx);  
        Matcher matcher = pattern.matcher(formula);  
        List<String> list = new ArrayList<>();  
        while (matcher.find()) {  
            String str = matcher.group();  
            //删除开头的$符号  
            str = str.substring(1);  
            list.add(str);  
        }  
        //去重  
        LinkedHashSet<String> set = new LinkedHashSet<>(list);  
        list = new ArrayList<>(set);  
        return list;  
    }  
}

【问题发现】 在Java 15及之后的版本中,Nashorn JavaScript 引擎已被移除。Nashorn 是一种用于在JVM上执行JavaScript代码的JavaScript引擎,但由于技术过时以及JavaScript生态系统的迅速发展,Oracle决定从JDK中移除它。 若还要使用该方法,获取ScriptEngine对象时null,需要导入nashorn-core依赖

<dependency>
    <groupId>org.openjdk.nashorn</groupId>
    <artifactId>nashorn-core</artifactId>
    <version>15.4</version>
</dependency>