现有情况:
系统中表单字段的值可以是运算获得,例如:总价=单价*数量。
因为表单是动态配置的,为了灵活使用,计算值也不能使用固定方法,所以引入了表达式计算。
因前期只考虑到了简单数学运算(加减乘除次方绝对值等),所以加入的是CalUtil工具类,但随着最大值、平均值、正余弦等高级数学运算,在系统中引用了scijava的jep来计算值;
Jep
jep 将字符串形式的公式,配置对应的参数得到计算结果。支持用户自定义变量、常量和函数。包括许多常用的数学函数和常量。
问题
但要计算日期时,例如任务开始时间2023-03-01,任务时长 16小时,要计算出任务结束时间,jep现有功能无法支持。
解决方案
添加Jep自定义函数
继承PostfixMathCommand,重写run方法
import cn.hutool.core.date.DateUtil;
import org.nfunk.jep.ParseException;
import org.nfunk.jep.function.PostfixMathCommand;
import java.util.Date;
import java.util.Stack;
/**
* 当前日期 默认格式yyyy-MM-dd,可传参指定格式
* @author xxx
*/
public class DateNow extends PostfixMathCommand {
public DateNow() {
super();
// 使用参数的数量
this.numberOfParameters = -1;
}
@Override
public void run(Stack inStack) throws ParseException {
//检查栈
checkStack(inStack);
String result;
System.out.println("curNumberOfParameters : "+curNumberOfParameters);
if (0 == curNumberOfParameters) {
result = DateUtil.today();
} else {
String fmt = inStack.pop().toString();
System.out.println("fmt : "+fmt);
result = DateUtil.format(new Date(), fmt);
}
inStack.push(result);
}
}
import com.kenway.common.enums.ResultCode;
import com.kenway.common.enums.workflow.Formula;
import com.kenway.common.exception.ApiException;
import org.nfunk.jep.FunctionTable;
import org.nfunk.jep.JEP;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 使用Jep脚本引擎运行脚本,得到结果
*
* @author xxx
*/
public class ExpressionCalcUtil {
private static JEP jep;
static {
jep = new JEP();
// 添加常用函数
jep.addStandardFunctions();
// 添加常用常量
jep.addStandardConstants();
FunctionTable functionTable = jep.getFunctionTable();
FunctionTable newFunctionTable=new FunctionTable();
functionTable.forEach((key,value)->{
newFunctionTable.put(((String)key).toUpperCase(),value);
});
functionTable.clear();
functionTable.putAll(newFunctionTable);
// 添加自定义函数
jep.addFunction(Formula.MIN.getCode(), new Min());
jep.addFunction(Formula.MAX.getCode(), new Max());
jep.addFunction(Formula.AVERAGE.getCode(), new Avg());
jep.addFunction(Formula.DATENOW.getCode(), new DateNow());
jep.addFunction(Formula.DATEADD.getCode(), new DateAdd());
}
/**
* 调用计算前需要调用该方法
*/
private static synchronized void init(){
//清空设置的变量
jep.initSymTab();
// 添加常用常量
jep.addStandardConstants();
}
public static Object expressionCalcObject(String expression, Map<String,Object> param){
init();
param.forEach((key,value)->{
jep.addVariable(key, value);
});
try{
jep.parseExpression(expression);
Object result=jep.getValueAsObject();
return result;
}catch (Exception e){
throw new ApiException(ResultCode.VALIDATE_FAILED.getCode(),"表达式计算出错");
}
}
虽然已实现新函数,但在扩展过程中查找api却很难找到,找到了singularsys封装的Jep文档,使用方式相似,但它的jar不在maven托管,更新时间跨度大。市面上还有其它做的不错的引擎,索性换一个有文档,活跃的。
计算/表达式引擎
- Fel :轻量级的高效的表达式计算引擎,语法与Java基本相同,每秒可以执行千万次表达式,扩展和修改Fel都非常简单。
- QLExpress :由阿里的电商业务规则、表达式(布尔组合)、特殊数学公式计算(高精度)、语法分析、脚本二次定制等强需求而设计的一门动态脚本引擎解析工具。
- Aviator:是一个高性能、轻量级的java语言实现的表达式求值引擎;支持大部分运算操作符,支持函数调用和自定义函数,但没有if else、do while等语句。
- Mvel:是一种动态/静态可嵌入的表达式语言,基于Java语法的一种表达式解析器。
- Expression4j、OGNL、SpEL、JEXL、JSEL等其它引擎感兴趣的同学可以自行查看。
最终选择
比对之后,选定了阿里的QLExpress,轻量、功能强大、用法简单易懂。
Github示例
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>QLExpress</artifactId>
<version>3.3.1</version>
</dependency>
ExpressRunner runner = new ExpressRunner();
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
context.put("a", 1);
context.put("b", 2);
context.put("c", 3);
String express = "a + b * c";
Object r = runner.execute(express, context, null, true, false);
System.out.println(r);
很简单吧,再把它封装成工具类,在项目中的应用
import cn.hutool.core.util.StrUtil;
import com.kenway.common.enums.ResultCode;
import com.kenway.common.exception.ApiException;
import com.ql.util.express.DefaultContext;
import com.ql.util.express.ExpressRunner;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* 使用QLExpress脚本引擎运行脚本,得到结果
*
* @author xxx
*/
@Slf4j
public class ExpressionCalcUtil {
private static ExpressRunner runner;
static {
runner = new ExpressRunner();
//以继承Operator类的方式扩展函数
runner.addFunction("datenow", new DateNow());
try {
//以指定类方法的方式扩展函数
runner.addFunctionOfClassMethod("abs", Math.class.getName(), "abs", new String[]{"double"}, null);
runner.addFunctionOfClassMethod("contains", StrUtil.class.getName(), "contains", new Class[]{CharSequence.class, CharSequence.class}, null);
} catch (Exception e) {
log.error("ExpressionCalc添加函数出错!", e);
}
}
public static Object expressionCalc(String expression, Map<String, Object> param) {
DefaultContext<String, Object> context = new DefaultContext<String, Object>();
context.putAll(param);
Object result;
try {
result = runner.execute(expression, context, null, true, false);
} catch (Exception e) {
log.error("expressionCalc execute expression error!", e);
throw new ApiException(ResultCode.VALIDATE_FAILED.getCode(), "表达式计算出错");
}
return result;
}
/**
* 当前日期 默认格式yyyy-MM-dd,可传参指定格式
*
* @author xxx
*/
public class DateNow extends Operator {
@Override
public Object executeInner(Object[] objects) {
String result;
System.out.println("objects : " + objects);
if (Array.getLength(objects) == 0) {
result = DateUtil.today();
} else {
String fmt = objects[0].toString();
System.out.println("fmt : " + fmt);
result = DateUtil.format(new Date(), fmt);
}
return result;
}
}
总结
计算变迁历程
手动解析 -> JEP引擎 -> QLExpress
希望看到文章的同学可以跳过前面的坑,拥抱QLExpress。