Apache Commons JEXL 3.7.0 配置参数详解

逐项讲解 Apache Commons JEXL 3.7.0 的 JexlBuilder 构建参数(permissions、namespaces、strict、silent、lexical、cache 等)与工程侧配置,全部以 Ubuntu 22.04 实测行为为据,附排障实录。

Apache Commons JexlApache Commons Jexl2026/10/14

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/编码/classpathrelease 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 字节码与真实报错实测):

  1. 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)
  1. 无参 createJxltEngine() 反汇编等价于 createJxltEngine(true, 256, 36, 35)——256 即默认缓存容量参数,可用 cache 对应思路理解;无特殊需求不必手动调。
  2. 模板指令前缀默认 "$$"(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 保持官方既定语义;本文给「默认行为实测锚点」+ 官方语义,便于按需调整:

参数签名默认值官方语义默认行为实测锚点(本文实测输出)
strictstrict(boolean)falsefalse 时对未定义变量求值返回 null;true 时抛 JexlException 严格报错System.getProperty('user.dir') 中未定义变量 System 实测返回 null(5.1 第 1 行输出),而非抛异常
silentsilent(boolean)falsefalse 时求值错误抛 JexlException;true 时不抛异常、记录日志并返回 nullRESTRICTED 拦截自定义类方法实测抛 JexlException$Method(5.1 第 4 行输出),证明默认非静默
lexicallexical(boolean)falsetrue 时启用词法作用域,var/const 遵循块级作用域;false 时变量作用域为脚本全局本文脚本示例统一 var 声明,两种模式下行为一致(实测兼容)
cachecache(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 17mvn compile 实测 Compiling 8 source files with javac [debug release 17](第二章)
源码编码pom project.build.sourceEncodingUTF-8含中文源码正常编译(第二章 BUILD SUCCESS)
依赖坐标pom dependenciesorg.apache.commons:commons-jexl3:3.7.0依赖树实测唯一传递依赖 commons-logging:1.4.0
运行 classpathcp.txt + -cp "target/classes:$(cat cp.txt)"双 jarcat cp.txt 实测输出两个 jar 路径(第二章),缺一报 NoClassDefFoundError
运行编码-Dfile.encoding=UTF-8UTF-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)让失败尽早暴露。

八、参考链接

  1. Apache Commons JEXL 官方站点:https://commons.apache.org/proper/commons-jexl/
  2. Apache Commons JEXL 3.7.0 Javadoc(JexlBuilder / JexlPermissions / JxltEngine 完整签名):https://commons.apache.org/proper/commons-jexl/apidocs/
  3. Apache Commons JEXL 用户指南:https://commons.apache.org/proper/commons-jexl/userguide.html
  4. Maven 中央仓库坐标页:https://repo1.maven.org/maven2/org/apache/commons/commons-jexl3/
  5. 官方发行下载目录(含 javadoc 与 sources 包):https://downloads.apache.org/commons/jexl/