Apache Commons JEXL 3.7.0 常用 API 与表达式语法详解

系统讲解 Apache Commons JEXL 3.7.0 的核心 API(JexlEngine、JexlExpression、JexlScript、JxltEngine、JexlPermissions)与表达式语法逐类用法,全部示例已在 Ubuntu 22.04 实测并附真实回显。

Apache Commons JexlApache Commons Jexl2026/10/14

Apache Commons JEXL 3.7.0 常用 API 与表达式语法详解

目标:系统掌握 Apache Commons JEXL 3.7.0 的五个核心 API(JexlEngine、JexlExpression、JexlScript、JxltEngine、JexlPermissions)与表达式语法的全部常用形态(算术、字符串、三元、变量导航、集合下标、控制流、lambda 与函数、命名空间函数、模板插值与指令),每个用法都配完整示例代码与实测输出。以下全部命令与回显均已在 Ubuntu 22.04(x86_64)实测通过。

一、环境信息

  • 操作系统: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)

确认 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

💡 说明:本文所有运行命令统一为 CP="target/classes:$(cat cp.txt)" && java -Dfile.encoding=UTF-8 -cp "$CP" demo.类名 形式,其中 cp.txt 的生成见第三章;-Dfile.encoding=UTF-8 保证中文输出(如 你好, JEXL、成年人)不乱码。

二、API 与语法概览

Apache Commons JEXL(Java Expression Language)是 Apache 基金会的 Java 表达式引擎:把表达式字符串解析为语法树求值,并提供脚本块与文本模板能力。它的 API 呈清晰的四层结构,语法能力则覆盖「运算 → 变量 → 集合 → 控制流 → 函数 → 模板」六个梯度。

💡 技术拓展: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 一个、API 从单表达式到模板完整覆盖;其 JexlPermissions 权限模型适合把动态求值安全地开放给外部输入。

核心 API 分层(第一列即调用入口,均经本文实测):

API角色关键方法详解章节
JexlBuilder引擎构建器(配置入口)permissions、namespaces、create4.1
JexlEngine表达式引擎(核心入口)createExpression、createScript、createJxltEngine4.1
JexlExpression单条表达式对象evaluate4.2
JexlContext / MapContext变量上下文set、(Map 构造)4.2
JexlScript多语句脚本对象execute(支持可变参数)4.3
JxltEngine / Template模板子引擎与模板对象createTemplate、evaluate(ctx, Writer)、asString4.4
JexlPermissions反射权限白名单UNRESTRICTED、RESTRICTED、parse 等4.5

表达式语法地图(全部实测,详见第五章):

语法类示例实测输出
算术/字符串/三元/逻辑1 + 2 * 3、'a' + 'b'、5 > 3 ? 'yes' : 'no'7、ab、yes
变量与 Map 成员x * y、user.name42、Alice
POJO 导航与方法person.name、person.greet()张三、完整问候
集合/数组/Map 下标list[0]、arr[1]、prices['apple']、prices.bananaapple、20、5、3
控制流var total = 0; for(var item : arr) { total = total + item; } return total;60
lambda 与函数(a, b) -> { return a + b; }、function fib(n){ if (n <= 1) return n; return fib(n-1) + fib(n-2); } fib(15)7、610
命名空间函数str:reverse(word)lxej
模板插值与指令Hello ${name}!、$$ if (user.age >= 18) {Hello JEXL 3.7.0!

⚠️ 注意:3.7.0 有四处与旧版不同的默认行为——默认权限收紧、脚本禁全局赋值、模板指令改 $$ 前缀、setFunctions 改名 namespaces。本文各节遇到时报错原文都会给出,可直接对照排障。

三、前置条件

创建工程目录并写入 /opt/jexl-demo/pom.xml(全文):

mkdir -p /opt/jexl-demo/src/main/java/demo
<?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 传递解析。

编译并生成运行 classpath(本文 8 个演示类编译回显中 Compiling 8 source files,读者逐篇追加示例时数字递增属正常):

cd /opt/jexl-demo && mvn compile
mvn dependency:build-classpath -Dmdep.outputFile=cp.txt -q && cat cp.txt

输出节选(实测,mvn compile 尾部与 classpath 全文):

[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] ------------------------------------------------------------------------
/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

验证点:BUILD SUCCESS 说明依赖与编码配置正确;classpath 恰好两个 jar(JEXL 本体 + commons-logging),缺后者运行时报 NoClassDefFoundError: org/apache/commons/logging/LogFactory(报错实录见《Ubuntu 22.04 安装 Apache Commons JEXL 指南》4.7)。

四、核心 API 详解

4.1 JexlBuilder 与 JexlEngine:引擎的创建

JexlBuilder 是引擎的配置入口,链式设置参数后调用 create() 得到 JexlEngine;引擎线程安全、创建后参数不可变,可全局复用一个实例。3.7.0 的关键构建方法可用 javap 核实(实测):

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);

两种实测过的创建方式:

// 方式一:默认引擎(本文多数示例使用;3.7.0 默认权限收紧,自定义类反射被拒)
JexlEngine jexl = new org.apache.commons.jexl3.JexlBuilder().create();

// 方式二:放开反射权限的引擎(POJO 导航、命名空间函数示例使用)
JexlEngine jexl2 = new org.apache.commons.jexl3.JexlBuilder()
        .permissions(org.apache.commons.jexl3.introspection.JexlPermissions.UNRESTRICTED)
        .create();

配置解读:permissions 控制反射白名单(4.5 详测);namespaces 注册函数命名空间(5.6 详测);其余 strict、silent、lexical、cache 等参数的逐项语义见《Apache Commons JEXL 3.7.0 配置参数详解》。

⚠️ 注意:不存在 setFunctions 方法——3.7.0 用 namespaces(Map) 取代(编译报 cannot find symbol,实录见 5.6)。

4.2 JexlExpression 与 JexlContext:表达式求值

JexlEngine.createExpression(String) 解析单条表达式返回 JexlExpression,evaluate(JexlContext) 求值;变量通过 JexlContext 注入,最常用实现是 MapContext(内部即一个 Map<String,Object>,支持用既有 Map 构造)。最小实测程序 demo/BasicEval.java 全文:

package demo;

import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.commons.jexl3.MapContext;

/** 算术、字符串、三元表达式求值 */
public class BasicEval {
    public static void main(String[] args) {
        JexlEngine jexl = new org.apache.commons.jexl3.JexlBuilder().create();
        Object r1 = jexl.createExpression("1 + 2 * 3").evaluate(null);
        System.out.println("1 + 2 * 3 = " + r1);

        Object r2 = jexl.createExpression("(1 + 2) * 3").evaluate(null);
        System.out.println("(1 + 2) * 3 = " + r2);

        Object r3 = jexl.createExpression("10 / 4").evaluate(null);
        System.out.println("10 / 4 = " + r3);

        Object r4 = jexl.createExpression("10 % 3").evaluate(null);
        System.out.println("10 % 3 = " + r4);

        // 字符串拼接
        Object r5 = jexl.createExpression("'Hello, ' + 'JEXL' + ' 3.7.0'").evaluate(null);
        System.out.println("concat: " + r5);

        // 三元表达式
        Object r6 = jexl.createExpression("5 > 3 ? 'yes' : 'no'").evaluate(null);
        System.out.println("ternary: " + r6);

        // 逻辑与比较
        Object r7 = jexl.createExpression("!(5 > 3) && (2 == 2)").evaluate(null);
        System.out.println("logic: " + r7);

        // 字符串方法
        Object r8 = jexl.createExpression("'jexl-demo'.toUpperCase()").evaluate(null);
        System.out.println("string method: " + r8);
    }
}

运行:

cd /opt/jexl-demo
CP="target/classes:$(cat cp.txt)" && java -Dfile.encoding=UTF-8 -cp "$CP" demo.BasicEval

输出:

1 + 2 * 3 = 7
(1 + 2) * 3 = 9
10 / 4 = 2
10 % 3 = 1
concat: Hello, JEXL 3.7.0
ternary: yes
logic: false
string method: JEXL-DEMO

逐行解读:1 + 2 * 3 = 7 说明运算符优先级正确(先乘后加);10 / 4 = 2 是整数除法截断;10 % 3 = 1 取模;+ 对字符串做拼接;三元 5 > 3 ? 'yes' : 'no' 取 yes;!(true) && (true) 得 false;字符串字面量可直接调用 toUpperCase()。evaluate(null) 在表达式不引用任何变量时可用——一旦表达式含变量就必须传非空 JexlContext。

4.3 JexlScript:多语句脚本与可调用对象

createScript(String) 解析脚本块(可含 var 声明、循环、return、函数定义),execute(JexlContext, Object... args) 求值;execute(null, args) 可直接给 lambda 传参。实测程序 demo/ScriptDemo.java 全文:

package demo;

import org.apache.commons.jexl3.JexlContext;
import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlScript;
import org.apache.commons.jexl3.MapContext;

/** JexlScript 多语句脚本:var 声明、return */
public class ScriptDemo {
    public static void main(String[] args) {
        JexlEngine jexl = new org.apache.commons.jexl3.JexlBuilder().create();
        JexlContext ctx = new MapContext();
        ctx.set("n", 10);

        // 多语句:var 声明 + 循环 + return
        JexlScript factorial = jexl.createScript(
                "var result = 1;" +
                "for (var i = 2; i <= n; i = i + 1) {" +
                "  result = result * i;" +
                "}" +
                "return result;"
        );
        System.out.println("factorial(10) = " + factorial.execute(ctx));

        // 带参数的脚本
        JexlScript add = jexl.createScript("(a, b) -> { return a + b; }");
        System.out.println("lambda add(3,4) = " + add.execute(null, 3, 4));

        // 函数定义脚本
        JexlScript func = jexl.createScript(
                "function fib(n) {" +
                "  if (n <= 1) return n;" +
                "  return fib(n - 1) + fib(n - 2);" +
                "} fib(15)"
        );
        System.out.println("fib(15) = " + func.execute(new MapContext()));

        // 脚本赋值到 context
        JexlScript assign = jexl.createScript("var half = n / 2; half");
        System.out.println("half = " + assign.execute(ctx));
    }
}

运行:

cd /opt/jexl-demo
CP="target/classes:$(cat cp.txt)" && java -Dfile.encoding=UTF-8 -cp "$CP" demo.ScriptDemo

输出:

factorial(10) = 3628800
lambda add(3,4) = 7
fib(15) = 610
half = 5

逐行解读:factorial 用 var 声明 result 与循环变量 i,for 循环连乘得 3628800(10! 正确);lambda (a, b) -> { return a + b; } 通过 execute(null, 3, 4) 传参得 7;function fib(n) 递归函数定义后立即调用 fib(15) 得 610(斐波那契第 15 项正确);var half = n / 2 中 n 从上下文取 10,表达式脚本把最后一条表达式的值作为结果返回 5。

💡 说明:execute 不传上下文(传 null)只适用于脚本不引用外部变量的场景;脚本内所有赋值目标(如 result、half)都必须 var 声明——3.7.0 禁止全局赋值,否则报 JexlException$Feature(报错实录见 5.4)。

4.4 JxltEngine:模板渲染

jexl.createJxltEngine() 创建模板子引擎,createTemplate(String) 生成 Template,求值走 evaluate(JexlContext, Writer)(或 asString())。3.7.0 的两条硬规则(javap 与真实报错实测):evaluate(ctx) 无此重载;模板指令为 $$ 行前缀。实测程序 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 纯 ${name} 插值;template2 同时插值 POJO 属性与三元表达式;template3 $$ if 命中 成年人 分支(\\n 是把换行转义显示)且指令块后的 :李四 正常拼接;template4 $$ for 遍历数组渲染 [a][b][c]。旧式 %{if} / #{if} 的两种失败报错原文见 5.7。

4.5 JexlPermissions:反射权限白名单

org.apache.commons.jexl3.introspection.JexlPermissions 有四个静态实例与一个工厂方法:UNRESTRICTED(全放行)、NONE(全拒绝)、RESTRICTED(内置白名单)、SECURE(更严白名单)、parse(String...)(自定义模式白名单)。3.7.0 默认引擎行为等价 RESTRICTED:白名单外类的属性/方法被拦截。一套覆盖全部场景的实测程序 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 行:裸写类名 System 被当作未定义变量,两种引擎下都返回 null——静态方法访问的正规形态是命名空间 ns:method(5.6),这与权限无关。
  • 第 3 行:RESTRICTED 白名单内的 String 方法可用。
  • 第 4、5 行:RESTRICTED 拦截自定义类方法,异常为 JexlException$Method: unsolvable function/method 'reverse(String)'(try-catch 打印,程序未中断)。
  • 第 6、7 行:默认引擎同样拒绝自定义 POJO 的 getter——「默认收紧」的直接证据;且注意拦截报错写的是 undefined property 'name',语义有迷惑性(属性其实存在)。
  • 第 8、9 行:parse("java.util.*") 自定义白名单——ArrayList 可用(返回 true),String 不在白名单被拒返回 null。

五、表达式语法详解

5.1 算术、字符串与逻辑运算

纯运算表达式无需上下文(evaluate(null) 即可),语法与 Java 基本一致,实测对照(输出取自 4.2 BasicEval 运行回显):

表达式实测输出语法要点
1 + 2 * 37乘除优先级高于加减
(1 + 2) * 39括号改变优先级
10 / 42整数除法截断小数
10 % 31取模
'Hello, ' + 'JEXL' + ' 3.7.0'Hello, JEXL 3.7.0+ 兼作字符串拼接,字符串用单引号
5 > 3 ? 'yes' : 'no'yesJava 风格三元
!(5 > 3) && (2 == 2)false逻辑非/与/等于
'jexl-demo'.toUpperCase()JEXL-DEMO字符串字面量可直接调方法

验证手段:上述八行与 4.2 的运行回显逐行一致,且每行结果可手工验算,语法行为与 Java 语义对齐。

5.2 变量注入与 Map 成员访问

上下文变量支持构造时批量注入(new MapContext(vars))与运行时写入(ctx.set("x", 100)),Map 类型变量支持点式成员访问。实测程序 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 验证构造注入;中文变量值拼接无乱码;ctx.set("x", 100) 后 100 + 6 = 106 验证运行时写入;脚本行输出 1200 的时序是——执行脚本时 x 已是 100,z = 100 * 6 = 600、z * 2 = 1200;user.name/user.score 点式访问 Map 等价 map.get(key)。

5.3 POJO 属性导航与方法调用

POJO 按 JavaBean 规范暴露 getter 时,表达式可点式访问属性并调用任意 public 方法。实测程序 demo/PojoNav.java 全文(注意引擎必须显式放开权限,原因见 5.3 末尾):

package demo;

import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.JexlExpression;
import org.apache.commons.jexl3.MapContext;

/** 自定义 POJO 属性导航与方法调用 */
public class PojoNav {
    public static class Person {
        private final String name;
        private final int age;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() { return name; }
        public int getAge() { return age; }
        public String greet() { return "你好, 我是 " + name + ", 今年 " + age + " 岁"; }
        public boolean isAdult() { return age >= 18; }
    }

    public static void main(String[] args) {
        // 实测:3.7.0 默认引擎(未设 permissions)对自定义 POJO 属性导航报 undefined property,
        // 需显式放开为 UNRESTRICTED 才能访问 demo 类的 getter
        JexlEngine jexl = new org.apache.commons.jexl3.JexlBuilder()
                .permissions(org.apache.commons.jexl3.introspection.JexlPermissions.UNRESTRICTED)
                .create();
        MapContext ctx = new MapContext();
        ctx.set("person", new Person("张三", 20));
        ctx.set("child", new Person("小明", 12));

        Object r1 = jexl.createExpression("person.name").evaluate(ctx);
        System.out.println("person.name = " + r1);

        Object r2 = jexl.createExpression("person.age").evaluate(ctx);
        System.out.println("person.age = " + r2);

        Object r3 = jexl.createExpression("person.greet()").evaluate(ctx);
        System.out.println("person.greet() = " + r3);

        Object r4 = jexl.createExpression("person.age > 18 ? '成年' : '未成年'").evaluate(ctx);
        System.out.println("person ternary = " + r4);

        Object r5 = jexl.createExpression("child.age > 18 ? '成年' : '未成年'").evaluate(ctx);
        System.out.println("child ternary = " + r5);

        Object r6 = jexl.createExpression("person.isAdult()").evaluate(ctx);
        System.out.println("person.isAdult() = " + r6);
    }
}

运行:

cd /opt/jexl-demo
CP="target/classes:$(cat cp.txt)" && java -Dfile.encoding=UTF-8 -cp "$CP" demo.PojoNav

输出:

person.name = 张三
person.age = 20
person.greet() = 你好, 我是 张三, 今年 20 岁
person ternary = 成年
child ternary = 未成年
person.isAdult() = true

逐行解读:person.name 经 getName() 取值(点式属性即 getter 调用);greet() 直接调用带中文返回的方法;两个三元按 age 与 18 比较分别取 成年/未成年;isAdult() 返回 true。

若用默认引擎(不设 permissions),POJO 导航被权限拦截,真实报错(原文照抄):

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' 不代表属性不存在,而是 3.7.0 默认 JexlPermissions 把自定义类的属性发现(introspection)拦掉了——遇此报错先检查 permissions,不要去找「属性名拼错」的问题。

5.4 集合、数组与 Map 下标

List、数组、Map 都支持下标访问,Map 额外支持点式访问;配合脚本可完成遍历求和/拼接。实测程序 demo/CollectionEval.java 全文:

package demo;

import org.apache.commons.jexl3.JexlEngine;
import org.apache.commons.jexl3.MapContext;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/** 集合:数组/Map 下标访问、size()、for 循环脚本 */
public class CollectionEval {
    public static void main(String[] args) {
        JexlEngine jexl = new org.apache.commons.jexl3.JexlBuilder().create();
        MapContext ctx = new MapContext();

        List<String> list = new ArrayList<>();
        list.add("apple");
        list.add("banana");
        list.add("cherry");
        ctx.set("list", list);

        int[] arr = {10, 20, 30};
        ctx.set("arr", arr);

        Map<String, Integer> prices = new HashMap<>();
        prices.put("apple", 5);
        prices.put("banana", 3);
        ctx.set("prices", prices);

        // 列表下标
        System.out.println("list[0] = " + jexl.createExpression("list[0]").evaluate(ctx));
        System.out.println("list.get(2) = " + jexl.createExpression("list.get(2)").evaluate(ctx));
        System.out.println("list.size() = " + jexl.createExpression("list.size()").evaluate(ctx));

        // 数组下标
        System.out.println("arr[1] = " + jexl.createExpression("arr[1]").evaluate(ctx));

        // Map 下标与属性式访问
        System.out.println("prices['apple'] = " + jexl.createExpression("prices['apple']").evaluate(ctx));
        System.out.println("prices.banana = " + jexl.createExpression("prices.banana").evaluate(ctx));

        // for 循环(脚本块),3.7.0 需用 var 声明局部变量
        Object sum = jexl.createScript(
                "var total = 0; for(var item : arr) { total = total + item; } return total;"
        ).execute(ctx);
        System.out.println("for-sum(arr) = " + sum);

        Object joined = jexl.createScript(
                "var sb = ''; for(var f : list) { sb = sb + f + ','; } return sb;"
        ).execute(ctx);
        System.out.println("for-join(list) = " + joined);

        // 三目 + 集合组合
        Object r = jexl.createExpression("list.size() > 2 ? '列表够大' : '列表太小'").evaluate(ctx);
        System.out.println("size ternary = " + r);
    }
}

运行:

cd /opt/jexl-demo
CP="target/classes:$(cat cp.txt)" && java -Dfile.encoding=UTF-8 -cp "$CP" demo.CollectionEval

输出:

list[0] = apple
list.get(2) = cherry
list.size() = 3
arr[1] = 20
prices['apple'] = 5
prices.banana = 3
for-sum(arr) = 60
for-join(list) = apple,banana,cherry,
size ternary = 列表够大

逐行解读:list[0] 与 list.get(2)、list.size() 分别验证下标语法与普通方法调用;arr[1] = 20 验证原生数组下标;prices['apple'] 与 prices.banana 两种 Map 访问形态等价;for-sum 对 {10,20,30} 求和得 60、for-join 拼接出 apple,banana,cherry,;三元与 list.size() 组合取 列表够大。

5.5 控制流:var、for 与 return

脚本块内的控制流语法要点(全部实测):

  • var 声明是强制的:脚本内所有赋值目标(包括循环体内赋值的累计变量)都要 var 声明。3.7.0 默认禁止全局赋值,违反时报错(原文照抄,出自 5.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)

堆栈显示报错从 createScript 抛出——该控制发生在解析阶段,脚本根本不会被执行;@1:1 指向赋值位置,in 'z' 指明被禁的赋值目标。循环变量同理,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)
  • for 两种形态:for(var item : arr) 增强 for 遍历集合/数组(5.4 实测),for (var i = 2; i <= n; i = i + 1) 经典三段式(4.3 factorial 实测)。
  • return 显式返回:return result; 立即结束脚本;脚本末尾不带 return 时,最后一条表达式的值即结果(4.3 的 var half = n / 2; half 返回 5 实测)。
  • if/else:if (n <= 1) return n;(4.3 fib 函数内实测)。

验证手段:上述每个语法点都有对应的成功输出(3628800、610、60、5),报错修复前后对比见 5.2 与 5.4 的运行回显。

5.6 lambda 与函数定义

脚本支持 lambda 表达式与命名函数定义两种「可调用结构」(实测程序与输出见 4.3 ScriptDemo):

  • lambda:(a, b) -> { return a + b; },通过 JexlScript.execute(null, 3, 4) 按位置传参,实测输出 lambda add(3,4) = 7。
  • 函数定义:function fib(n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); } 定义后可在同脚本内递归调用,实测 fib(15) = 610(第 15 个斐波那契数)。

语法对照:

形态脚本写法调用方式实测结果
lambda(a, b) -> { return a + b; }execute(null, 3, 4)7
命名函数function fib(n) { if (n <= 1) return n; return fib(n - 1) + fib(n - 2); } fib(15)定义后立即调用610

💡 说明:lambda 与函数体内同样遵守 var 声明与 return 语义;参数按位置绑定,与 Java 的 lambda 形态一致,迁移成本很低。

5.7 命名空间函数与模板指令

命名空间函数:JexlBuilder.namespaces(Map) 把工具类注册进引擎,表达式以 命名空间:方法 形式调用(如 str:reverse(word))。实测程序 demo/FunctionNS.java 全文:

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));
    }
}

运行:

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;组合表达式 9 + 4 = 13 证明命名空间函数可与原生运算自由混用。两个排障实录:其一,旧 API setFunctions 编译报 cannot find symbol(3.7.0 已移除):

[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

其二,默认引擎下自定义类方法被权限拦截(原文照抄):

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)

模板插值与 $$ 指令:插值 ${name} 等保持 Java EL 习惯;指令必须用 $$ 行前缀。两种旧式写法的失败实录(原文照抄):%{if} 指令不被识别而原样输出、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)

$$ 行前缀的正确形态($$ if (user.age >= 18) {、$$ for (var it : items) {)与成功输出见 4.4 TemplateDemo(template3: 成年人\n:李四、template4: [a]\n[b]\n[c]\n)。模板完整示例程序与逐行解读见 4.4。

六、注意事项

综合全文实测,使用 JEXL 3.7.0 时按重要度排列的注意事项:

  1. 运行 classpath 必须含 commons-logging:3.7.0 唯一传递依赖,手工拼 classpath 缺它即报 NoClassDefFoundError: org/apache/commons/logging/LogFactory(实录见《Ubuntu 22.04 安装 Apache Commons JEXL 指南》4.7)。
  2. 自定义类反射默认被拒:默认引擎(不设 permissions)对 POJO 报 undefined property、对命名空间方法报 unsolvable function/method,需显式 JexlPermissions.UNRESTRICTED 或 parse("java.util.*") 白名单(4.5 全量实测)。
  3. 脚本变量一律 var 声明:全局赋值在解析阶段即被拒(JexlException$Feature,5.5 实录),从旧版脚本迁移时逐个补 var。
  4. 模板指令用 $$ 行前缀:旧式 %{}/#{} 分别表现为原样输出与解析报错(5.7 实录);Template.evaluate 必须传 Writer。
  5. 中文编码两头固定:编译 -encoding UTF-8(Maven 工程用 sourceEncoding)、运行 -Dfile.encoding=UTF-8,否则遇 POSIX locale 报 unmappable character(实录见《Ubuntu 22.04 安装 Apache Commons JEXL 指南》5.5)。
  6. 上下文写入有时序效应:ctx.set("x", 100) 会影响其后的所有求值(5.2 的 1200 案例实测),求值服务中应避免复用可变上下文,或每次求值前重建 MapContext。
  7. 表达式解析结果有缓存:同一字符串反复解析命中缓存,性能敏感场景保持表达式字符串稳定(勿手工拼参)而用上下文变量传值。

七、常见问题(FAQ)

Q1:表达式里的变量明明 set 了,为什么求值是 null 或报 undefined?

先查上下文是否传对:evaluate(null) 只适用于无变量表达式;确认变量名大小写一致;再确认没有踩 4.5 的权限拦截(自定义 POJO 属性被拒时也报 undefined property,需放开 permissions)。

Q2:报 JexlException$Feature: global assign/modify error in 'z' 怎么解决?

3.7.0 禁止脚本对未声明变量赋值且在解析阶段拦截(5.5 实录堆栈)。给报错指明的目标(如实例中的 z、total)加 var 声明即可,循环体内的累计变量同样要声明。

Q3:POJO 属性报 undefined property 但 getter 存在?

默认权限收紧所致,与属性是否存在无关。用 .permissions(JexlPermissions.UNRESTRICTED) 放开(4.5/5.3 实测),或用 parse("java.util.*") 白名单精确放行。

Q4:模板指令怎么写?旧教程的 %{if} / #{if} 为什么失败?

3.7.0 指令统一为 $$ 行前缀(如 $$ if (user.age >= 18) {)。%{} 旧式写法指令不被执行(原样输出,实录见 5.7);#{} 旧式写法直接报 JexlException$Parsing: parsing error in 'if'(实录见 5.7)。插值 ${name} 等语法不变。

Q5:怎么调用自定义工具方法?

两种形态:表达式内可访问的实例方法直接 object.method()(如 person.greet()、'abc'.length());跨对象的静态工具用命名空间 ns:method 形式,注册方式 JexlBuilder.namespaces(Map)(5.6 实测四种调用形态)。注意裸写类名(如 System.getProperty('user.dir'))只是未定义变量,返回 null(4.5 实测)。

八、参考链接

  1. Apache Commons JEXL 官方站点:https://commons.apache.org/proper/commons-jexl/
  2. Apache Commons JEXL 3.7.0 Javadoc: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. 官方发行下载目录:https://downloads.apache.org/commons/jexl/