在日常开发过程中,我们经常遇到临时开发一些额外的功能(需要在Test接口中手动调用),每次都必须重新提交代码,打包发布,无疑费时费力。
那么有什么方法可以跳过繁琐的打包过程呢?
答案是有的,Java 从6开始提供了动态编译API
Java Compiler
Java Compiler API,这是JDK6开始提供的标准API,提供了与javac对等的编译功能,即动态编译,文档地址
步骤
- 通过 Controller 接口,提交Java代码,代码统一实现Callable接口;
- 动态编译Java代码为字节码;
- 使用类加载器加载编译的字节码;
- 使用反射拿到类的元数据信息,执行call方法,完成对应的功能。
代码实现
public class ClassGenerator {
private String classRootDir;
public ClassGenerator() {
this(".");
}
public ClassGenerator(String classRootDir) {
this.classRootDir = classRootDir;
}
public Class<?> generate(String classFullName, String code, String jarFile) throws MalformedURLException, ClassNotFoundException {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
JavaFileObject fileObject = new JavaSourceFromString(classFullName, code);
File root = new File(classRootDir);
if (!root.exists()) {
root.mkdirs();
}
String jars = getJars(jarFile);
Iterable<String> options = Arrays.asList("-d", classRootDir, "-cp", jars + File.pathSeparator + classRootDir);
Iterable<? extends JavaFileObject> compilationUnits = Arrays.asList(fileObject);
JavaCompiler.CompilationTask task = compiler.getTask(null, null, null, options, null, compilationUnits);
// 动态编译
boolean success = task.call();
if (!success) {
return null;
}
SLogger.info("compile success root path = " + root.getPath());
URL[] urls = new URL[]{root.toURI().toURL()};
// 设置父类加载器
URLClassLoader classLoader = new URLClassLoader(urls, ClassGenerator.class.getClassLoader());
return Class.forName(classFullName, true, classLoader);
}
private String getJars(String jarFile) {
File file = new File(jarFile);
if (!file.exists()) {
return "";
}
StringBuilder builder = new StringBuilder();
for (File jar : Objects.requireNonNull(file.listFiles())) {
builder.append(jar.getPath()).append(File.pathSeparator);
}
return builder.toString();
}
}
复制代码
public class JavaSourceFromString extends SimpleJavaFileObject {
private String code;
JavaSourceFromString(String name, String code) {
super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
this.code = code;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}
复制代码
public class DynamicCompile {
public static Object compileInvoke(String code, String classFullName) throws Throwable {
String root = DynamicCompile.class.getClassLoader().getResource("").getPath();
String jarFile = root.replace("/classes", "/lib");
ClassGenerator builder = new ClassGenerator(root);
Class<?> cls = builder.generate(classFullName, code, jarFile);
Object instance = cls.newInstance();
if (!(instance instanceof Callable)) {
throw new RuntimeException("only support Callable");
}
MethodType methodType = MethodType.methodType(Object.class);
MethodHandle methodHandle = MethodHandles.lookup().findVirtual(cls, "call", methodType);
return methodHandle.invoke(instance);
}
}
复制代码
@RestController
@RequestMapping("/backend")
public class ExtendController {
@PostMapping("/extend")
public JSONResult executeByCode(@RequestParam("code") String code,
@RequestParam("class") String cls) {
if (StringUtils.isBlank(code)) {
return JSONResult.paramErrorResult("code is null");
}
if (StringUtils.isBlank(cls)) {
return JSONResult.paramErrorResult("class is null");
}
try {
Object invoke = DynamicCompile.compileInvoke(code, cls);
return JSONResult.okResult(invoke);
} catch (Exception e) {
SLogger.error(e.getMessage(), e);
return JSONResult.failureResult(e.getMessage());
}
}
/**
* Java 文件提交
* @param java
* @param cls
* @return
* @throws IOException
*/
@PostMapping("/extend/java")
public JSONResult executeByJava(MultipartFile java,
@RequestParam("class") String cls) throws IOException {
if (java == null) {
return JSONResult.paramErrorResult("java is null");
}
if (StringUtils.isBlank(cls)) {
return JSONResult.paramErrorResult("class is null");
}
byte[] bytes = java.getBytes();
String code = new String(bytes);
try {
Object invoke = DynamicCompile.compileInvoke(code, cls);
return JSONResult.okResult(invoke);
} catch (Exception e) {
SLogger.error(e.getMessage(), e);
return JSONResult.failureResult(e.getMessage());
}
}
}
复制代码
热更新
这样基本上可以实现我们的需求了,但是如果想要实现通过替换class文件来动态修复线上Bug,会发现并没有生效,执行的还是原来的结果,这是因为 Java 的类加载是存在缓存的,代码有删减:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 先检查缓存是否加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
// 未加载过委托父类加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
// 加载类信息
if (c == null) {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
复制代码
有什么办法可以跳过缓存呢?是否可以主动卸载类呢?Java 没有提供对应的API,只能通过自定义类加载器实现。
思路:通过 Map 缓存类的最新修改时间,每次加载的时候检查 class 文件的修改时间是否和缓存一致,不一样则重新加载。
类生命周期
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
- 该类所有的实例都已经被GC。
- 加载该类的ClassLoader实例已经被GC。
- 该类的java.lang.Class对象没有在任何地方被引用。
类加载机制
Java 类加载机制是“双亲委派”模型,即优先让父ClassLoader去加载。
Java 默认使用以下三种类加载器:
- 启动类加载器(Bootstrap ClassLoader):这个加载器是Java虚拟机实现的一部分,不是Java语言实现的,一般是C++实现的,它负责加载Java的基础类,主要是<JAVA_HOME>/lib/rt.jar,我们日常用的Java类库比如String、ArrayList等都位于该包内。
- 扩展类加载器(Extension ClassLoader):这个加载器的实现类是sun.misc.Laun-cher$ExtClassLoader,它负责加载Java的一些扩展类,一般是<JAVA_HOME>/lib/ext目录中的jar包。
- 应用程序类加载器(Application ClassLoader):这个加载器的实现类是sun.misc. Launcher$AppClassLoader,它负责加载应用程序的类,包括自己写的和引入的第三方法类库,即所有在类路径中指定的类。
这三个类加载器有一定的关系,可以认为是父子关系,Application ClassLoader的父亲是Extension ClassLoader, Extension的父亲是Bootstrap ClassLoader。注意不是父子继承关系,而是父子委派关系,子ClassLoader有一个变量parent指向父ClassLoader,在子Class-Loader加载类时,一般会首先通过父ClassLoader加载,具体来说,在加载一个类时,基本过程是:
- 判断是否已经加载过了,加载过了,直接返回Class对象,一个类只会被一个Class-Loader加载一次。
- 如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象。
- 在父ClassLoader没有加载成功的前提下,自己尝试加载类。
注意,不同类加载器加载同一类的得到的Class对象是不同的,Tomcat 通过 WebappClassLoader 隔离不同的web应用。
需要说明的是,Java 9引入了模块的概念。在模块化系统中,类加载的过程有一些变化,扩展类的目录被删除掉了,原来的扩展类加载器没有了,增加了一个平台类加载器(Platform Class Loader),角色类似于扩展类加载器,它分担了一部分启动类加载器的职责,另外,加载的顺序也有一些变化。
总结:
使用动态编译可以在不打包编译重启整个应用的情况下,实现功能的扩展,结合自定义类加载器,可以替换已加载过的类,实现线上Bug的修复,不过需要替换Tomcat 默认的类加载器 WebappClassLoader 此外不适合API这样的多节点,需要其他手段来保证代码提交到各个节点