概要
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);
}
}
}
一个公式通过参数提供给另一个公式调用,我们可以把它称之为子公式,子公式需要先计算出结果值,那如何保证它先计算呢。?
采用递归计算,检测子公式未出结果,先计算出公式值,然后提供给调用公式赋参数值(公式作为参数变量,提供其它公式调用)。
下面,我提供在实际开发过程中如何封装公式表达式:
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();
}
}
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;
}
}
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);
}
}
}
@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"));
}
}