在 CentOS 上实现 Java 热编译的可行路径
一 概念澄清与适用场景
二 方案一 运行时编译 + 自定义类加载器(通用 Java 程序)
~/hotcompile
├── src
│ └── com
│ └── example
│ └── Hello.java
└── classes
package com.example;
public class Hello {
public String say() { return "Hello, CentOS hot compile at " + System.currentTimeMillis(); }
}
#!/usr/bin/env bash
set -e
JAVA_HOME=/usr/lib/jvm/java-11-openjdk # 按实际路径调整
SRC_DIR=src
OUT_DIR=classes
mkdir -p "$OUT_DIR"
"$JAVA_HOME/bin/javac" -d "$OUT_DIR" -cp "$OUT_DIR" "$SRC_DIR/com/example/Hello.java"
#!/usr/bin/env bash
# 需引入:tools.jar(JDK 8)或 jdk.compiler 模块(JDK 9+)
# 例如:JDK 8 启动参数:-cp "$OUT_DIR:$JAVA_HOME/lib/tools.jar"
# JDK 11+ 启动参数(若使用模块化,需 --add-modules jdk.compiler)
JAVA_HOME=/usr/lib/jvm/java-11-openjdk
OUT_DIR=classes
MAIN_CLASS=com.example.HelloRunner # 见下方 Java 代码
"$JAVA_HOME/bin/java" -cp "$OUT_DIR" "$MAIN_CLASS"
package com.example;
import javax.tools.*;
import java.io.*;
import java.lang.reflect.Method;
import java.net.URI;
import java.nio.file.*;
import java.util.Collections;
public class HelloRunner {
private static final Path SRC_DIR = Paths.get("src");
private static final Path OUT_DIR = Paths.get("classes");
private static final String CLASS_NAME = "com.example.Hello";
private static volatile Class<?> cachedClass = null;
private static volatile Object instance = null;
public static void main(String[] args) throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) throw new IllegalStateException("需使用 JDK 运行(JRE 无编译器)");
StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null);
try {
while (true) {
// 1) 监听 .java 变更(简化:每次循环都尝试编译)
Path src = SRC_DIR.resolve("com/example/Hello.java");
if (!Files.exists(src)) { Thread.sleep(1000); continue; }
// 2) 编译
JavaFileObject srcFile = fm.getJavaFileObjects(src.toFile()).iterator().next();
JavaCompiler.CompilationTask task = compiler.getTask(
null, fm, null,
new String[]{"-d", OUT_DIR.toString()}, // 输出目录
null,
Collections.singletonList(srcFile)
);
boolean ok = task.call();
if (!ok) { Thread.sleep(1000); continue; }
// 3) 仅当 .class 更新时才重新加载(避免无谓 redefine)
Path cls = OUT_DIR.resolve("com/example/Hello.class");
long lastModified = Files.getLastModifiedTime(cls).toMillis();
if (cachedClass != null) {
long prev = (Long) cachedClass.getDeclaredField("LOADED_AT").get(null);
if (lastModified <= prev) { Thread.sleep(500); continue; }
}
// 4) 自定义 ClassLoader 隔离并加载新类
URLClassLoader cl = new URLClassLoader(
new URL[]{OUT_DIR.toUri().toURL()},
HelloRunner.class.getClassLoader()
) {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.equals(CLASS_NAME)) {
// 打破双亲委派,优先加载新版本
Class<?> c = findLoadedClass(name);
if (c == null) c = findClass(name);
if (resolve) resolveClass(c);
return c;
}
return super.loadClass(name, resolve);
}
};
Class<?> newCls = cl.loadClass(CLASS_NAME);
// 触发类初始化,记录加载时间(字段仅用于演示)
newCls.getDeclaredField("LOADED_AT").set(null, System.currentTimeMillis());
// 5) 替换旧实例并调用
Object newInst = newCls.getDeclaredConstructor().newInstance();
Method m = newCls.getMethod("say");
System.out.println(">>> " + m.invoke(newInst));
// 6) 释放旧引用,避免 Metaspace 泄漏
instance = newInst;
cachedClass = newCls;
cl.close();
Thread.sleep(1000);
}
} finally {
fm.close();
}
}
}
三 方案二 文件监听 + 自动编译 + 热加载(工程化增强)
四 开发期框架与 IDE 的热部署工具
五 常见坑与最佳实践