Apache Jelly 在 Ubuntu 22.04 集成使用指南

在 Ubuntu 22.04 + OpenJDK 17 上从零组装 commons-jelly 1.0.1 运行环境:依赖 jar 集合下载、jelly 启动脚本、第一个 .jelly 脚本、核心标签(forEach/if/expr 等)与 JellyContext API 嵌入的完整实测流程。

Apache JellyApache JellyJavaUbuntuUbuntu 22.04XML2026/10/16

Apache Jelly 在 Ubuntu 22.04 集成使用指南

目标:在一台全新的 Ubuntu 22.04 服务器上,从零组装 Apache Commons Jelly 1.0.1 的运行环境——下载依赖 jar 集合、编写 jelly 启动脚本、跑通第一个 .jelly 脚本、实测核心标签(set/forEach/if/choose/while/switch/expr/catch/invokeStatic 等)与 JellyContext Java API 嵌入调用。全部命令与回显均已在 Ubuntu 22.04 + OpenJDK 17.0.20.1 实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04(x86_64)
  • JDK:OpenJDK 17.0.20.1(apt 仓库 openjdk-17-jdk-headless)
  • 辅助工具:curl、wget、unzip(apt 安装)
  • 实测对象:commons-jelly 1.0.1(Maven Central 坐标 commons-jelly:commons-jelly,最新正式版)
  • 运行环境目录:/opt/jelly/lib/(依赖 jar 集合)
  • 示例目录:/root/jelly-demo/

核实 JDK 与系统版本的真实回显:

cat /etc/os-release | head -2
PRETTY_NAME="Ubuntu 22.04.x LTS"
NAME="Ubuntu"
java -version
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)

💡 技术拓展:Jelly 依赖的 XML 解析器有几种接入方式——

方式原理说明
JDK 内置 JAXPJDK 自带 Xerces 移植版零配置,推荐
显式引入 xerces 2.2.1用 POM 声明的老版本覆盖本实测按 POM 全量引入
dom4j + jaxenDOM4J 文档模型 + XPathJelly 内部用于 XML 标签库

本次实测按 POM 全量引入(含 xerces 2.2.1),从堆栈可见引擎实际走的是 classpath 上的 org.apache.xerces(如 AbstractSAXParser),即显式依赖优先于 JDK 内置解析器。

二、前置条件

2.1 安装基础工具与 JDK

apt-get update
apt-get install -y openjdk-17-jdk-headless curl wget unzip

实测回显(节选):

Setting up openjdk-17-jdk-headless:amd64 (17.0.20.1+1-1-22.04-Ubuntu) ...
Setting up unzip:amd64 (6.0-26ubuntu3.2) ...

验证:

java -version && curl --version | head -1
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)
curl 7.81.0 (x86_64-pc-linux-gnu) libcurl/7.81.0 OpenSSL/3.0.2

2.2 依赖 jar 集合(实测清单)

commons-jelly 1.0.1 的 POM 声明了 15 个依赖,运行期真正必需的是其中 12 个左右。本次实测「POM 全量引入」方案,清单如下(全部来自 Maven Central):

构件版本作用必需性
commons-jelly1.0.1引擎本体必需
commons-jexl1.0表达式语言 ${...}必需
commons-cli1.0命令行解析(jelly 命令的参数)必需(CLI 方式)
commons-lang2.0工具类必需
commons-discovery20030211.213356服务发现必需
commons-beanutils1.6标签属性反射注入必需
commons-collections2.1集合工具必需
commons-logging1.0.3日志门面必需
dom4j1.5.2XML 文档模型必需(xml 标签库)
jaxen1.1-beta-4XPath 引擎必需(XPath)
saxpath1.0-FCSjaxen 1.1-beta-4 的 XPath 语法层必需(jaxen 1.1-beta-4 拆分包)
forehead1.0-beta-5老式类加载器(POM 声明)建议引入
jstl1.0.6JSTL 兼容建议引入
junit3.8.1测试框架可选(仅测试用)
servletapi2.3Servlet 标签库依赖可选(CLI 场景不用)
xml-apis1.0.b2老 XML API可选(JDK 17 内置 JAXP 可替代)
xerces2.2.1SAX 解析器实现建议引入(POM 原生)

⚠️ 注意:saxpath 不在 1.0.1 的 POM 里,但 jaxen 1.1-beta-4 运行期会用到它(org.saxpath 包);漏掉时使用 XPath 功能会抛 NoClassDefFoundError。本表按实测结果补充。

依赖缺失时的典型报错(实测样例,供对照排障):

Exception in thread "main" java.lang.NoClassDefFoundError: org/apache/tools/ant/BuildLogger
        at java.base/java.lang.Class.getDeclaredConstructors0(Native Method)
        ...
        at org.apache.commons.jelly.JellyContext.getTagLibrary(JellyContext.java:473)

三、依赖引入(下载与校验)

3.1 创建目录并下载 jar

mkdir -p /opt/jelly/lib
cd /opt/jelly/lib
BASE=https://repo1.maven.org/maven2

逐个下载(curl -fsSL -o 文件名 URL,每条独立可执行):

curl -fsSL -o commons-jelly-1.0.1.jar          $BASE/commons-jelly/commons-jelly/1.0.1/commons-jelly-1.0.1.jar
curl -fsSL -o commons-cli-1.0.jar              $BASE/commons-cli/commons-cli/1.0/commons-cli-1.0.jar
curl -fsSL -o commons-lang-2.0.jar             $BASE/commons-lang/commons-lang/2.0/commons-lang-2.0.jar
curl -fsSL -o commons-discovery-20030211.213356.jar $BASE/commons-discovery/commons-discovery/20030211.213356/commons-discovery-20030211.213356.jar
curl -fsSL -o forehead-1.0-beta-5.jar          $BASE/forehead/forehead/1.0-beta-5/forehead-1.0-beta-5.jar
curl -fsSL -o jstl-1.0.6.jar                   $BASE/jstl/jstl/1.0.6/jstl-1.0.6.jar
curl -fsSL -o junit-3.8.1.jar                  $BASE/junit/junit/3.8.1/junit-3.8.1.jar
curl -fsSL -o commons-jexl-1.0.jar             $BASE/commons-jexl/commons-jexl/1.0/commons-jexl-1.0.jar
curl -fsSL -o xml-apis-1.0.b2.jar              $BASE/xml-apis/xml-apis/1.0.b2/xml-apis-1.0.b2.jar
curl -fsSL -o commons-beanutils-1.6.jar        $BASE/commons-beanutils/commons-beanutils/1.6/commons-beanutils-1.6.jar
curl -fsSL -o commons-collections-2.1.jar      $BASE/commons-collections/commons-collections/2.1/commons-collections-2.1.jar
curl -fsSL -o commons-logging-1.0.3.jar        $BASE/commons-logging/commons-logging/1.0.3/commons-logging-1.0.3.jar
curl -fsSL -o dom4j-1.5.2.jar                  $BASE/dom4j/dom4j/1.5.2/dom4j-1.5.2.jar
curl -fsSL -o jaxen-1.1-beta-4.jar             $BASE/jaxen/jaxen/1.1-beta-4/jaxen-1.1-beta-4.jar
curl -fsSL -o saxpath-1.0-FCS.jar              $BASE/saxpath/saxpath/1.0-FCS/saxpath-1.0-FCS.jar
curl -fsSL -o xerces-2.2.1.jar                 $BASE/xerces/xerces/2.2.1/xerces-2.2.1.jar
curl -fsSL -o servletapi-2.3.jar               $BASE/servletapi/servletapi/2.3/servletapi-2.3.jar

下载完成的文件清单(实测 ls -la 输出):

-rw-r--r-- 1 root root 118483 commons-beanutils-1.6.jar
-rw-r--r-- 1 root root  30117 commons-cli-1.0.jar
-rw-r--r-- 1 root root 165119 commons-collections-2.1.jar
-rw-r--r-- 1 root root  72176 commons-discovery-20030211.213356.jar
-rw-r--r-- 1 root root 160069 commons-jelly-1.0.1.jar
-rw-r--r-- 1 root root 131458 commons-jexl-1.0.jar
-rw-r--r-- 1 root root 169763 commons-lang-2.0.jar
-rw-r--r-- 1 root root  31605 commons-logging-1.0.3.jar
-rw-r--r-- 1 root root 312509 dom4j-1.5.2.jar
-rw-r--r-- 1 root root  12847 forehead-1.0-beta-5.jar
-rw-r--r-- 1 root root 242743 jaxen-1.1-beta-4.jar
-rw-r--r-- 1 root root  20812 jstl-1.0.6.jar
-rw-r--r-- 1 root root 121070 junit-3.8.1.jar
-rw-r--r-- 1 root root  23563 saxpath-1.0-FCS.jar
-rw-r--r-- 1 root root  77977 servletapi-2.3.jar
-rw-r--r-- 1 root root 834966 xerces-2.2.1.jar
-rw-r--r-- 1 root root 109318 xml-apis-1.0.b2.jar

3.2 校验 jar 完整性

对关键 jar 计算 sha256(实测值,供读者下载后比对):

cd /opt/jelly/lib && sha256sum commons-jelly-1.0.1.jar commons-jexl-1.0.jar dom4j-1.5.2.jar
9ff2648995b4ee02dab24bbe8b05deacada7fa54c7bb78454badd357ba48e674  commons-jelly-1.0.1.jar
8358bca1d1e55409aefd1daff8ac3d96ccd652e991f4ebf076ef3bc4a0205738  commons-jexl-1.0.jar
e6a026b7aa343e14f5eadb32a089954ad89548006845ccd10effb234cc8be19c  dom4j-1.5.2.jar

四、安装 jelly 启动脚本

4.1 编写启动脚本

Central 不提供带启动器的发行包,自行写一个把 /opt/jelly/lib 全部 jar 拼进 classpath 的包装脚本:

cat > /usr/local/bin/jelly <<'EOF'
#!/bin/bash
# jelly 启动脚本:以完整 classpath 调用 org.apache.commons.jelly.Jelly
JELLY_HOME=/opt/jelly
CP=$(ls $JELLY_HOME/lib/*.jar | tr '\n' ':')
exec java -cp "${CP%/}" org.apache.commons.jelly.Jelly "$@"
EOF
chmod +x /usr/local/bin/jelly

主类名依据(jar 内实测可见):org/apache/commons/jelly/Jelly.class 是官方 CLI 入口,其 main 方法委托给 CommandLineParser.invokeCommandLineJelly 处理参数。

4.2 验证安装

jelly --help

真实回显:

usage: jelly [scriptFile] [-script scriptFile] [-o outputFile]
             [-Dsysprop=syspropval] [-awt]
 -swing         Synonym of "-awt".
 -awt           Wether to run in the AWT thread.
 -h,--help      Give this help message
 -o             Output file
 -script        Jelly script to run
 -v,--version   prints Jelly's version and exits

⚠️ 注意:jelly -v 在 Central 的 1.0.1 jar 上会崩溃(jar 缺少 jelly-version.txt 资源),这是发布打包缺陷而非安装问题。真实回显:

Exception in thread "main" java.lang.IllegalStateException: Resource "jelly-version.txt" not found.
        at org.apache.commons.jelly.Jelly.readBuildTimestampResource(Jelly.java:128)
        at org.apache.commons.jelly.Jelly.getJellyVersion(Jelly.java:108)
        at org.apache.commons.jelly.util.CommandLineParser.invokeCommandLineJelly(CommandLineParser.java:82)
        at org.apache.commons.jelly.Jelly.main(Jelly.java:93)

判断安装是否成功以 jelly --help 与实际运行脚本为准。

五、核心 API 与配置机制

5.1 三个核心类

类用途实测要点
JellyContext变量上下文setVariable/getVariable 与脚本 ${var} 双向互通;runScript(File, XMLOutput) 一行即跑
Script编译后的脚本context.compileScript(url) 得到;run(context, output) 可重复执行
XMLOutput输出通道createXMLOutput(System.out) 输出到标准输出,也可指向文件

5.2 脚本根元素与命名空间

.jelly 文件就是 XML 文档,根元素习惯用 j:jelly 并把 xmlns:j="jelly:core" 指到 core 标签库。jelly:core 这类 URI 的解析规则写在主 jar 的 org/apache/commons/jelly/jelly.properties(实测解包可见 core = org.apache.commons.jelly.tags.core.CoreTagLibrary),引擎按命名空间 URI 实例化对应 TagLibrary。

5.3 jelly.properties 变量注入

Jelly 引擎启动时会依次从 用户主目录 → 当前目录 → classpath 加载 jelly.properties,把其中的键值注入为脚本变量。实测:在当前目录放置

who=from-jelly.properties
mode=demo

运行脚本 <p>who = ${who} / mode = ${mode}</p> 的真实输出:

who = from-jelly.properties / mode = demo

六、集成示例(可独立执行)

6.1 示例一:第一个 .jelly 脚本

mkdir -p /root/jelly-demo
cat > /root/jelly-demo/hello.jelly <<'EOF'
<?xml version="1.0"?>
<j:jelly xmlns:j="jelly:core">
  Hello, Jelly!
  <j:set var="name" value="Ubuntu 22.04"/>
  Hello, ${name}!
</j:jelly>
EOF
cd /root/jelly-demo && jelly hello.jelly

真实回显:

Hello, Jelly!
  Hello, Ubuntu 22.04!

要点:<j:set> 把变量 name 写入上下文;${name} 由 JEXL 求值。输出中的缩进来自 XML 源文件的空白(可用 <j:whitespace trim="true"> 控制)。

6.2 示例二:核心标签综合(forEach/if/expr/while/switch/catch)

cat > /root/jelly-demo/core-tags.jelly <<'EOF'
<?xml version="1.0"?>
<j:jelly xmlns:j="jelly:core">

  <j:set var="os" value="Ubuntu 22.04"/>
  <j:set var="ver" value="1.0.1"/>
  1) set+expr: ${os} + ${ver}

  <j:new var="apps" className="java.util.ArrayList"/>
  <j:invoke on="${apps}" method="add">
    <j:arg type="java.lang.String" value="nginx"/>
  </j:invoke>
  <j:invoke on="${apps}" method="add">
    <j:arg type="java.lang.String" value="redis"/>
  </j:invoke>
  <j:invoke on="${apps}" method="add">
    <j:arg type="java.lang.String" value="jelly"/>
  </j:invoke>
  list.size = ${apps.size()}
  <j:forEach var="item" items="${apps}">
  2) forEach item = ${item}
  </j:forEach>

  <j:forEach var="i" begin="1" end="5" step="2">
  3) forEach index i = ${i}
  </j:forEach>

  <j:if test="${os == 'Ubuntu 22.04'}">
  4) if: os is Ubuntu 22.04
  </j:if>

  <j:choose>
    <j:when test="${ver >= '2.0'}">
  5) choose: ver >= 2.0
    </j:when>
    <j:otherwise>
  5) choose: ver &lt; 2.0
    </j:otherwise>
  </j:choose>

  <j:set var="n" value="${0}"/>
  <j:while test="${n &lt; 3}">
  6) while n = ${n}
    <j:set var="n" value="${n + 1}"/>
  </j:while>

  <j:switch on="${os}">
    <j:case value="CentOS 8">
  7) switch: CentOS branch
    </j:case>
    <j:case value="Ubuntu 22.04">
  7) switch: Ubuntu 22.04 branch
    </j:case>
    <j:default>
  7) switch: default branch
    </j:default>
  </j:switch>

  <j:set var="word" value="jelly"/>
  <j:expr value="${word.toUpperCase()}"/> (8) expr tag printed above

  <j:invokeStatic var="millis" className="java.lang.System" method="currentTimeMillis"/>
  9) invokeStatic System.currentTimeMillis = ${millis > 0}
  <j:getStatic var="sep" className="java.io.File" field="pathSeparator"/>
  9) getStatic File.pathSeparator = [${sep}]

  <j:catch var="err">
    <j:include file="no-such-include.jelly"/>
  </j:catch>
  10) catch captured: ${err}

</j:jelly>
EOF
cd /root/jelly-demo && jelly core-tags.jelly

真实回显:


  1) set+expr: Ubuntu 22.04 + 1.0.1



  list.size = 3
  2) forEach item = nginx2) forEach item = redis2) forEach item = jelly3) forEach index i = 13) forEach index i = 33) forEach index i = 54) if: os is Ubuntu 22.045) choose: ver < 2.0
  6) while n = 0

  6) while n = 1

  6) while n = 2
    7) switch: Ubuntu 22.04 branchJELLY(8) expr tag printed above
  9) invokeStatic System.currentTimeMillis = true

  9) getStatic File.pathSeparator = [:]



  10) catch captured: org.apache.commons.jelly.JellyTagException: file:/root/jelly-demo/core-tags.jelly:77:46: <j:include> could not include jelly script: no-such-include.jelly. Reason: org.apache.commons.jelly.JellyException: null:-1:-1: <null> Could not parse Jelly script

逐项说明:

  • j:new + j:invoke + j:arg:创建 ArrayList 并逐个 add 元素——这是 JEXL 1.0 下的标准「造列表」姿势(不能用 [...] 字面量);
  • j:forEach items="${apps}":迭代 Java 集合;begin/end/step 形式迭代数字区间;
  • j:if/choose/when/otherwise/switch/case/default:三种分支结构;
  • j:expr 输出表达式结果,word.toUpperCase() 演示对 变量 调用 Java 方法;
  • j:invokeStatic/j:getStatic:直接调用静态方法/读取静态字段;
  • j:catch var="err":捕获 标签级 异常存入 err(表达式求值异常在求值层被吞掉、不会进 catch,实测确认)。

⚠️ 注意:示例中 ver &lt; 2.0、n &lt; 3 处用了 XML 实体 &lt;——.jelly 是 XML,文本与属性里的裸 < 会直接破坏良构性,实测报错 The content of elements must consist of well-formed character data or markup。

6.3 示例三:JellyContext Java API 嵌入

示例脚本 /root/jelly-demo/api.jelly:

<?xml version="1.0"?>
<j:jelly xmlns:j="jelly:core">
  Hello ${user} from JellyContext API! (x${count})
  <j:set var="greeting" value="done-${count}"/>
</j:jelly>

Java 调用端 /root/jelly-demo/JellyApiDemo.java:

import java.io.File;

import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.Script;
import org.apache.commons.jelly.XMLOutput;

public class JellyApiDemo {
    public static void main(String[] args) throws Exception {
        File scriptFile = new File("/root/jelly-demo/api.jelly");

        // 方式一:runScript 直接运行
        JellyContext ctx1 = new JellyContext();
        ctx1.setVariable("user", "server-admin");
        ctx1.setVariable("count", 3);
        System.out.println("== mode 1: runScript ==");
        ctx1.runScript(scriptFile, XMLOutput.createXMLOutput(System.out));
        System.out.println();
        System.out.println("variable written by script: greeting = " + ctx1.getVariable("greeting"));

        // 方式二:compileScript 编译后运行
        JellyContext ctx2 = new JellyContext();
        ctx2.setVariable("user", "root");
        ctx2.setVariable("count", 7);
        Script script = ctx2.compileScript(scriptFile.toURL());
        System.out.println("== mode 2: compileScript + run ==");
        script.run(ctx2, XMLOutput.createXMLOutput(System.out));
        System.out.println();
        System.out.println("variable written by script: greeting = " + ctx2.getVariable("greeting"));

        // 方式三:同一编译产物复用(换变量再跑一次)
        ctx2.setVariable("count", 9);
        System.out.println("== mode 3: reuse compiled Script ==");
        script.run(ctx2, XMLOutput.createXMLOutput(System.out));
        System.out.println();
        System.out.println("variable written by script: greeting = " + ctx2.getVariable("greeting"));
    }
}

编译与运行(注意 -encoding UTF-8,原因见七、Q4):

cd /root/jelly-demo
CP=$(ls /opt/jelly/lib/*.jar | tr '\n' ':')
javac -encoding UTF-8 -cp "${CP%/}" JellyApiDemo.java
java -cp ".:${CP%/}" JellyApiDemo

真实回显:

Note: JellyApiDemo.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
== mode 1: runScript ==

  Hello server-admin from JellyContext API! (x3)

variable written by script: greeting = done-3
== mode 2: compileScript + run ==

  Hello root from JellyContext API! (x7)

variable written by script: greeting = done-7
== mode 3: reuse compiled Script ==

  Hello root from JellyContext API! (x9)

variable written by script: greeting = done-9

三种方式全部成功:变量 Java 传入(user/count)与 脚本回写(greeting)双向互通;编译产物 Script 复用时换变量重跑,输出随之变化(x9/done-9)。toURL() 的 deprecation 提示可忽略(该方法至今仍在)。

七、注意事项与常见问题

Q1:items="${['a','b']}" 直接报 ParseException?

JEXL 1.0 不支持列表字面量。实测报错(节选):

org.apache.commons.jelly.JellyException: null:-1:-1: <null> Unable to create expression: ['nginx','redis','jelly']
Caused by: org.apache.commons.jexl.parser.ParseException: Encountered "[" at line 1, column 1.

解法:用 j:new+j:invoke 构造 ArrayList(见六、2),或在 Java 侧把集合放进上下文。Map 字面量 ${{'a':'1'}} 同样不支持(词法层直接报 Lexical error ... Encountered: ":")。

Q2:对字符串字面量调方法报错?

${'jelly'.toUpperCase()} 实测报 Unable to create expression: 'jelly'.toUpperCase()。解法:先 <j:set var="word" value="jelly"/>,再 ${word.toUpperCase()}(对变量调方法没问题)。

Q3:${1/0} 没有抛异常,catch 不到?

JEXL 1.0 的 / 是浮点除,1/0 求值为 0.0 不抛异常;且表达式求值异常发生在求值层、不进 j:catch。要测 j:catch 需用标签级异常(如 include 不存在的文件,见六、2 第 10 项)。

Q4:javac 编译报 unmappable character (0xE6)?

Ubuntu 22.04 最小化安装常是 POSIX locale,javac 默认按 US-ASCII 读源码,中文注释/字符串全报错。实测报错形态:

JellyApiDemo.java:8: error: unmappable character (0xE6) for encoding US-ASCII
 * JellyContext API ?????????

解法:javac -encoding UTF-8 ...;根治用 apt install locales 配置 zh_CN.UTF-8 或 C.UTF-8 并 export LANG=C.UTF-8。

Q5:脚本文件放 classpath 里也能跑?

是。脚本解析顺序(源码核实):先按 classpath 资源 找(ClassLoader.getResource),再按 文件路径 找,最后按 URL 处理。放 /root/jelly-demo/ 下用相对/绝对路径均可。

八、参考链接