Apache Commons JEXL 3.7.0 配置参数详解
逐项讲解 Apache Commons JEXL 3.7.0 的 JexlBuilder 构建参数(permissions、namespaces、strict、silent、lexical、cache 等)与工程侧配置,全部以 Ubuntu 22.04 实测行为为据,附排障实录。
Apache Commons JEXL 3.7.0 配置参数详解
目标:把 Apache Commons JEXL 3.7.0 的全部关键配置参数讲透——引擎构建参数(
JexlBuilder的permissions、namespaces、strict、silent、lexical、cache等)、脚本行为控制与工程侧构建配置,每一项都给出实测行为与真实报错回显。本文全部命令与回显已在 Ubuntu 22.04(x86_64)实测通过,JEXL 版本为 3.7.0。
一、环境信息
- 操作系统:Ubuntu 22.04(x86_64)
- JDK:OpenJDK 17.0.20.1(
java -version实测) - 构建工具:Apache Maven 3.6.3
- 目标库:Apache Commons JEXL 3.7.0(
org.apache.commons:commons-jexl3:3.7.0) - 传递依赖:commons-logging 1.4.0(有且仅有一个)
- Maven 示例工程目录:
/opt/jexl-demo(含 8 个演示类:BasicEval、ContextVar、PojoNav、CollectionEval、ScriptDemo、TemplateDemo、FunctionNS、PermissionDemo) - API 反查工具:
javap(JDK 自带,用于核实 3.7.0 真实 API)
先确认 JDK 与 Maven 版本基线:
java -version 2>&1
mvn -v | head -3
输出:
openjdk version "17.0.20.1" 2026-08-18
OpenJDK Runtime Environment (build 17.0.20.1+1-1-22.04-Ubuntu)
OpenJDK 64-Bit Server VM (build 17.0.20.1+1-1-22.04-Ubuntu, mixed mode, sharing)
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 17.0.20.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Apache Commons JEXL(Java Expression Language)是 Apache 基金会的 Java 表达式引擎,本文讲解它 3.7.0 版本的参数体系。它的配置分三层,载体与验证手段各不相同:
| 配置层 | 载体 | 典型参数 | 本文对应章节 |
|---|---|---|---|
| 引擎构建参数 | JexlBuilder 链式调用 | permissions、namespaces、strict、silent、lexical、cache | 三至五章 |
| 脚本行为控制 | 解析器内置特性(FeatureController) | 全局变量赋值禁令、var 声明要求 | 5.3 |
| 工程构建参数 | pom.xml、classpath、编码 | release 17、sourceEncoding、依赖坐标 | 5.6 |
💡 技术拓展:JEXL 属于「Java 表达式引擎」赛道(在运行期解析并求值动态表达式字符串的库),同一赛道常见选项对比——
引擎 语法风格 安全模型 性能/形态 适用场景 Apache Commons JEXL 类 Java / JSP EL,支持脚本块与模板 JexlPermissions白名单,3.x 起默认收紧轻量纯库,表达式缓存,依赖极少 规则引擎、动态条件、模板渲染 Spring SpEL 类 Java,可引用 Bean 与方法 StandardEvaluationContext全开 /SimpleEvaluationContext受限与 Spring 生态深度集成 Spring 应用内注解与配置表达式 OGNL 导航链式语法 早期默认宽松,需自行封锁 成熟但年代较早 Struts/WebWork 遗留系统 MVEL 类 Java 脚本 + 模板 默认宽松 以运行时字节码生成著称 Drools 等规则引擎内嵌脚本 Groovy 脚本 完整 JVM 动态语言 SecureASTCustomizer白名单沙箱编译为字节码,能力最强也更重 复杂内嵌脚本、DSL 本文采用 JEXL 3.7.0:Apache 出品、运行期依赖仅 commons-logging 一个、提供「单表达式 → 多语句脚本 → 模板」完整梯度;其
JexlPermissions权限模型(本文 5.1 全量实测)适合把动态求值安全地开放给外部输入。
⚠️ 注意:3.7.0 与 3.x 旧版相比有多处默认行为变化(默认权限收紧、脚本禁全局赋值、模板指令改
$$前缀、setFunctions改名namespaces),本文各参数详解节均给出实测报错,从旧版迁移时可直接对照。
二、前置条件
本文示例共用一个 Maven 工程 /opt/jexl-demo,先按以下步骤就绪(与《Ubuntu 22.04 安装 Apache Commons JEXL 指南》方案 A 一致,可独立执行)。
创建工程目录与 /opt/jexl-demo/pom.xml 全文:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demo</groupId>
<artifactId>jexl-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jexl3</artifactId>
<version>3.7.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
</plugins>
</build>
</project>
逐项说明:sourceEncoding=UTF-8 保证含中文源码正常编译;maven.compiler.release=17 用 JDK 17 语义产出字节码;依赖仅声明 commons-jexl3:3.7.0,commons-logging:1.4.0 由 Maven 传递解析。
编译验证(实测工程含 8 个演示类,本文各节的示例代码即其中成员;读者只放单文件时 Compiling 1 source files 属正常):
cd /opt/jexl-demo && mvn compile
输出节选(实测尾部):
[INFO] --- maven-compiler-plugin:3.11.0:compile (default-compile) @ jexl-demo ---
[INFO] Changes detected - recompiling the module! :source
[INFO] Compiling 8 source files with javac [debug release 17] to target/classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.805 s
[INFO] Finished at: 2026-10-14T10:22:34Z
[INFO] ------------------------------------------------------------------------
生成运行 classpath(后续所有 java -cp 命令依赖它):
cd /opt/jexl-demo && mvn dependency:build-classpath -Dmdep.outputFile=cp.txt -q && cat cp.txt
输出:
/root/.m2/repository/org/apache/commons/commons-jexl3/3.7.0/commons-jexl3-3.7.0.jar:/root/.m2/repository/commons-logging/commons-logging/1.4.0/commons-logging-1.4.0.jar
验证点:classpath 含 JEXL 本体与 commons-logging 两个 jar,与依赖树结论一致;mvn compile 的 BUILD SUCCESS 说明工程就绪,可进入参数详解。
三、参数查看与修改方式
3.1 用 javap 核实参数的真实 API
JexlBuilder 的参数都通过链式方法设置。3.7.0 相比旧版有 API 变动,动手前先用 JDK 自带的 javap 反查真实签名(进入官方二进制包解压目录 /opt/jexl3-dist,获取方式见《Ubuntu 22.04 安装 Apache Commons JEXL 指南》方案 B):
cd /opt/jexl3-dist && javap -cp commons-jexl3-3.7.0.jar org.apache.commons.jexl3.JexlBuilder | grep -Ei "function|namespace|permission"
输出:
public java.util.Map<java.lang.String, java.lang.Object> namespaces();
public org.apache.commons.jexl3.JexlBuilder namespaces(java.util.Map<java.lang.String, java.lang.Object>);
public org.apache.commons.jexl3.introspection.JexlPermissions permissions();
public org.apache.commons.jexl3.JexlBuilder permissions(org.apache.commons.jexl3.introspection.JexlPermissions);
再核实模板接口的方法签名:
cd /opt/jexl3-dist && javap -cp commons-jexl3-3.7.0.jar 'org.apache.commons.jexl3.JxltEngine$Template'
输出节选(实测):
public interface org.apache.commons.jexl3.JxltEngine$Template {
public abstract java.lang.String asString();
public abstract void evaluate(org.apache.commons.jexl3.JexlContext, java.io.Writer);
public abstract void evaluate(org.apache.commons.jexl3.JexlContext, java.io.Writer, java.lang.Object...);
...
解读:JexlBuilder 只有 namespaces 与 permissions 两个函数/权限相关方法——没有旧版的 setFunctions;Template 接口的 evaluate 必须传 java.io.Writer,不存在无参/仅传 Context 的重载。这两点正是从旧教程迁移时的两大高频坑(见 FAQ Q2、Q4)。
💡 说明:
javap是参数配置的「权威清单」——文档可能滞后,但字节码签名不会说谎。遇到 API 不确定时优先用它核实。
3.2 参数的修改方式:Builder 链式调用
所有引擎级参数都在创建引擎时通过链式调用一次性指定,创建后不可再改;如需不同配置,创建多个引擎实例即可(本文 5.1 的 PermissionDemo 就同时创建了 UNRESTRICTED 与 RESTRICTED 两个引擎作对照)。典型形态:
JexlEngine engine = new JexlBuilder()
.permissions(JexlPermissions.RESTRICTED) // 反射权限白名单,见 5.1
.namespaces(funcs) // 自定义函数命名空间,见 5.2
.create(); // 按 Builder 配置创建引擎
⚠️ 注意:
new JexlBuilder().create()与new JexlBuilder().permissions(某值).create()产出的引擎默认行为可能截然不同——3.7.0 的默认权限是收紧的,5.1 有逐行实测对比。
四、参数总览
3.7.0 关键参数按「实测深度」分类汇总如下,后文逐节展开:
| 参数 / 特性 | 类型 | 3.7.0 默认行为 | 实测深度 | 详解 |
|---|---|---|---|---|
| permissions(JexlPermissions) | 反射权限白名单 | 收紧(等价 RESTRICTED),拦截白名单外 POJO/方法 | 全量实测(含 4 种权限实例与自定义白名单) | 5.1 |
| namespaces(Map) | 函数命名空间注册 | 未注册则 ns: 方法不可用;取代旧版 setFunctions | 全量实测 | 5.2 |
| 全局变量赋值禁令 | 解析器特性(FeatureController) | 禁止脚本对未声明变量赋值,须 var 声明 | 全量实测(含两种报错场景) | 5.3 |
| 模板引擎(JxltEngine) | 模板子引擎 | 默认创建;指令 $$ 行前缀;缓存参数实测默认含 256 | 全量实测(含新旧语法对比) | 5.4 |
| strict(boolean) | 求值严格性 | false:未定义变量求值返回 null 而非报错 | 默认行为实测锚点 + 官方语义 | 5.5 |
| silent(boolean) | 错误静默 | false:权限拒绝等错误抛 JexlException | 默认行为实测锚点 + 官方语义 | 5.5 |
| lexical(boolean) | 词法作用域 | false:变量作用域为脚本全局 | 官方语义 | 5.5 |
| cache(int) | 表达式解析缓存 | 默认容量(模板引擎默认参数实测含 256) | 默认参数实测锚点 | 5.5 |
| 工程构建参数 | pom/编码/classpath | release 17、UTF-8、双 jar classpath | 全量实测 | 5.6 |
解读:表中「全量实测」指该参数在本文有完整的构造代码、运行输出与报错回显;「实测锚点 + 官方语义」指参数语义以官方定义为准,但其默认值行为在实测输出中有直接证据;lexical 的默认值行为在本文示例中不可区分(示例均兼容两种模式),故仅给官方语义。
五、核心参数分类详解
5.1 permissions:反射权限白名单(默认收紧)
permissions(JexlPermissions) 控制引擎级反射的放行范围——POJO 属性导航(person.name)、方法调用('abc'.length())、命名空间函数(util:reverse('abc'))都受它约束。3.7.0 把默认值收紧了:不设置时行为等价 RESTRICTED。
JexlPermissions 可用实例与工厂方法(org.apache.commons.jexl3.introspection 包):
| 实例 / 工厂 | 语义 |
|---|---|
JexlPermissions.UNRESTRICTED | 全部放行,任意类属性/方法可访问 |
JexlPermissions.NONE | 全部拒绝 |
JexlPermissions.RESTRICTED | 内置白名单(String、Math 等基础类),自定义类被拒 |
JexlPermissions.SECURE | 比 RESTRICTED 更严格的白名单 |
JexlPermissions.parse("java.util.*") | 按类名模式自定义白名单(实测形态) |
实测结论(原文要点):
- 3.7.0 默认引擎(不设 permissions)行为等价 RESTRICTED:白名单外类的属性/方法被拦截;
- 被拦截时的三种表现:命名空间方法调用抛
JexlException$Method: unsolvable function/method;POJO 属性导航抛JexlException$Property: undefined property;普通方法/属性在表达式上下文中可能静默返回 null。
一个覆盖全部权限场景的实测程序 demo/PermissionDemo.java 全文:
package demo;
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlException;
import org.apache.commons.jexl3.MapContext;
import org.apache.commons.jexl3.introspection.JexlPermissions;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** JexlPermissions 安全权限:RESTRICTED 与自定义白名单阻断危险类调用 */
public class PermissionDemo {
public static void main(String[] args) {
String sysExpr = "System.getProperty('user.dir')";
// 1. 对照:表达式里直接写类名 System.getProperty(...) 时,System 被当作未定义变量 -> null
// (JEXL 访问静态方法须用命名空间 ns:method 形式,不能裸写类名导航)
JexlEngine open = new JexlBuilder()
.permissions(JexlPermissions.UNRESTRICTED)
.create();
Object r1 = open.createExpression(sysExpr).evaluate(new MapContext());
System.out.println("UNRESTRICTED 引擎 System.getProperty('user.dir') -> " + r1 + " (System 是未定义变量)");
// 2. RESTRICTED 引擎同表达式同样返回 null
JexlEngine restricted = new JexlBuilder()
.permissions(JexlPermissions.RESTRICTED)
.create();
Object r2 = restricted.createExpression(sysExpr).evaluate(new MapContext());
System.out.println("RESTRICTED 引擎同表达式 -> " + r2);
// 3. RESTRICTED 白名单内的类(String/Math 等)正常工作
Object r3 = restricted.createExpression("'abc'.length()").evaluate(new MapContext());
System.out.println("RESTRICTED 引擎 'abc'.length() -> " + r3);
// 4. RESTRICTED 下命名空间调用自定义类方法 -> 被拒抛 JexlException$Method
Map<String, Object> fns = new HashMap<>();
fns.put("util", new FunctionNS.Strings());
JexlEngine restrictedNs = new JexlBuilder()
.permissions(JexlPermissions.RESTRICTED)
.namespaces(fns)
.create();
try {
Object r4 = restrictedNs.createExpression("util:reverse('abc')").evaluate(new MapContext());
System.out.println("RESTRICTED 命名空间调用竟然成功: " + r4);
} catch (JexlException e) {
System.out.println("RESTRICTED 拒绝自定义类方法调用 -> " + e.getClass().getName());
System.out.println("异常消息: " + e.getMessage());
}
// 5. 默认引擎(不设 permissions)行为等价 RESTRICTED:自定义 POJO 属性导航报 undefined property
JexlEngine defaultEngine = new JexlBuilder().create();
MapContext pctx = new MapContext();
pctx.set("person", new PojoNav.Person("王五", 30));
try {
Object r5 = defaultEngine.createExpression("person.name").evaluate(pctx);
System.out.println("默认引擎 POJO 竟然成功: " + r5);
} catch (JexlException e) {
System.out.println("默认引擎拒绝 POJO 属性导航 -> " + e.getClass().getName());
System.out.println("异常消息: " + e.getMessage());
}
// 6. 自定义白名单 parse("java.util.*"):ArrayList 可用,String 被拒
JexlEngine custom = new JexlBuilder()
.permissions(JexlPermissions.parse("java.util.*"))
.create();
MapContext ctx = new MapContext();
List<String> list = new ArrayList<>();
ctx.set("list", list);
Object r6 = custom.createExpression("list.add('x') && list.size() == 1").evaluate(ctx);
System.out.println("自定义白名单(java.util.*) ArrayList -> " + r6);
Object r7 = custom.createExpression("'abc'.length()").evaluate(ctx);
System.out.println("自定义白名单 String.length() -> " + r7 + " (String 不在 java.util.*, 被拒返回 null)");
}
}
运行:
cd /opt/jexl-demo
CP="target/classes:$(cat cp.txt)" && java -Dfile.encoding=UTF-8 -cp "$CP" demo.PermissionDemo
输出:
UNRESTRICTED 引擎 System.getProperty('user.dir') -> null (System 是未定义变量)
RESTRICTED 引擎同表达式 -> null
RESTRICTED 引擎 'abc'.length() -> 3
RESTRICTED 拒绝自定义类方法调用 -> org.apache.commons.jexl3.JexlException$Method
异常消息: demo.PermissionDemo.main:46@1:13 unsolvable function/method 'reverse(String)'
默认引擎拒绝 POJO 属性导航 -> org.apache.commons.jexl3.JexlException$Property
异常消息: demo.PermissionDemo.main:58@1:11 undefined property 'name'
自定义白名单(java.util.*) ArrayList -> true
自定义白名单 String.length() -> null (String 不在 java.util.*, 被拒返回 null)
逐行解读:
- 第 1、2 行:UNRESTRICTED 与 RESTRICTED 引擎下
System.getProperty('user.dir')都返回 null——表达式里裸写类名System被当作未定义变量;JEXL 访问静态方法须用命名空间ns:method形式(见 5.2),这与权限无关。 - 第 3 行:RESTRICTED 白名单内的
String方法'abc'.length()正常返回 3。 - 第 4、5 行:RESTRICTED 拦截自定义类
demo.FunctionNS$Strings的静态方法调用,抛JexlException$Method: unsolvable function/method 'reverse(String)'(异常在代码中被 try-catch 打印,未中断程序)。 - 第 6、7 行:默认引擎(不设 permissions)同样拒绝自定义 POJO
demo.PojoNav$Person的 getter——这就是「3.7.0 默认收紧」的直接证据。 - 第 8、9 行:
parse("java.util.*")自定义白名单下ArrayList的add/size可用,String不在白名单被拒返回 null。
自定义 POJO 被默认引擎拦截的独立复现(demo/PojoNav.java 默认引擎版报错,原文照抄):
Exception in thread "main" org.apache.commons.jexl3.JexlException$Property: demo.PojoNav.main:30@1:11 undefined property 'name'
at demo.PojoNav.main(PojoNav.java:30)
💡 说明:
undefined property 'name'并不是真的没有该属性(Person有getName()),而是权限层把属性发现(introspection)拦掉了——报错语义有迷惑性,看到自定义类报undefined property应先怀疑 permissions 而非数据。显式放开后person.name实测返回张三、person.greet()返回完整中文问候(见 5.1 末尾的 PojoNav 完整版输出,或《常用 API 与表达式语法详解》属性导航一节)。
⚠️ 注意:面向外部输入的求值服务不要随手
UNRESTRICTED——先评估表达式来源,能用parse("java.util.*")这类白名单就用白名单;SECURE适合只允许基础运算的极端保守场景。
5.2 namespaces:函数命名空间注册(取代 setFunctions)
namespaces(Map<String,Object>) 把自定义工具类注册进引擎,表达式里用 命名空间:方法 形式调用(如 str:reverse(word))。3.7.0 移除了旧版 setFunctions,沿用旧 API 编译直接报错:
[ERROR] /opt/jexl-demo/src/main/java/demo/FunctionNS.java:[34,17] cannot find symbol
[ERROR] symbol: method setFunctions(java.util.Map<java.lang.String,java.lang.Object>)
[ERROR] location: class org.apache.commons.jexl3.JexlBuilder
修复即改名:.setFunctions(funcs) 改为 .namespaces(funcs)(真实签名见 3.1 javap 输出)。
实测程序 demo/FunctionNS.java 全文(注册静态工具类的实例,命名空间 str、m):
package demo;
import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.MapContext;
import java.util.HashMap;
import java.util.Map;
/** 自定义函数命名空间:JexlBuilder.namespaces,表达式里用 ns:method(...) 调用 */
public class FunctionNS {
/** 静态工具类,注册为命名空间 */
public static class Strings {
public static String reverse(String s) {
return new StringBuilder(s).reverse().toString();
}
public static int length(String s) {
return s.length();
}
}
public static class Maths {
public static int square(int x) {
return x * x;
}
}
public static void main(String[] args) {
Map<String, Object> funcs = new HashMap<>();
funcs.put("str", new FunctionNS.Strings());
funcs.put("m", new FunctionNS.Maths());
JexlEngine jexl = new org.apache.commons.jexl3.JexlBuilder()
.permissions(org.apache.commons.jexl3.introspection.JexlPermissions.UNRESTRICTED)
.namespaces(funcs)
.create();
JexlContext ctx = new MapContext();
ctx.set("word", "jexl");
System.out.println("str:reverse(word) = " + jexl.createExpression("str:reverse(word)").evaluate(ctx));
System.out.println("str:length('apache') = " + jexl.createExpression("str:length('apache')").evaluate(ctx));
System.out.println("m:square(9) = " + jexl.createExpression("m:square(9)").evaluate(ctx));
// 组合:命名空间函数 + 原生运算
System.out.println("组合 = " + jexl.createExpression("m:square(3) + str:length(word)").evaluate(ctx));
}
}
若只注册命名空间而不放开权限(默认引擎),自定义类方法被拦截(原文照抄):
Exception in thread "main" org.apache.commons.jexl3.JexlException$Method: demo.FunctionNS.main:40@1:12 unsolvable function/method 'reverse(String)'
at demo.FunctionNS.main(FunctionNS.java:40)
显式 .permissions(JexlPermissions.UNRESTRICTED) 后运行:
cd /opt/jexl-demo
CP="target/classes:$(cat cp.txt)" && java -Dfile.encoding=UTF-8 -cp "$CP" demo.FunctionNS
输出:
str:reverse(word) = lxej
str:length('apache') = 6
m:square(9) = 81
组合 = 13
逐行验证:reverse("jexl") 反转得 lxej;length("apache") 得 6;square(9) 得 81;square(3) + length("jexl") 得 9 + 4 = 13,命名空间函数与原生运算可自由组合。
配置要点:
- 注册对象为静态工具类的实例(
new FunctionNS.Strings()),方法可为 static;命名空间名即 Map 的 key,表达式里写成key:method(args)。 - 3.7.0 默认权限下自定义类方法会被 RESTRICTED 拦截(5.1),所以命名空间与 permissions 往往要一起配置。
- RESTRICTED 引擎即使注册了命名空间,白名单外类的方法调用同样被拒(5.1 PermissionDemo 第 4 组实测)。
5.3 脚本赋值控制:全局变量禁改与 var 声明
3.7.0 的解析器默认禁止脚本对未声明的全局变量赋值(z = x * y 这类写法直接被拒),必须用 var 声明。这不是独立参数,而是解析器内置特性(由解析器内部的 FeatureController 控制)。
实测程序 demo/ContextVar.java 最初版本在脚本中写全局赋值 z = x * y; z * 2 的真实报错(原文照抄):
Exception in thread "main" org.apache.commons.jexl3.JexlException$Feature: @1:1 global assign/modify error in 'z'
at org.apache.commons.jexl3.parser.FeatureController.throwFeatureException(FeatureController.java:107)
at org.apache.commons.jexl3.parser.FeatureController.controlSideEffect(FeatureController.java:54)
...
at org.apache.commons.jexl3.JexlEngine.createScript(JexlEngine.java:423)
at demo.ContextVar.main(ContextVar.java:33)
解读三个细节:报错类型是 JexlException$Feature(特性类异常,不是语法解析错误);@1:1 定位到第一个字符——赋值目标是 z;堆栈从 createScript 抛出,说明该控制在脚本创建(解析)阶段即生效,根本不会等到求值。
改用 var z = x * y; return z * 2; 后,demo/ContextVar.java(全文)运行通过:
package demo;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.commons.jexl3.MapContext;
import java.util.HashMap;
import java.util.Map;
/** JexlContext 变量注入与读取(MapContext) */
public class ContextVar {
public static void main(String[] args) {
JexlEngine jexl = new org.apache.commons.jexl3.JexlBuilder().create();
Map<String, Object> vars = new HashMap<>();
vars.put("x", 7);
vars.put("y", 6);
MapContext ctx = new MapContext(vars);
ctx.set("greeting", "你好");
Object r1 = jexl.createExpression("x * y").evaluate(ctx);
System.out.println("x * y = " + r1);
Object r2 = jexl.createExpression("greeting + ', JEXL'").evaluate(ctx);
System.out.println("greeting = " + r2);
// 变量读写
ctx.set("x", 100);
Object r3 = jexl.createExpression("x + y").evaluate(ctx);
System.out.println("after set x=100, x + y = " + r3);
// 注:3.7.0 默认禁止脚本对未声明全局变量赋值(z = x*y 会抛 JexlException$Feature),
// 使用 var 声明局部变量
Object r4 = jexl.createScript("var z = x * y; return z * 2;").execute(ctx);
System.out.println("script var z = x*y, return z*2 = " + r4);
// Map 变量的成员访问
Map<String, Object> user = new HashMap<>();
user.put("name", "Alice");
user.put("score", 92);
ctx.set("user", user);
Object r5 = jexl.createExpression("user.name + ' score=' + user.score").evaluate(ctx);
System.out.println("map access: " + r5);
}
}
运行:
cd /opt/jexl-demo
CP="target/classes:$(cat cp.txt)" && java -Dfile.encoding=UTF-8 -cp "$CP" demo.ContextVar
输出:
x * y = 42
greeting = 你好, JEXL
after set x=100, x + y = 106
script var z = x*y, return z*2 = 1200
map access: Alice score=92
逐行验证:7 * 6 = 42;你好 + ', JEXL' 中文正常;ctx.set("x", 100) 后 100 + 6 = 106(上下文写入对后续表达式生效);脚本一行输出 1200 的由来是——var z = x * y 执行时 x 已在上方被改为 100,故 z = 100 * 6 = 600,z * 2 = 1200,与上一行 after set x=100 的时序一致;Map 成员 user.name/user.score 点式访问正常。
for 循环脚本中同理——demo/CollectionEval.java 最初版本对循环变量 total 直接赋值的报错(原文照抄首行与尾部关键帧):
Exception in thread "main" org.apache.commons.jexl3.JexlException$Feature: @1:1 global assign/modify error in 'total'
at org.apache.commons.jexl3.parser.FeatureController.throwFeatureException(FeatureController.java:107)
...
at demo.CollectionEval.main(CollectionEval.java:44)
改为 var total = 0; for(var item : arr) { total = total + item; } return total; 后运行成功(完整集合能力实测见《常用 API 与表达式语法详解》):
for-sum(arr) = 60
for-join(list) = apple,banana,cherry,
💡 说明:从旧版迁移的脚本出现
JexlException$Feature: global assign/modify error时,逐个给赋值目标补var声明即可;这一收紧杜绝了脚本意外覆盖上下文变量的风险,属于安全增强。
5.4 模板引擎参数:createJxltEngine 与 $$ 指令前缀
模板子引擎 JxltEngine 由主引擎的 createJxltEngine() 创建(不存在 createJexlTemplate 这类方法),其配置要点有三(均为 javap 字节码与真实报错实测):
Template.evaluate(ctx)无此重载,必须evaluate(ctx, Writer)或asString()。旧写法编译报错(原文照抄):
[ERROR] /opt/jexl-demo/src/main/java/demo/TemplateDemo.java:[30,46] no suitable method found for evaluate(org.apache.commons.jexl3.JexlContext)
[ERROR] method org.apache.commons.jexl3.JxltEngine.Template.evaluate(org.apache.commons.jexl3.JexlContext,java.io.Writer) is not applicable
[ERROR] (actual and formal argument lists differ in length)
- 无参
createJxltEngine()反汇编等价于createJxltEngine(true, 256, 36, 35)——256 即默认缓存容量参数,可用cache对应思路理解;无特殊需求不必手动调。 - 模板指令前缀默认
"$$"(createTemplate源码确认):3.7.0 模板指令为$$行前缀,旧式%{if}/#{if}全部失效,两种失败形态实测如下。
旧式 %{if(...)}...%{end}——指令未被执行、原样输出,for 指令更是直接报变量未定义:
template3: %{if(user.age >= 18)}成年人%{else}未成年人%{end}:李四
Exception in thread "main" org.apache.commons.jexl3.JexlException$Variable: demo.TemplateDemo.main:44@1:1 variable 'it' is undefined
at demo.TemplateDemo.main(TemplateDemo.java:46)
旧式 #{if}——创建模板时即报解析错误(原文照抄):
Exception in thread "main" org.apache.commons.jexl3.JexlException$Parsing: demo.TemplateDemo.main:35@1:1 parsing error in 'if'
at org.apache.commons.jexl3.JxltEngine.createTemplate(JxltEngine.java:371)
at org.apache.commons.jexl3.JxltEngine.createTemplate(JxltEngine.java:404)
at demo.TemplateDemo.main(TemplateDemo.java:35)
$$ 行前缀的正确写法与实测输出(模板插值 ${name} 等语法保持不变,仅指令行加 $$ 前缀):
template1: Hello JEXL 3.7.0!
template2: 李四 的年龄是 25,已成年
template3: 成年人\n:李四
template4: [a]\n[b]\n[c]\n
完整模板程序 demo/TemplateDemo.java 全文:
package demo;
import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JxltEngine;
import org.apache.commons.jexl3.MapContext;
import java.io.StringWriter;
/** JxltEngine 模板功能:"Hello ${name}" */
public class TemplateDemo {
public static void main(String[] args) {
JexlEngine jexl = new org.apache.commons.jexl3.JexlBuilder()
.permissions(org.apache.commons.jexl3.introspection.JexlPermissions.UNRESTRICTED)
.create();
JxltEngine jxlt = jexl.createJxltEngine();
JexlContext ctx = new MapContext();
ctx.set("name", "JEXL 3.7.0");
ctx.set("user", new PojoNav.Person("李四", 25));
// 简单插值
JxltEngine.Template t1 = jxlt.createTemplate("Hello ${name}!");
StringWriter w1 = new StringWriter();
t1.evaluate(ctx, w1);
System.out.println("template1: " + w1);
// 带表达式的插值
JxltEngine.Template t2 = jxlt.createTemplate("${user.name} 的年龄是 ${user.age},${user.age >= 18 ? '已成年' : '未成年'}");
StringWriter w2 = new StringWriter();
t2.evaluate(ctx, w2);
System.out.println("template2: " + w2);
// if 指令模板:3.7.0 模板指令为 $$ 行前缀(createTemplate 默认 prefix "$$"),不再支持旧式 %{...}
JxltEngine.Template t3 = jxlt.createTemplate(
"$$ if (user.age >= 18) {\n" +
"成年人\n" +
"$$ } else {\n" +
"未成年人\n" +
"$$ }\n" +
":${user.name}"
);
StringWriter w3 = new StringWriter();
t3.evaluate(ctx, w3);
System.out.println("template3: " + w3.toString().replace("\n", "\\n"));
// for 指令模板
ctx.set("items", new String[]{"a", "b", "c"});
JxltEngine.Template t4 = jxlt.createTemplate(
"$$ for (var it : items) {\n" +
"[${it}]\n" +
"$$ }\n"
);
StringWriter w4 = new StringWriter();
t4.evaluate(ctx, w4);
System.out.println("template4: " + w4.toString().replace("\n", "\\n"));
}
}
运行:
cd /opt/jexl-demo
CP="target/classes:$(cat cp.txt)" && java -Dfile.encoding=UTF-8 -cp "$CP" demo.TemplateDemo
输出:
template1: Hello JEXL 3.7.0!
template2: 李四 的年龄是 25,已成年
template3: 成年人\n:李四
template4: [a]\n[b]\n[c]\n
逐行验证:template1 纯插值;template2 同时插值 POJO 属性与三元表达式(25 >= 18 取 已成年);template3 $$ if 指令命中 成年人 分支且 :李四 紧随其后(\\n 为输出时把换行转义显示);template4 $$ for 遍历数组逐项渲染 [a][b][c]。注意脚本模板中循环变量 it 同样遵守 var 声明要求(5.3)。
⚠️ 注意:
evaluate(ctx, Writer)是模板求值的唯一通道(或用asString()),模板示例中的${name}属 JEXL 表达式语法本身,不是 shell 变量——不要试图在 shell 层替换它。
5.5 strict / silent / lexical / cache:官方语义速览
以下四个参数为引擎级布尔/数值开关,3.7.0 保持官方既定语义;本文给「默认行为实测锚点」+ 官方语义,便于按需调整:
| 参数 | 签名 | 默认值 | 官方语义 | 默认行为实测锚点(本文实测输出) |
|---|---|---|---|---|
| strict | strict(boolean) | false | false 时对未定义变量求值返回 null;true 时抛 JexlException 严格报错 | System.getProperty('user.dir') 中未定义变量 System 实测返回 null(5.1 第 1 行输出),而非抛异常 |
| silent | silent(boolean) | false | false 时求值错误抛 JexlException;true 时不抛异常、记录日志并返回 null | RESTRICTED 拦截自定义类方法实测抛 JexlException$Method(5.1 第 4 行输出),证明默认非静默 |
| lexical | lexical(boolean) | false | true 时启用词法作用域,var/const 遵循块级作用域;false 时变量作用域为脚本全局 | 本文脚本示例统一 var 声明,两种模式下行为一致(实测兼容) |
| cache | cache(int) | 默认容量 256(模板引擎实测) | 控制表达式解析结果缓存的容量,高频动态表达式场景可调大 | 无参 createJxltEngine() 反汇编默认参数含 256(5.4) |
解读:
- strict 与 silent 组合决定「宽松求值」与「快速失败」的风格:默认
strict=false + silent=false表示「未定义变量容忍(null),权限拒绝报错」——5.1 的 9 行输出正是这套默认组合的完整画像。 - lexical 影响
var的可见范围;3.7.0 的「全局赋值禁令」(5.3)与 lexical 开关无关,属默认开启的解析器特性。 - cache 只影响性能不影响语义;规则引擎这类高频解析场景可结合表达式基数调大。
💡 说明:以上默认值锚点均来自本文实测输出;如需 true 值行为(如 strict=true 的具体异常形态),建议按 3.2 的 Builder 写法自行加一行实测确认后上生产。
5.6 工程侧参数:编译目标、编码与 classpath
引擎之外的「配置」发生在 Maven/编译/运行层,逐项说明(全部实测):
| 参数 | 配置位置 | 实测值 | 作用与依据 |
|---|---|---|---|
| 编译目标 | pom <release>17</release> | release 17 | mvn compile 实测 Compiling 8 source files with javac [debug release 17](第二章) |
| 源码编码 | pom project.build.sourceEncoding | UTF-8 | 含中文源码正常编译(第二章 BUILD SUCCESS) |
| 依赖坐标 | pom dependencies | org.apache.commons:commons-jexl3:3.7.0 | 依赖树实测唯一传递依赖 commons-logging:1.4.0 |
| 运行 classpath | cp.txt + -cp "target/classes:$(cat cp.txt)" | 双 jar | cat cp.txt 实测输出两个 jar 路径(第二章),缺一报 NoClassDefFoundError |
| 运行编码 | -Dfile.encoding=UTF-8 | UTF-8 | 中文输出(你好, JEXL、成年人)实测无乱码 |
⚠️ 注意:
commons-logging是 JEXL 3.7.0 的强制运行时依赖——不仅编译要解析它,运行 classpath 也必须包含,这一点在《Ubuntu 22.04 安装 Apache Commons JEXL 指南》4.7 有完整报错实录。
六、生产配置模板
按两类典型场景给出推荐构造,两种组合的全部调用点均有本文实测依据(UNRESTRICTED 见 5.2,parse("java.util.*") 见 5.1 第 8/9 行输出),按需取用:
import org.apache.commons.jexl3.JexlBuilder;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.introspection.JexlPermissions;
import java.util.HashMap;
import java.util.Map;
public class JexlConfig {
/** 组合一:内部场景——可信代码求值自有 POJO,显式放开反射并注册函数命名空间 */
public static JexlEngine createInternalEngine() {
Map<String, Object> funcs = new HashMap<>();
funcs.put("str", new FunctionNS.Strings());
funcs.put("m", new FunctionNS.Maths());
return new JexlBuilder()
.permissions(JexlPermissions.UNRESTRICTED) // 默认收紧会拦截自有 POJO,见 5.1 实测
.namespaces(funcs) // 函数命名空间,见 5.2 实测
.create();
}
/** 组合二:外部输入场景——最小白名单,仅放行 java.util 集合操作 */
public static JexlEngine createSafeEngine() {
return new JexlBuilder()
.permissions(JexlPermissions.parse("java.util.*")) // 白名单外被拒,见 5.1 实测
.create();
}
}
配置解读:
- 组合一面向规则引擎后端这类「表达式由内部维护、数据对象是自有 POJO」的场景:UNRESTRICTED 是 3.7.0 下访问自有 POJO 的最低门槛(5.1 默认引擎实测被拒),函数命名空间承载自定义逻辑(5.2 实测四种调用形态)。
- 组合二面向「表达式来自外部输入」的场景:只放行
java.util.*,集合可用而String等其他类的方法被拒返回 null(5.1 实测),配合 5.3 的全局赋值禁令,外部表达式既读不到任意类也改不了上下文。 - 两个组合均未显式设置 strict/silent/lexical/cache——保持默认值(5.5 的实测画像:未定义变量返 null、权限拒绝抛异常);确有需要时在 Builder 上追加对应方法即可。
七、常见问题(FAQ)
Q1:默认引擎访问自有 POJO 报 undefined property,属性明明存在?
3.7.0 默认权限收紧,POJO 属性导航被 JexlPermissions 拦截,报错语义有迷惑性(实测原文见 5.1)。解决:内部场景显式 .permissions(JexlPermissions.UNRESTRICTED),外部输入场景用 .permissions(JexlPermissions.parse("模式")) 白名单。相关报错:JexlException$Property: undefined property 'name'。
Q2:编译报 cannot find symbol: method setFunctions?
3.7.0 已移除 setFunctions,改为 namespaces(Map)(报错原文与 javap 权威签名见 5.2)。同族的另一处 API 变化:Template.evaluate(ctx) 无此重载,必须 evaluate(ctx, Writer) 或 asString()(编译报错原文见 5.4)。
Q3:脚本报 JexlException$Feature: global assign/modify error in 'z'?
3.7.0 默认禁止脚本对未声明全局变量赋值,且在 createScript 解析阶段即拦截(5.3 实测堆栈)。解决:给赋值目标加 var 声明,如 var z = x * y;。
Q4:模板指令 %{if} / #{if} 输出原样或报 parsing error in 'if'?
3.7.0 模板指令改为 $$ 行前缀,旧式两种写法均失败(两种失败形态的报错原文见 5.4)。解决:指令行加 $$ 前缀(如 $$ if (user.age >= 18) {),插值 ${name} 等语法不变。
Q5:同一表达式在 RESTRICTED 下有时抛异常、有时返回 null?
这正是 JexlPermissions 拦截的三种表现并存(5.1 实测结论):命名空间方法调用抛 JexlException$Method、POJO 属性导航抛 JexlException$Property、部分普通方法/属性静默返回 null。排查时先确认类是否在白名单内,再结合 5.5 的 strict/silent 语义理解;对关键求值建议显式设置严格组合(strict=true)让失败尽早暴露。
八、参考链接
- Apache Commons JEXL 官方站点:https://commons.apache.org/proper/commons-jexl/
- Apache Commons JEXL 3.7.0 Javadoc(JexlBuilder / JexlPermissions / JxltEngine 完整签名):https://commons.apache.org/proper/commons-jexl/apidocs/
- Apache Commons JEXL 用户指南:https://commons.apache.org/proper/commons-jexl/userguide.html
- Maven 中央仓库坐标页:https://repo1.maven.org/maven2/org/apache/commons/commons-jexl3/
- 官方发行下载目录(含 javadoc 与 sources 包):https://downloads.apache.org/commons/jexl/