【开发需求】
- 数据源配置公式,根据公式计算。
例如:($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>