Apache Jelly 在 Ubuntu 22.04 集成使用指南
在 Ubuntu 22.04 + OpenJDK 17 上从零组装 commons-jelly 1.0.1 运行环境:依赖 jar 集合下载、jelly 启动脚本、第一个 .jelly 脚本、核心标签(forEach/if/expr 等)与 JellyContext API 嵌入的完整实测流程。
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 等)与JellyContextJava 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 内置 JAXP JDK 自带 Xerces 移植版 零配置,推荐 显式引入 xerces 2.2.1 用 POM 声明的老版本覆盖 本实测按 POM 全量引入 dom4j + jaxen DOM4J 文档模型 + XPath Jelly 内部用于 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-jelly | 1.0.1 | 引擎本体 | 必需 |
| commons-jexl | 1.0 | 表达式语言 ${...} | 必需 |
| commons-cli | 1.0 | 命令行解析(jelly 命令的参数) | 必需(CLI 方式) |
| commons-lang | 2.0 | 工具类 | 必需 |
| commons-discovery | 20030211.213356 | 服务发现 | 必需 |
| commons-beanutils | 1.6 | 标签属性反射注入 | 必需 |
| commons-collections | 2.1 | 集合工具 | 必需 |
| commons-logging | 1.0.3 | 日志门面 | 必需 |
| dom4j | 1.5.2 | XML 文档模型 | 必需(xml 标签库) |
| jaxen | 1.1-beta-4 | XPath 引擎 | 必需(XPath) |
| saxpath | 1.0-FCS | jaxen 1.1-beta-4 的 XPath 语法层 | 必需(jaxen 1.1-beta-4 拆分包) |
| forehead | 1.0-beta-5 | 老式类加载器(POM 声明) | 建议引入 |
| jstl | 1.0.6 | JSTL 兼容 | 建议引入 |
| junit | 3.8.1 | 测试框架 | 可选(仅测试用) |
| servletapi | 2.3 | Servlet 标签库依赖 | 可选(CLI 场景不用) |
| xml-apis | 1.0.b2 | 老 XML API | 可选(JDK 17 内置 JAXP 可替代) |
| xerces | 2.2.1 | SAX 解析器实现 | 建议引入(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 < 2.0
</j:otherwise>
</j:choose>
<j:set var="n" value="${0}"/>
<j:while test="${n < 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 < 2.0、n < 3处用了 XML 实体<——.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/ 下用相对/绝对路径均可。
八、参考链接
- Maven Central(jar 与 POM):https://repo1.maven.org/maven2/commons-jelly/commons-jelly/1.0.1/
- 官网 Getting Started / Tutorial:https://commons.apache.org/proper/commons-jelly/
- 选型与停滞现状分析:《Apache Jelly 介绍与选型指南》
- 命令行与 Ant 集成逐参数实测:《Apache Jelly 常用 CLI 命令与工具详解》