表达式运算升级记录

137 阅读4分钟

现有情况:

系统中表单字段的值可以是运算获得,例如:总价=单价*数量。
因为表单是动态配置的,为了灵活使用,计算值也不能使用固定方法,所以引入了表达式计算。 因前期只考虑到了简单数学运算(加减乘除次方绝对值等),所以加入的是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示例

地址: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。