如何解析JEXL递归表达式并动态向上下文添加关联变量?
动态为JEXL表达式添加递归变量上下文的实现方案
这个问题我之前做类似业务需求的时候研究过,两种方案都能实现你的需求——用JEXL解析器主动遍历表达式(推荐)和通过捕获异常被动补全上下文,下面分别详细说明并给出示例:
方案一:利用JEXL解析器主动遍历表达式(优雅高效)
JEXL本身提供了AST(抽象语法树)解析能力,我们可以遍历表达式的语法树,主动收集所有用到的变量(包括递归嵌套的VAR1这类变量里的依赖),然后一次性把所有变量的计算结果注入上下文,避免重复执行表达式。
核心思路
- 定义一个递归方法,用于遍历JEXL表达式的AST节点,收集所有未定义的变量名;
- 对每个收集到的变量,解析它对应的子表达式,递归收集子表达式里的依赖变量;
- 按依赖顺序(先底层变量,再上层变量)计算所有变量的值,注入到JEXL上下文;
- 最后执行初始表达式。
Java代码示例
import org.apache.commons.jexl3.*; import org.apache.commons.jexl3.parser.ASTIdentifier; import org.apache.commons.jexl3.parser.JexlNode; import java.util.HashSet; import java.util.Map; import java.util.Set; public class JexlDynamicContext { private static final JexlEngine JEXL_ENGINE = new JexlBuilder().create(); // 递归收集表达式中所有的变量名 private static void collectVariables(JexlNode node, Set<String> variables) { if (node instanceof ASTIdentifier) { ASTIdentifier identifier = (ASTIdentifier) node; variables.add(identifier.getName()); } // 遍历子节点,处理递归嵌套的情况 for (int i = 0; i < node.jjtGetNumChildren(); i++) { collectVariables(node.jjtGetChild(i), variables); } } // 递归解析并注入所有变量到上下文 private static void resolveVariables(Map<String, String> variableExpressions, JexlContext context) { Set<String> unresolvedVars = new HashSet<>(); // 先收集所有当前未解析的变量 for (Map.Entry<String, String> entry : variableExpressions.entrySet()) { if (!context.has(entry.getKey())) { unresolvedVars.add(entry.getKey()); } } if (unresolvedVars.isEmpty()) { return; } // 遍历未解析的变量,解析其表达式并收集依赖 for (String varName : unresolvedVars) { String exprStr = variableExpressions.get(varName); Expression expr = JEXL_ENGINE.createExpression(exprStr); Set<String> subVars = new HashSet<>(); collectVariables(expr.getAST(), subVars); // 先解析子变量的依赖 subVars.removeIf(context::has); // 移除已解析的变量 if (!subVars.isEmpty()) { // 递归处理子变量(假设子变量的表达式也在variableExpressions中) resolveVariables(variableExpressions, context); } // 计算当前变量的值并注入上下文 Object value = expr.evaluate(context); context.set(varName, value); } } public static void main(String[] args) { // 模拟变量表达式映射:VAR1、VAR2本身是其他表达式,VAR3是直接值 Map<String, String> variableExpressions = Map.of( "VAR1", "VAR3 * 2", "VAR2", "VAR1 + 5", "VAR3", "10" ); // 初始表达式 String initialExpr = "VAR2 - 3"; // 创建JEXL上下文 JexlContext context = new MapContext(); // 先注入已知的直接值变量(比如VAR3如果是固定值,可以直接注入) context.set("VAR3", 10); // 递归解析所有变量并注入上下文 resolveVariables(variableExpressions, context); // 执行初始表达式 Expression initialExpression = JEXL_ENGINE.createExpression(initialExpr); Object result = initialExpression.evaluate(context); System.out.println("初始表达式计算结果:" + result); // 输出:22 } }
方案二:通过捕获JEXL异常被动补全上下文
这个方案更简单粗暴:先尝试执行初始表达式,当JEXL抛出VariableUndefinedException时,捕获异常并获取未定义的变量名,然后计算该变量的值注入上下文,再重新执行表达式,直到没有异常抛出。这种方法适合变量层级不深的场景,缺点是可能会多次执行表达式,效率稍低。
Java代码示例
import org.apache.commons.jexl3.*; import java.util.Map; public class JexlExceptionCatch { private static final JexlEngine JEXL_ENGINE = new JexlBuilder().create(); public static void main(String[] args) { Map<String, String> variableExpressions = Map.of( "VAR1", "VAR3 * 2", "VAR2", "VAR1 + 5", "VAR3", "10" ); String initialExpr = "VAR2 - 3"; JexlContext context = new MapContext(); Expression initialExpression = JEXL_ENGINE.createExpression(initialExpr); Object result = null; while (true) { try { result = initialExpression.evaluate(context); break; // 执行成功,退出循环 } catch (JexlException.VariableUndefined e) { String varName = e.getVariable(); String exprStr = variableExpressions.get(varName); if (exprStr == null) { throw new RuntimeException("找不到变量" + varName + "对应的表达式"); } // 计算变量值并注入上下文 Expression varExpr = JEXL_ENGINE.createExpression(exprStr); Object varValue = varExpr.evaluate(context); context.set(varName, varValue); } } System.out.println("初始表达式计算结果:" + result); // 输出:22 } }
注意事项
- 两种方案都需要你维护一个变量名→对应表达式的映射表,这样才能知道每个VAR对应的计算逻辑;
- 如果变量的表达式依赖外部数据源(比如数据库、配置中心),你需要在解析前先获取到对应的表达式内容;
- 解析器方案的效率更高,适合复杂嵌套的变量场景,异常捕获方案适合快速实现简单场景。
内容的提问来源于stack exchange,提问作者Osy
相关产品推荐
相关产品推荐

