动态编译 Java 源码为 Class
一.背景
1.Jdk 版本
版本查看命令:java -version
2.需求
本来想看下项目热部署的实现,比如 SpringBoot 不停机热加载 Jar 实现功能修改;后来看到 Jdk 支持源码动态编译,如果可以实现,那么就可以在线直接修改代码,再利用 SpringBoot 管理起来,替换旧的 Bean,实现功能修改。可能实际应用场景不多,可以做应急修改,线上服务最终还是需要把修改后的代码重新部署更为稳妥。
其实动态修改代码还可以通过 Arthas 实现,包括反编译、编译等更多功能
二.Java 源码动态编译实现
源码编译需要用到的关键类:
类 | 说明 |
---|---|
JavaCompiler | 编译器 ToolProvider.getSystemJavaCompiler(); |
SimpleJavaFileObject | 文件对象类,可以表示源码、类文件 |
ClassLoader | 顶层类加载器,抽象类 |
ForwardingJavaFileManager | 文件管理器 |
项目结构如图
1.Maven 依赖
暂时只是一个 Maven 项目,未引入其他依赖
<?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>org.example</groupId>
<artifactId>DynamicDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>19</maven.compiler.source>
<maven.compiler.target>19</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
2.源码包装类
基于 SimpleJavaFileObject 扩展,用于封装类名、源码信息
package org.example.demo.util;
import javax.tools.SimpleJavaFileObject;
import java.io.IOException;
import java.net.URI;
/**
* @author moon
* @date 2023-02-15 20:32
* @since 1.8
*/
public class CustomSourceCode extends SimpleJavaFileObject {
/**
* 类名称
*/
private String className;
/**
* 类源码
*/
private String contents;
/**
* 源码初始化
* @param className
* @param contents
*/
public CustomSourceCode(String className, String contents) {
super(URI.create("string:///" + className.replace('.', '/')
+ Kind.SOURCE.extension), Kind.SOURCE);
this.contents = contents;
this.className = className;
}
/**
* 获取类名
* @return
*/
public String getClassName() {
return className;
}
/**
* 源码字符序列
* @param ignoreEncodingErrors ignore encoding errors if true
* @return
* @throws IOException
*/
public CharSequence getCharContent(boolean ignoreEncodingErrors)
throws IOException {
return contents;
}
}
3.Java 文件对象封装类
基于 SimpleJavaFileObject 实现,封装了类名、类字节输出流信息
package org.example.demo.util;
import javax.tools.SimpleJavaFileObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
/**
* @author moon
* @date 2023-02-15 20:52
* @since 1.8
*/
public class CustomJavaFileObject extends SimpleJavaFileObject {
/**
* 类名称
*/
private String className;
/**
* 输出的字节码流
*/
private ByteArrayOutputStream toByteArray = new ByteArrayOutputStream();
/**
* Construct a SimpleJavaFileObject of the given kind and with the
* given URI.
*
* @param className
*/
public CustomJavaFileObject(String className) throws URISyntaxException {
super(new URI(className), Kind.CLASS);
this.className = className;
}
@Override
public OutputStream openOutputStream() throws IOException {
return toByteArray;
}
/**
* 获取类名
* @return
*/
public String getClassName() {
return className;
}
/**
* 获取字节信息
* @return
*/
public byte[] getByteCode() {
return toByteArray.toByteArray();
}
}
4.文件管理器封装类
package org.example.demo.util;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import