apache commons-jexl 公式中运用(自定义函数、公式之间相互调用)

概要

jexl java表达式语言,本文主要讨论它在公式计算中运用。


自定义函数--》
        比如:判断函数(if/else)、包含函数(in(...))、最大函数(max(...))、最小函数(min(...))等等,根据实际业务需要自行添加,在公式中运用;


公式之间相互调用--》
        实际开发过程中,会遇到公式之间相互调用(公式太长,拆分成几个公式),即其中一个公式需要另一个公式的结果值进行计算。

自定义函数


通过以下代码例子,了解基本步骤:

// 通过test进行测试(添加自定义函数)
public class FormulaTestService {

    @Test
    public void test_formula_1() {
        // 1、创建 JexlBuilder  
        JexlBuilder jexlB = new JexlBuilder().charset(StandardCharsets.UTF_8).cache(1024).strict(true).silent(false);

        // 2、用户自定义函数(调用方式--》函数名:方法名)
        Map<String, Object> funcs = Maps.newLinkedHashMap();
        funcs.put("fn_min", new MinFunction());
        jexlB.namespaces(funcs);

        // 3、创建 JexlEngine
        JexlEngine jexl = jexlB.create();

        // 4、创建表达式对象(函数:方法名)
        String jexlExp = "fn_min:exec(x,y,x)";  // 表达式
        JexlExpression e = jexl.createExpression(jexlExp);

        // 5、创建 context,用于传递参数
        JexlContext jc = new MapContext();
        jc.set("x", 8);
        jc.set("y", 1);
        jc.set("z", 3);

        // 6、执行表达式
        Object o = e.evaluate(jc);
        System.out.println("返回结果:" + o);
    }

// 取最小值函数
    @Data
   public class MinFunction {
        public Object exec(Object... args) {
            return Stream.of(args)
                .mapToDouble(i -> {
                    String iVal = Objects.toString(i, null);
                    return NumberUtils.isCreatable(iVal) ? NumberUtils.toDouble(iVal) : 0;
                }).min()
                .orElse(0);
        }
    }
}

公式之间相互调用

        一个公式通过参数提供给另一个公式调用,我们可以把它称之为子公式,子公式需要先计算出结果值,那如何保证它先计算呢。?
        采用递归计算,检测子公式未出结果,先计算出公式值,然后提供给调用公式赋参数值(公式作为参数变量,提供其它公式调用)。

        下面,我提供在实际开发过程中如何封装公式表达式:

1、创建表达式引擎

package com.ml.module.formula;

import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlEngine;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;

/**
 * 表达式引擎, 使用Apache Jexl实现
 */
@Slf4j
@Component
public class ExpressionEngine implements ApplicationContextAware {
    private static final int CACHE_SIZE = 1024;

    /**
     * 自定义函数的上下文
     */
    private Map<String, Object> functionalContext = Maps.newHashMap();

    private JexlEngine jexlEngine;

    private void buildEngine() {
        JexlBuilder builder = new JexlBuilder()
                .charset(StandardCharsets.UTF_8)
                .cache(CACHE_SIZE)
                .strict(true)
                .silent(false);

        builder.namespaces(functionalContext);
        log.debug("加载表达式引擎命名空间->[{}]", functionalContext.keySet());

        jexlEngine = builder.create();
    }


    /**
     * 根据源码生成公式
     * case1: 求单个项的值, createFormula().evaluate()
     *
     * @param name 便于理解的中文标识
     */
    public Formula createFormula(String source, String name, IFormulaConfig config) {
        if (Objects.isNull(jexlEngine)) buildEngine();

        Formula formula = Formula.of(jexlEngine.createExpression(source), config);
        formula.setName(name);

        return formula;
    }

    /**
     * 根据源码生成公式
     * case1: 求单个项的值, createFormula().evaluate()
     */
    public Formula createFormula(String source, IFormulaConfig config) {
        if (Objects.isNull(jexlEngine)) buildEngine();

        return Formula.of(jexlEngine.createExpression(source), config);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        applicationContext.getBeansOfType(ICustomFunction.class).values()
                .forEach(i -> this.functionalContext.put(i.getFullNamespace(), i));
        log.info("计算引擎初始化完成, 注册自定义函数->{}", functionalContext);
    }

}


// 自定义函数
// 1、接口列
/**
 * 可用于表达式引擎 注册命名空间内函数
 */
public interface ICustomFunction {

    String FUNCTION_PREFIX = "fn_";

    /**
     * 用于注册到表达式引擎中的函数前缀
     */
    String getNamespace();

    /**
     * 该函数的描述信息
     */
    default String getDescription() {
        return StringUtils.EMPTY;
    }

    /**
     * 函数全称
     */
    default String getFullNamespace() {
        return FUNCTION_PREFIX.concat(getNamespace());
    }

}

// 2、自定义函数实现类(目前只提供if函数,其它函数参考添加)
/**
 * if
 */
@Slf4j
@Component
public class IfFunction implements ICustomFunction {
    private static final String FUNCTION_NAME = "if";

    public Object exec(boolean testExpression, Object trueResult, Object falseResult) {
        log.trace("函数[如果]开始执行, test = {}, true = {}, false = {}", testExpression, trueResult, falseResult);

        return testExpression
            ? trueResult
            : falseResult;

    }

    @Override
    public String getNamespace() {
        return FUNCTION_NAME;
    }

    @Override
    public String getDescription() {
        return "如果(条件, 测试成功, 测试失败)";
    }

    @Override
    public String toString() {
        return this.getDescription();
    }

}

 2、创建公式类

package com.ml.module.formula;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.ml.support.core.BusinessException;
import com.ml.support.core.ErrorCode;
import lombok.*;
import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.commons.jexl3.MapContext;
import org.apache.commons.jexl3.internal.Script;
import org.springframework.data.util.Pair;

import java.util.List;
import java.util.Map;
import java.util.Objects;

import static java.util.stream.Collectors.toList;

/**
 * 公式, 公式中的变量必须符合格式 "分类.变量名的两段格式"
 */
@ToString(of = "expression")
@Getter @Setter
@RequiredArgsConstructor(staticName = "of")
public class Formula {

    /**
     * 默认的主公式的取值键
     */
    public static final String KEY_MAIN_FORMULA = "__MAIN_FORMULA";

    public static final String KEY_TPL_GROUP = "G.G";

    /**
     * 公式变量的分隔符
     */
    public static final String ITEM_SPLITTER = ".";

    @NonNull private JexlExpression expression;

    @NonNull private IFormulaConfig config;

    /**
     * 方便调试理解的中文标示
     */
    private String name;

    /**
     * 该条公式内涉及的变量
     */
    private List<String> itemNames = Lists.newArrayList();

    /**
     * 计算公式值
     */
    public Object evaluate(Map<String, Object> varContext) {
        JexlContext context = new MapContext();
        varContext.forEach(context::set);


        return expression.evaluate(context);
    }

    /**
     * 分割公式中涉及的变量项
     */
    public List<String> getItemNames() {
        if (Objects.isNull(expression)) return ImmutableList.of();

        if (itemNames.isEmpty()) {
            ((Script) expression).getVariables().forEach(var -> {
                if (this.config.isStrictVariableName()) {
                    if (var.size() != 2) throw BusinessException.withArgs(ErrorCode.ERR_22201, var);
                    if (!config.getItemPrefix().contains(var.get(0)))
                        throw BusinessException.withArgs(ErrorCode.ERR_22202, var.get(0), config.getItemPrefix());

                    itemNames.add(var.get(0).concat(ITEM_SPLITTER).concat(var.get(1)));
                } else itemNames.add(var.get(0));

            });
        }

        return itemNames;
    }

}

3、表达式计算器

package com.ml.module.formula;

import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import com.ml.support.core.BusinessException;
import com.ml.support.core.ErrorCode;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 表达式计算器
 */
@Slf4j
@RequiredArgsConstructor
public class ExpressionCalculator {

    @NonNull
    private final Map<String, Formula> formulaItems;

    @NonNull
    private final ExpressionEngine expressionEngine;

    /**
     * 计算工资项
     *
     * @param originContext 原始上下文, 包含已经计算出来的值
     */
    public void evaluate(Map<String, Object> originContext) throws FormulaCalcException {
        log.debug("开始使用上下文[{}] 计算公式[{}]...", originContext, formulaItems);
        Stopwatch sw = Stopwatch.createStarted();

        formulaItems.forEach((key, value) -> {
            List<String> itemChain = Lists.newArrayList();
            itemChain.add(key);
            evaluateItem(originContext, itemChain, key, value);
            itemChain.remove(key);
        });

        log.debug("计算结束, 耗时[{}]ms", sw.stop().elapsed(TimeUnit.MICROSECONDS));
        log.debug("<------------------------------------------------------------------------>");
    }

    /**
     * 计算单项
     */
    private void evaluateItem(Map<String, Object> context, List<String> chain, String itemKey, Formula formula) {
        log.debug("[{}/{}]开始计算公式[{}]", formula.getName(), itemKey, formula);
        log.debug("[{}/{}]当前上下文[{}], 调用链[{}]", formula.getName(), itemKey, context, chain);
        if (context.containsKey(itemKey)) return; //已经计算出结果
        if (chain.indexOf(itemKey) != chain.size() - 1) throw BusinessException.withArgs(ErrorCode.ERR_22203, itemKey);

        List<String> itemRequired = formula.getItemNames();

        itemRequired.forEach(subItem -> {
            if (context.containsKey(subItem)) return; //已经结算过或者是普通项

            if (!this.formulaItems.containsKey(subItem)) {
                String message = String.format("在进行调用链%s的表达式%s进行求值时发生异常: 未提供对应的值", chain, subItem);
                throw new FormulaCalcException(message);
            }

            chain.add(subItem);
            evaluateItem(context, chain, subItem, this.formulaItems.get(subItem));
            chain.remove(subItem);
        });

        if (itemRequired.stream().allMatch(context::containsKey)) {
            Object result = formula.evaluate(context);
            log.debug("[{}/{}]求值完成, result = {}", formula.getName(), itemKey, result);
            context.put(itemKey, result);
        }

    }

}

4、测试用例

@Slf4j
@SpringBootTest
@ActiveProfiles("localdev")
@RunWith(SpringRunner.class)
public class ExpressionCalculatorTest {
    @Test
    public void test_formula() {
        String text = " 如果  (  [学生人数]  <  = 0 , 0 ,  如果  (  [学生人数]  <  = 50 , 150 , 150 +  (  [学生人数]  - 50 )  * 2 )  *  [培训班期数]  ) ";
        String sourceCode = "fn_if:exec(C.C1 <= 0, 0, G.G1 * C.C2)";
        String subSourceCode = "fn_if:exec(C.C1 <= 50, 150, 150 + (C.C1 - 50) * 2)";

        IFormulaConfig config = () -> ImmutableList.of("C", "G");
        Map<String, Formula> formulaItems = Maps.newHashMap();
        formulaItems.put("G.G1", expressionEngine.createFormula(subSourceCode, config));
        formulaItems.put("result", expressionEngine.createFormula(sourceCode, config));

        ExpressionCalculator calculator = new ExpressionCalculator(formulaItems, new ExpressionEngine());
         Map<String, Object> context = Maps.newHashMap(ImmutableMap.of(
                "C.C1", 150,
                "C.C2", 20
        ));

        calculator.evaluate(context);
        System.out.println("total->" + stopwatch.stop().elapsed(TimeUnit.MILLISECONDS) + "ms");
        System.out.println(context.get("result"));
    }
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值