Ubuntu 22.04 安装 Apache Commons JCI 指南

在 Ubuntu 22.04 上从零安装 Apache Commons JCI 1.1,覆盖 JDK 17 与 Maven 环境、Maven 依赖与 jar 直用两种方案、eclipse 与 janino 编译器适配的最小验证、javac 适配器在 JDK 17 不可用的实测结论与卸载清理,命令回显均实测通过。

Apache Commons JCIApache Commons JCI2026/10/15

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 是库而非服务,环境中不存在需要常驻的守护进程;「安装完成」的判定标准是可编译并运行一个调用其 JavaCompiler API 的最小程序,而不是某个端口处于监听状态。

二、前置条件

本节说明安装前必须满足的条件。Ubuntu 22.04 自带可用的软件源,联网即可通过 apt 获取 JDK 与 Maven。

  1. 具有 root 或 sudo 权限的 Ubuntu 22.04 系统。
  2. 可访问 Ubuntu 官方软件源(apt update 能正常拉取索引)。
  3. 可访问 Maven 中央仓库 repo1.maven.org(JCI 没有独立的官方二进制下载包,全部以 Maven 中央仓库为分发渠道,本文实测网络直连可达)。
  4. 至少 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-core1.12013-10-09commons-io 2.2、commons-logging 1.1.32013 年后无更新
commons-jci-eclipse1.12013-10-09org.eclipse.jdt.core.compiler:ecj 4.2.22013 年后无更新
commons-jci-janino1.12013-10-09org.codehaus.janino:janino 2.6.12013 年后无更新
commons-jci-javac1.02010 年org.vafer:dependency 0.2(含 junit 3.8.2、asm 2.2.x)2010 年后无更新
commons-jci-groovy1.12013-10-09groovy-all 1.7.112013 年后无更新
commons-jci-fam1.12013-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/groovy2013 年停更,接口稳定
javax.tools(JDK 6+)JDK 原生编译 APIjavac(可挂 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 产出 jarjavac 产出 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

十、参考链接

  1. Apache Commons JCI 官方主页:https://commons.apache.org/proper/commons-jci/
  2. Maven 中央仓库 commons-jci-core 坐标:https://repo1.maven.org/maven2/org/apache/commons/commons-jci-core/
  3. Maven 中央仓库 commons-jci-eclipse 坐标:https://repo1.maven.org/maven2/org/apache/commons/commons-jci-eclipse/
  4. Maven 中央仓库 ecj 4.2.2 坐标:https://repo1.maven.org/maven2/org/eclipse/jdt/core/compiler/ecj/
  5. Janino 项目主页(适配器后端 2.6.1):https://janino-compiler.github.io/janino/
  6. OpenJDK 17 安装包(Ubuntu 官方源):apt-cache show openjdk-17-jdk