Ubuntu 22.04 安装 Apache Commons JCI 指南
在 Ubuntu 22.04 上从零安装 Apache Commons JCI 1.1,覆盖 JDK 17 与 Maven 环境、Maven 依赖与 jar 直用两种方案、eclipse 与 janino 编译器适配的最小验证、javac 适配器在 JDK 17 不可用的实测结论与卸载清理,命令回显均实测通过。
Ubuntu 22.04 安装 Apache Commons JCI 指南
目标:在 Ubuntu 22.04 上从零搭建 Apache Commons JCI 1.1(Java 编译器接口库)的开发与运行环境(JDK 17 + Maven 3.6.3),跑通 eclipse 与 janino 两种编译器适配的「源码字符串进、字节码出、类加载执行」完整闭环,并如实记录 javac 适配器在 JDK 17 下不可用的实测结论;本文全部命令与回显均已在 Ubuntu 22.04 实测通过。
Apache Commons JCI(Java Compiler Interface)是 Apache Commons 项目下的编译器门面库,把 Eclipse JDT(ecj)、Janino、javac、Groovy 等多种编译器统一成一套 JavaCompiler API,配合内存读写器与存储器实现「不落盘的内存编译」,并为热更新场景提供文件监听与类重载组件。它是纯 Java 库,没有常驻进程,安装的本质就是「装好 JDK/构建工具 → 引入依赖 → 写最小验证代码 → 编译运行」。本文给出两种落地方式:面向开发的 Maven 依赖方案,以及面向无构建体系场景的中央仓库 jar 直用方案。
一、环境信息
本节先固定可复现的基线环境,后续所有命令均在此环境执行。列出操作系统、架构、JDK、构建工具与组件版本,便于读者对照复现。
- 操作系统:Ubuntu 22.04.5 LTS(PRETTY_NAME="Ubuntu 22.04.5 LTS")
- 架构:x86_64
- JDK:OpenJDK 17.0.20.1(apt 包
openjdk-17-jdk) - 构建工具:Apache Maven 3.6.3
- 组件版本:commons-jci-core 1.1(本文主线,另有 eclipse 1.1、janino 1.1、javac 1.0、groovy 1.1、fam 1.1 子模块)
- Maven 坐标:
org.apache.commons:commons-jci-core:1.1 - 验证用工作目录:
/opt/jci-demo(Maven 工程)、/opt/jci-lib与/opt/jci-app(jar 直用)
系统与架构确认(实时回显):
cat /etc/os-release | head -3
PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
uname -m
x86_64
💡 说明:JCI 是库而非服务,环境中不存在需要常驻的守护进程;「安装完成」的判定标准是可编译并运行一个调用其
JavaCompilerAPI 的最小程序,而不是某个端口处于监听状态。
二、前置条件
本节说明安装前必须满足的条件。Ubuntu 22.04 自带可用的软件源,联网即可通过 apt 获取 JDK 与 Maven。
- 具有 root 或 sudo 权限的 Ubuntu 22.04 系统。
- 可访问 Ubuntu 官方软件源(
apt update能正常拉取索引)。 - 可访问 Maven 中央仓库
repo1.maven.org(JCI 没有独立的官方二进制下载包,全部以 Maven 中央仓库为分发渠道,本文实测网络直连可达)。 - 至少 2 GB 可用磁盘空间(JDK + Maven + 依赖合计约 500 MB)。
中央仓库连通性确认(实时回显):
curl -sI --max-time 20 https://repo1.maven.org/maven2/org/apache/commons/commons-jci-core/1.1/commons-jci-core-1.1.jar | head -1
HTTP/2 200
三、版本选型说明
本节给出 JCI 的版本谱系与维护状态,帮助读者建立正确的预期。JCI 是老项目:最后一个大版本 1.1 发布于 2013 年,此后约 13 年无更新,但它仍是 Maven 中央仓库上可用的 latest/release 版本。以下版本事实全部来自 Maven Central metadata 与 jar 响应头的实时查询。
最新版本确认(实时回显):
curl -s --max-time 30 https://repo1.maven.org/maven2/org/apache/commons/commons-jci-core/maven-metadata.xml
<?xml version="1.0" encoding="UTF-8"?>
<metadata>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jci-core</artifactId>
<versioning>
<latest>1.1</latest>
<release>1.1</release>
<versions>
<version>1.0</version>
<version>1.1</version>
</versions>
<lastUpdated>20131014122043</lastUpdated>
</versioning>
</metadata>
jar 发布时间与大小确认(实时回显):
curl -sI --max-time 30 https://repo1.maven.org/maven2/org/apache/commons/commons-jci-core/1.1/commons-jci-core-1.1.jar | grep -iE 'last-modified|content-length'
content-length: 32404
last-modified: Wed, 09 Oct 2013 14:24:14 GMT
各子模块的版本谱系(同一实时查询方式,逐个访问各自 metadata 与 jar 响应头汇总):
| 模块 | 最新版 | jar 发布时间(GMT) | 传递依赖(实测) | 维护状态 |
|---|---|---|---|---|
| commons-jci-core | 1.1 | 2013-10-09 | commons-io 2.2、commons-logging 1.1.3 | 2013 年后无更新 |
| commons-jci-eclipse | 1.1 | 2013-10-09 | org.eclipse.jdt.core.compiler:ecj 4.2.2 | 2013 年后无更新 |
| commons-jci-janino | 1.1 | 2013-10-09 | org.codehaus.janino:janino 2.6.1 | 2013 年后无更新 |
| commons-jci-javac | 1.0 | 2010 年 | org.vafer:dependency 0.2(含 junit 3.8.2、asm 2.2.x) | 2010 年后无更新 |
| commons-jci-groovy | 1.1 | 2013-10-09 | groovy-all 1.7.11 | 2013 年后无更新 |
| commons-jci-fam | 1.1 | 2013-10-09 | 无(零传递依赖) | 2013 年后无更新 |
关键结论与选型:JCI 1.1 是最终版本,官方已停止演进,选它不是因为它新,而是因为它把「多编译器统一 API + 内存编译 + 热更新监听」封装成一套稳定接口,且 jar 总量小(core 仅 32 KB)。本文实际采用 1.1,eclipse 适配器(ecj 4.2.2)与 janino 适配器(janino 2.6.1)在 JDK 17 上实测可用;javac 适配器(1.0,2010 年)在 JDK 9 及以上不可用(实测结论与替代方案见第七章与 FAQ),groovy 适配器在 JDK 17 需额外 JVM 参数。
💡 技术拓展:「统一编译器接口」这条路线不止 JCI 一家,常见方案对比如下——
方案 定位 编译器后端 维护状态 Commons JCI 老牌门面库 ecj/janino/javac/groovy 2013 年停更,接口稳定 javax.tools(JDK 6+) JDK 原生编译 API javac(可挂 ecj) 随 JDK 持续演进 ECJ 直用 Eclipse 编译器本体 ecj 随 Eclipse 持续演进 Janino 直用 轻量快速编译器 janino 随 Janino 持续演进 本文采用 Commons JCI 1.1,理由是其
MemoryResourceReader+MemoryResourceStore+JavaCompiler三件套把「内存编译」压缩到十几行代码,且自带FilesystemAlterationMonitor、CompilingListener、ReloadingClassLoader热更新组件;对 JCI 未覆盖的 javax.tools 细节(如注解处理器链),再按需下沉到原生 API。
四、方案 A:Maven 依赖安装(在线,推荐)
本节面向常规 Java 项目开发,通过 apt 安装 JDK 与 Maven,再用 Maven 坐标引入 JCI 依赖并跑通最小验证程序。这是最贴合工程实践的方式。
4.1 方案说明
方案 A 的链路是:apt 安装 openjdk-17-jdk 与 maven → 在 pom.xml 中声明 commons-jci-core:1.1、commons-jci-eclipse:1.1、commons-jci-janino:1.1 → Maven 自动从中央仓库下载 jar 到本地仓库 → 编译并运行验证程序。javac 与 groovy 两个适配模块不引入(原因见 4.4 节)。优点是版本由 pom 统一管控、依赖自动解析;缺点是首次构建需要联网。
4.2 安装 JDK 17
Ubuntu 22.04 官方源内置 openjdk-17-jdk,直接安装即可:
apt-get update -qq && apt-get install -y openjdk-17-jdk
# 输出节选(实测首尾)
openjdk-17-jdk is already the newest version (17.0.20.1+1-1~22.04).
0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.
安装后确认版本:
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)
验证点:java -version 能输出 17.0.x 即表示 JDK 就绪;若仍指向旧版本,用 update-alternatives --config java 选择 17。
4.3 安装 Maven
apt-get install -y maven
# 输出节选(实测首尾)
maven is already the newest version (3.6.3-5).
0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.
确认版本与 Maven 实际使用的 JDK:
mvn -version
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
验证点:Java version 一行显示 17.0.20.1,说明 Maven 已绑定到 JDK 17,无需额外设置 JAVA_HOME。
4.4 创建工程并引入依赖
建立一个标准 Maven 工程,pom.xml 中声明 JCI 依赖。刻意不引入 commons-jci-javac:它在 JDK 9 及以上不可用(见第七章实测),引入后其传递依赖 org.vafer:dependency 0.2 还会把 compile 作用域的 junit 3.8.2 与 asm 2.2.x 一并拖入依赖树,徒增体积;groovy 模块同理按需引入。
mkdir -p /opt/jci-demo/src/main/java
cd /opt/jci-demo
pom.xml(完整内容,文件路径 /opt/jci-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>jci-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jci-core</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jci-eclipse</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jci-janino</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-jci-fam</artifactId>
<version>1.1</version>
</dependency>
</dependencies>
</project>
逐项说明关键配置:
commons-jci-core:1.1:门面核心,含JavaCompilerFactory、读写器与存储器,自动带入 commons-io 2.2 与 commons-logging 1.1.3。commons-jci-eclipse:1.1:Eclipse 编译器适配,自动带入 ecj 4.2.2(约 1.8 MB,是全部依赖里最大的 jar)。commons-jci-janino:1.1:Janino 编译器适配,自动带入 janino 2.6.1 与 commons-compiler 2.6.1。commons-jci-fam:1.1:文件变更监听模块(零传递依赖),做热更新时必需。maven.compiler.source/target=8:宿主工程按 Java 8 语法编译,JDK 17 可执行;demo 代码避免使用 JDK 8 之后的新语法,便于在 JDK 8 机器上复用。
4.5 依赖解析验证
解析依赖树,确认 JCI 各模块的真实传递依赖与版本(实时回显,sed 截取依赖树主体,直接执行 mvn -B dependency:tree 时以下行逐行出现在完整日志中):
cd /opt/jci-demo && mvn -B dependency:tree 2>&1 | sed -n '/demo:jci-probe/,/BUILD SUCCESS/p'
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ jci-probe ---
[INFO] demo:jci-probe:jar:1.0.0
[INFO] +- org.apache.commons:commons-jci-core:jar:1.1:compile
[INFO] | +- commons-io:commons-io:jar:2.2:compile
[INFO] | \- commons-logging:commons-logging:jar:1.1.3:compile
[INFO] +- org.apache.commons:commons-jci-eclipse:jar:1.1:compile
[INFO] | \- org.eclipse.jdt.core.compiler:ecj:jar:4.2.2:compile
[INFO] +- org.apache.commons:commons-jci-janino:jar:1.1:compile
[INFO] | \- org.codehaus.janino:janino:jar:2.6.1:compile
[INFO] | \- org.codehaus.janino:commons-compiler:jar:2.6.1:compile
[INFO] +- org.apache.commons:commons-jci-fam:jar:1.1:compile
验证点:树中出现 commons-jci-core:1.1、ecj:4.2.2、janino:2.6.1 三个关键节点即依赖解析成功;若提示 Could not resolve dependencies,检查网络可达 repo.maven.apache.org。
4.6 最小验证代码
验证程序 Probe2.java(文件路径 /opt/jci-demo/src/main/java/Probe2.java)对 eclipse 与 janino 两个适配器分别执行「读入内存源码 → 编译 → 取字节码 → 自定义 ClassLoader 加载 → 反射调用」的完整闭环,并实测版本上限行为:
import org.apache.commons.jci.compilers.CompilationProblem;
import org.apache.commons.jci.compilers.CompilationResult;
import org.apache.commons.jci.compilers.JavaCompiler;
import org.apache.commons.jci.compilers.JavaCompilerFactory;
import org.apache.commons.jci.compilers.JavaCompilerSettings;
import org.apache.commons.jci.readers.MemoryResourceReader;
import org.apache.commons.jci.stores.MemoryResourceStore;
import java.lang.reflect.Method;
public class Probe2 {
static final String SRC = "public class Hello { public String say() { return \"hello-\" + Math.max(1, 2); } }\n";
public static void main(String[] args) throws Exception {
run("eclipse", "1.6", "1.6");
run("eclipse", "1.8", "1.8");
run("janino", null, null);
}
static void run(String name, String source, String target) throws Exception {
System.out.println("---- [" + name + " source=" + source + " target=" + target + "] ----");
try {
JavaCompiler compiler = JavaCompilerFactory.getInstance().createCompiler(name);
JavaCompilerSettings settings = compiler.createDefaultSettings();
if (source != null) {
settings.setSourceVersion(source);
settings.setTargetVersion(target);
}
MemoryResourceReader reader = new MemoryResourceReader();
reader.add("Hello.java", SRC.getBytes("UTF-8"));
MemoryResourceStore store = new MemoryResourceStore();
CompilationResult result = compiler.compile(
new String[]{"Hello.java"}, reader, store, Probe2.class.getClassLoader(), settings);
System.out.println("errors=" + result.getErrors().length + " warnings=" + result.getWarnings().length);
CompilationProblem[] errs = result.getErrors();
int i = 0;
do {
if (i < errs.length) {
System.out.println(" err: " + errs[i].getMessage());
}
i++;
} while (i < errs.length);
byte[] bytes = store.read("Hello.class");
System.out.println("bytecode bytes=" + (bytes == null ? -1 : bytes.length));
if (bytes != null) {
Class<?> cls = new ByteLoader(Probe2.class.getClassLoader()).define(bytes);
Object hello = cls.getDeclaredConstructor().newInstance();
Method say = cls.getMethod("say");
System.out.println("invoke say() = " + say.invoke(hello));
}
} catch (Throwable t) {
System.out.println("FAILED: " + t.getClass().getName() + ": " + String.valueOf(t.getMessage()));
}
}
static class ByteLoader extends ClassLoader {
ByteLoader(ClassLoader parent) { super(parent); }
Class<?> define(byte[] bytes) {
return defineClass("Hello", bytes, 0, bytes.length);
}
}
}
4.7 编译与运行验证
编译并运行(依赖 jar 已在本地仓库,classpath 由 lib/* 提供,先用 copy-dependencies 归集):
cd /opt/jci-demo && mvn -B -q clean compile && echo "EXIT=$?"
mvn -B -q dependency:copy-dependencies -DoutputDirectory=/opt/jci-demo/lib
EXIT=0
java -Dfile.encoding=UTF-8 -cp "target/classes:lib/*" Probe2
---- [eclipse source=1.6 target=1.6] ----
errors=0 warnings=0
bytecode bytes=536
invoke say() = hello-2
---- [eclipse source=1.8 target=1.8] ----
FAILED: java.lang.RuntimeException: unknown version 1.8
---- [janino source=null target=null] ----
errors=0 warnings=0
bytecode bytes=371
invoke say() = hello-2
结果解读:三组演示两组成功——eclipse 适配器在 source/target=1.6 下完成内存编译(536 字节产物)并成功加载调用(hello-2,即 Math.max(1, 2) 的结果);janino 适配器零配置开箱即用(371 字节产物)。eclipse 设 1.8 抛 unknown version 1.8 是 ecj 4.2.2(2012 年产物)的版本上限所致,属预期行为,处理方式见 FAQ Q2。这表示 JCI 1.1 已在 Ubuntu 22.04 + JDK 17 上完整可用。
五、方案 B:中央仓库 jar 直用(离线友好)
本节面向没有 Maven/构建体系的场景,直接从 Maven 中央仓库下载 jar,用 javac + java 完成编译运行,不依赖任何构建工具。
5.1 方案说明
链路是:确定 eclipse 适配器可用的最小 jar 集 → 从 repo1.maven.org 下载 5 个 jar 与 sha1 → 校验 → 用 javac -cp 编译源码 → 用 java -cp 运行。适用于离线环境、脚本化部署或纯粹想「拿到 jar 就能用」的读者。最小 jar 集来自 4.5 节依赖树:core + eclipse + ecj + core 的两个传递依赖(commons-io、commons-logging)。
5.2 下载与完整性校验
mkdir -p /opt/jci-lib /opt/jci-app && cd /opt/jci-lib
B=https://repo1.maven.org/maven2
curl -sO $B/org/apache/commons/commons-jci-core/1.1/commons-jci-core-1.1.jar
curl -sO $B/org/apache/commons/commons-jci-eclipse/1.1/commons-jci-eclipse-1.1.jar
curl -sO $B/org/eclipse/jdt/core/compiler/ecj/4.2.2/ecj-4.2.2.jar
curl -sO $B/commons-io/commons-io/2.2/commons-io-2.2.jar
curl -sO $B/commons-logging/commons-logging/1.1.3/commons-logging-1.1.3.jar
ls -l | awk '{print $5, $9}'
173587 commons-io-2.2.jar
32404 commons-jci-core-1.1.jar
20459 commons-jci-eclipse-1.1.jar
62050 commons-logging-1.1.3.jar
1801636 ecj-4.2.2.jar
逐 jar 校验 sha1(期望值取自中央仓库同名 .sha1 文件,实测值来自本地计算,两者必须逐字一致):
cd /opt/jci-lib
B=https://repo1.maven.org/maven2
curl -s $B/org/apache/commons/commons-jci-core/1.1/commons-jci-core-1.1.jar.sha1 | xargs echo "core 期望:"
sha1sum commons-jci-core-1.1.jar | cut -d' ' -f1 | xargs echo "core 实测:"
curl -s $B/org/eclipse/jdt/core/compiler/ecj/4.2.2/ecj-4.2.2.jar.sha1 | xargs echo "ecj 期望:"
sha1sum ecj-4.2.2.jar | cut -d' ' -f1 | xargs echo "ecj 实测:"
core 期望: 765f587c613cc59459c1811d9e85271fc722af55
core 实测: 765f587c613cc59459c1811d9e85271fc722af55
ecj 期望: d9fd79499c664bccaf24dbe04be417de6c6af64a
ecj 实测: d9fd79499c664bccaf24dbe04be417de6c6af64a
⚠️ 注意:若 sha1 不一致,说明下载包损坏或被篡改,须删除后重新下载;切勿跳过校验直接使用。
5.3 javac 编译与 java 运行
验证程序 Demo.java(文件路径 /opt/jci-app/Demo.java):
import org.apache.commons.jci.compilers.CompilationResult;
import org.apache.commons.jci.compilers.JavaCompiler;
import org.apache.commons.jci.compilers.JavaCompilerFactory;
import org.apache.commons.jci.compilers.JavaCompilerSettings;
import org.apache.commons.jci.readers.MemoryResourceReader;
import org.apache.commons.jci.stores.MemoryResourceStore;
public class Demo {
public static void main(String[] args) throws Exception {
JavaCompiler compiler = new JavaCompilerFactory().createCompiler("eclipse");
JavaCompilerSettings settings = compiler.createDefaultSettings();
settings.setSourceVersion("1.6");
settings.setTargetVersion("1.6");
MemoryResourceReader reader = new MemoryResourceReader();
String src = "public class Greet { public String hi() { return \"jci-ok-\" + Integer.MAX_VALUE; } }";
reader.add("Greet.java", src.getBytes("UTF-8"));
MemoryResourceStore store = new MemoryResourceStore();
CompilationResult result = compiler.compile(
new String[]{"Greet.java"}, reader, store, Demo.class.getClassLoader(), settings);
System.out.println("errors=" + result.getErrors().length);
byte[] bytes = store.read("Greet.class");
System.out.println("bytecode bytes=" + bytes.length);
Class<?> cls = new Sub(Demo.class.getClassLoader()).define(bytes);
System.out.println(cls.getMethod("hi").invoke(cls.getDeclaredConstructor().newInstance()));
}
static class Sub extends ClassLoader {
Sub(ClassLoader p) { super(p); }
Class<?> define(byte[] b) { return defineClass("Greet", b, 0, b.length); }
}
}
编译与运行:
cd /opt/jci-app
javac -encoding UTF-8 -cp "/opt/jci-lib/*" -d classes Demo.java
java -Dfile.encoding=UTF-8 -cp "classes:/opt/jci-lib/*" Demo
errors=0
bytecode bytes=350
jci-ok-2147483647
验证点:无 Maven、无 ~/.m2,仅靠 5 个 jar 总计约 2 MB 完成内存编译闭环,jci-ok-2147483647 即动态编译出的 Greet.hi() 返回 Integer.MAX_VALUE。出现 NoClassDefFoundError 时按第 5.1 节 jar 集对照补齐(常见是漏掉 ecj 或 commons-io)。
5.4 JVM 内存参数位置与修改方法
JVM 参数直接加在 java 命令后、主类名之前。JCI 内存编译的字节码与源码都驻留堆内,小机器建议限定堆避免抢占:
java -Xms256m -Xmx256m -Dfile.encoding=UTF-8 -cp "classes:/opt/jci-lib/*" Demo
errors=0
bytecode bytes=350
jci-ok-2147483647
修改方法:-Xms 调初始堆、-Xmx 调最大堆(建议 256–512 MB 起步,如 -Xms256m -Xmx512m);需要持久生效时写入启动脚本或 systemd 服务的 Environment=JAVA_OPTS=-Xms256m -Xmx256m 一行,再由脚本 java $JAVA_OPTS -cp "classes:/opt/jci-lib/*" Demo 引用。Maven 方式同理,在 MAVEN_OPTS 中设置:export MAVEN_OPTS=-Xms256m -Xmx256m。
六、方案对比与选择建议
本节汇总两种方案的差异,给出选型建议。两种方案产出同一份 JCI 1.1 代码,差别仅在获取与装配方式。
| 维度 | 方案 A:Maven 依赖 | 方案 B:中央仓库 jar 直用 |
|---|---|---|
| 运行期依赖 | Maven 自动解析 | 手工下载 5 个 jar 拼 classpath |
| 是否联网 | 首次构建需联网 | 仅下载时需要联网 |
| 依赖版本管控 | pom 统一声明,可锁版本 | 靠文件名区分,手工对齐 |
| 多依赖协同(传递依赖) | 自动处理 | 需按依赖树手工收集 |
| 可复现产物 | mvn package 产出 jar | javac 产出 class + 引用 jar |
| 适用场景 | 常规工程开发 | 离线脚本、临时验证、无构建体系 |
选型建议:若项目有构建体系(Maven/Gradle),一律选方案 A,版本、传递依赖与打包全部自动化;若在无构建工具的老旧服务器上临时验证,或需要把 jar 手工嵌入既有脚本,选方案 B。两者也可共存——用方案 A 开发、用方案 B 分发 jar。
七、基础配置说明
本节说明 JCI 使用中真正需要设置的「配置」。它是库而非服务,没有独立配置文件,全部行为通过代码参数或 JDK 环境变量控制。
环境变量 JAVA_HOME(Maven 依赖它定位 JDK)。默认会话中该变量为空(实测回显):
echo "JAVA_HOME=$JAVA_HOME"
JAVA_HOME=
此时 Maven 仍能正常工作,是因为 apt 版 Maven 通过 PATH 中的 java(由 update-alternatives 指向 JDK 17,见 4.3 节 mvn -version 回显的 runtime 一行)完成 JDK 定位,不依赖 JAVA_HOME。若手工安装 JDK 或需要显式固定 JDK 路径,可显式导出(实测回显):
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 && echo "JAVA_HOME=$JAVA_HOME"
JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
逐项说明:
JAVA_HOME:Maven 定位 JDK 的优先来源。Ubuntu 官方源的 Maven 自带兼容逻辑,JAVA_HOME为空时按PATH中的java定位,实测无需手工设置即可构建;仅当手工安装 JDK 或 Maven 报「找不到 JDK」时,再按上面的方式显式导出该变量。- 代码级「参数」:编译器版本由
JavaCompilerSettings控制(setSourceVersion/setTargetVersion/setSourceEncoding),ecj 4.2.2 的合法取值区间是 1.4 至 1.7,设 1.8 实测抛RuntimeException: unknown version 1.8;janino 2.6.1 不感知版本设置。运行堆参数见 5.4 节。各参数语义的完整清单见本专栏《Apache Commons JCI 1.1 编译器适配与 API 详解》。
八、常见问题(FAQ)
Q1:javac 适配器在 JDK 17 下能用吗?
不能,本文实测失败。把 commons-jci-javac:1.0 加入依赖并调用 createCompiler("javac") 后,编译返回错误 Error while executing the compiler: java.lang.reflect.InvocationTargetException。根因(反汇编与逐步复现实测):该适配器通过 JavacClassLoader 加载 com.sun.tools.javac.Main 并用 2006 年的 asm 2.2 做字节码转换(把文件读取改写为内存读取代理),而 JDK 17 的 javac 字节码带 NestMembers 属性(major 61),asm 2.2 解析报 ClassFormatError: Nest member class_info_index 129 has bad constant type in class file com/sun/tools/javac/main/Main,无 JVM 参数可救。替代方案:用 eclipse 适配器(本文主线)或直接使用 JDK 内置的 javax.tools.JavaCompiler。
Q2:eclipse 适配器设 source/target 1.8 报 unknown version 1.8 怎么办?
这是 ecj 4.2.2(2012 年、面向 JDK 7 时代)的版本上限:实测 1.4 至 1.7 均可编译,1.8 抛 RuntimeException: unknown version 1.8。解法两种:一是把目标代码按 1.7 语法编写(JCI 场景动态编译的代码通常很小,降语法代价低);二是升级依赖 org.eclipse.jdt.core.compiler:ecj 到新版(如 3.33.0),JCI 的 eclipse 适配接口未变,实测可配合新版 ecj 提升版本上限。
Q3:janino 适配器编译泛型代码报 is not an lvalue?
janino 2.6.1 是简化编译器,不支持泛型语法,实测对 java.util.List<String> list = new java.util.ArrayList<String>() 报 Expression "java.util.List < String > list" is not an lvalue。解法:喂给 janino 的源码按 Java 1.4 语法编写(裸类型 + 强转),或改用 eclipse 适配器。注意 janino 对 JavaCompilerSettings 里的 source/target 版本不敏感(设了也按自身规则编译),实测默认设置即可直接编译非泛型代码。
Q4:运行中文输出乱码(问号)?
默认 POSIX locale 下 JVM 会用 ASCII 输出中文。解决:运行时加 -Dfile.encoding=UTF-8(本文所有 java 命令已带),或把系统 locale 调成 UTF-8(export LANG=C.UTF-8)。
Q5:JCI 1.1 十三年没更新了,JDK 17 上跑有风险吗?
实测结论:核心链路(工厂、eclipse/janino 适配、内存读写器、文件监听、类重载)在 JDK 17 上稳定可用,风险集中在两点——javac 适配器不可用(见 Q1),groovy 适配器在 JDK 17 需要 --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED 才能完成编译产物实例化(JDK 9+ 模块强封装所致)。按本文 4.4 节的依赖组合规避即可。
九、卸载与清理
JCI 本身不产生系统级安装,卸载即移除依赖与本地文件。
移除本文创建的工程、jar 与工作目录:
rm -rf /opt/jci-demo /opt/jci-lib /opt/jci-app
ls /opt/jci-demo 2>&1
ls: cannot access '/opt/jci-demo': No such file or directory
移除 Maven 本地仓库中的 JCI 相关构件(影响所有工程,慎用):
rm -rf $HOME/.m2/repository/org/apache/commons/commons-jci-core
rm -rf $HOME/.m2/repository/org/apache/commons/commons-jci-eclipse
rm -rf $HOME/.m2/repository/org/apache/commons/commons-jci-janino
rm -rf $HOME/.m2/repository/org/apache/commons/commons-jci-fam
rm -rf $HOME/.m2/repository/org/eclipse/jdt/core/compiler/ecj/4.2.2
卸载 JDK 与 Maven(仅当不再需要)。先以 -s 预演确认将删除的包,无误后去掉 -s 真正执行:
apt-get -s purge -y openjdk-17-jdk maven
# 输出节选(实测首尾)
The following packages will be REMOVED:
maven* openjdk-17-jdk*
0 upgraded, 0 newly installed, 2 to remove and 1 not upgraded.
Purg maven [3.6.3-5]
Purg openjdk-17-jdk [17.0.20.1+1-1~22.04]
确认无误后真正卸载:
apt-get purge -y openjdk-17-jdk maven && apt-get autoremove -y
十、参考链接
- Apache Commons JCI 官方主页:https://commons.apache.org/proper/commons-jci/
- Maven 中央仓库 commons-jci-core 坐标:https://repo1.maven.org/maven2/org/apache/commons/commons-jci-core/
- Maven 中央仓库 commons-jci-eclipse 坐标:https://repo1.maven.org/maven2/org/apache/commons/commons-jci-eclipse/
- Maven 中央仓库 ecj 4.2.2 坐标:https://repo1.maven.org/maven2/org/eclipse/jdt/core/compiler/ecj/
- Janino 项目主页(适配器后端 2.6.1):https://janino-compiler.github.io/janino/
- OpenJDK 17 安装包(Ubuntu 官方源):
apt-cache show openjdk-17-jdk