动态编译 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 javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
/**
* @author moon
* @date 2023-02-15 20:00
* @since 1.8
*/
public class CustomJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
/**
* 自定义类加载器
*/
private CustomClassLoader loader;
/**
* 初始化
* @param fileManager
* @param loader
*/
protected CustomJavaFileManager(JavaFileManager fileManager, CustomClassLoader loader) {
super(fileManager);
this.loader = loader;
}
@Override
public JavaFileObject getJavaFileForOutput(
JavaFileManager.Location location, String className,
JavaFileObject.Kind kind, FileObject sibling) {
try {
CustomJavaFileObject innerClass = new CustomJavaFileObject(className);
loader.addJavaCode(innerClass);
return innerClass;
} catch (Exception e) {
throw new RuntimeException("exception when creating in-memory output stream for " + className, e);
}
}
@Override
public ClassLoader getClassLoader(JavaFileManager.Location location) {
return loader;
}
}
5.类加载器
用于从 CustomJavaFileObject 获取字节流,并通过 ClassLoader.defineClass 生成类
package org.example.demo.util;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author moon
* @date 2023-02-15 20:50
* @since 1.8
*/
public class CustomClassLoader extends ClassLoader{
/**
* 缓存源代码对象
*/
private Map<String, CustomJavaFileObject> fileCacheMap = new ConcurrentHashMap<>(16);
/**
* 初始化类加载器
* @param parent
*/
public CustomClassLoader(ClassLoader parent) {
super(parent);
}
/**
* 添加源码缓存
* @param obj
*/
public void addJavaCode(CustomJavaFileObject obj) {
fileCacheMap.put(obj.getName(), obj);
}
/**
* 获取类
* @param className
* The <a href="#binary-name">binary name</a> of the class
*
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
if (fileCacheMap.containsKey(className)){
byte[] byteCode = fileCacheMap.get(className).getByteCode();
return defineClass(className, byteCode, 0, byteCode.length);
} else {
return super.findClass(className);
}
}
}
6.类编译器
简要说明一下调用流程:读取源码 -> 编译 -> 加载为 Class => 构建对象及使用
package org.example.demo.util;
import javax.tools.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* 类编译器
*
* @author moon
* @date 2023-02-15 20:09
* @since 1.8
*/
public class CustomClassCompiler {
private JavaCompiler javac;
private CustomClassLoader classLoader;
private Iterable<String> options;
boolean ignoreWarnings = false;
/**
* 缓存待编译的源码
*/
private Map<String, CustomSourceCode> sourceCodes = new ConcurrentHashMap<>(16);
/**
* 缓存生成的类
*/
private Map<String, Class<?>> classMap = new ConcurrentHashMap<>(16);
/**
* 单例编译器
*/
private static volatile CustomClassCompiler compiler;
/**
* 获取实例
* @return
*/
public static CustomClassCompiler newInstance(ClassLoader parent) {
if ( null==compiler ){
synchronized (CustomClassCompiler.class){
if (null==compiler){
compiler = new CustomClassCompiler();
if (null!=parent){
compiler.useParentClassLoader(parent);
}
}
}
}
return compiler;
}
/**
* 默认类加载器 (私有化构造)
*/
private CustomClassCompiler() {
this.javac = ToolProvider.getSystemJavaCompiler();
this.classLoader = new CustomClassLoader(ClassLoader.getSystemClassLoader());
}
/**
* 使用父类加载器
* @param parent
* @return
*/
private CustomClassCompiler useParentClassLoader(ClassLoader parent) {
this.classLoader = new CustomClassLoader(parent);
return this;
}
/**
* @return the class loader used internally by the compiler
*/
public ClassLoader getClassloader() {
return classLoader;
}
/**
* Options used by the compiler, e.g. '-Xlint:unchecked'.
*
* @param options
* @return
*/
public void useOptions(String... options) {
this.options = Arrays.asList(options);
}
/**
* 忽略警告信息
*/
public void ignoreWarnings() {
ignoreWarnings = true;
}
/**
* 向编译器添加源码
* @param className
* @param sourceCode
* @return
* @throws Exception
*/
public CustomClassCompiler addSource(String className, String sourceCode) {
sourceCodes.put(className, new CustomSourceCode(className, sourceCode));
return this;
}
/**
* 编译源码
*
* @param classNames
* @return
*/
public boolean compile(String ... classNames) {
try {
compileByNames(Arrays.asList(classNames));
return true;
} catch (Exception e) {
System.out.println("Compile Exception:" + e.getMessage());
return false;
}
}
/**
* 获取类
* @param className
* @return
*/
public Class<?> getClassByName(String className){
return classMap.get(className);
}
/**
* 编译源码
*
* @return Map containing instances of all compiled classes
* @throws Exception
*/
private void compileByNames(List<String> classNames) throws Exception {
if (sourceCodes.size() == 0) {
throw new RuntimeException("No source code to compile");
}
//获取待编译类源码
Collection<CustomSourceCode> compilationUnits;
Set<String> keyTemp = null;
if (null != classNames && classNames.size() > 0){
compilationUnits = new ArrayList<>(classNames.size());
keyTemp = new HashSet<>(classNames.size());
for (String key:classNames){
if (sourceCodes.containsKey(key)){
keyTemp.add(key);
compilationUnits.add(sourceCodes.get(key));
}
}
} else {
keyTemp = sourceCodes.keySet();
compilationUnits = sourceCodes.values();
}
//检测源码是否全部存在
if (keyTemp.size() < classNames.size()){
throw new RuntimeException("Some source code not exist");
}
//定义警告和错误信息输出集合
DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
CustomJavaFileManager fileManager = new CustomJavaFileManager(javac.getStandardFileManager(null, null, StandardCharsets.UTF_8), classLoader);
JavaCompiler.CompilationTask task = javac.getTask(null, fileManager, collector, options, null, compilationUnits);
//编译
boolean result = task.call();
//编译结果
if (!result || collector.getDiagnostics().size() > 0) {
StringBuffer exceptionMsg = new StringBuffer();
exceptionMsg.append("Unable to compile the source");
boolean hasWarnings = false;
boolean hasErrors = false;
for (Diagnostic<? extends JavaFileObject> d : collector.getDiagnostics()) {
switch (d.getKind()) {
case NOTE:
case MANDATORY_WARNING:
case WARNING:
hasWarnings = true;
break;
case OTHER:
case ERROR:
hasErrors = true;
break;
default:
break;
}
exceptionMsg.append("\n").append("[kind=").append(d.getKind());
exceptionMsg.append(", ").append("line=").append(d.getLineNumber());
exceptionMsg.append(", ").append("message=").append(d.getMessage(Locale.US)).append("]");
}
//是否忽略警告
if ((hasWarnings && !ignoreWarnings ) || hasErrors) {
throw new RuntimeException(exceptionMsg.toString());
}
}
//遍历并缓存编译后的源码
for (String className : keyTemp) {
classMap.put(className, classLoader.loadClass(className));
}
}
}
三.动态编译测试
1.普通测试类
定义一个普通测试类,包含:有、无参构造初始化,有、无参方法调用
用户积分器
package org.example.demo.common;
public class UserSort {
private String name;
private int score;
public UserSort (){
}
public UserSort (String name, int sort){
this.name = name;
this.score = sort;
}
public void reset(){
this.score = 0;
System.out.println("姓名: " + this.name + " 积分重置: " + this.score);
}
public void insert(int score){
this.score += score;
System.out.println("姓名: " + this.name + " 加分结果: " + this.score);
}
public void reduce(int score){
this.score -= score;
System.out.println("姓名: " + this.name + " 减分结果: " + this.score);
}
}
封装一个静态方法:
public static void commonClass(CustomClassCompiler compiler) throws Exception{
String sourceCode = "package org.example.demo.common;\n" +
"\n" +
"public class UserSort {\n" +
"\n" +
" private String name;\n" +
" private int score;\n" +
"\n" +
" public UserSort (){\n" +
" }\n" +
"\n" +
" public UserSort (String name, int sort){\n" +
" this.name = name;\n" +
" this.score = sort;\n" +
" }\n" +
"\n" +
" public void reset(){\n" +
" this.score = 0;\n" +
" System.out.println(\"姓名: \" + this.name + \" 积分重置: \" + this.score);\n" +
" }\n" +
"\n" +
" public void insert(int score){\n" +
" this.score += score;\n" +
" System.out.println(\"姓名: \" + this.name + \" 加分结果: \" + this.score);\n" +
" }\n" +
"\n" +
" public void reduce(int score){\n" +
" this.score -= score;\n" +
" System.out.println(\"姓名: \" + this.name + \" 减分结果: \" + this.score);\n" +
" }\n" +
"}";
String className = "org.example.demo.common.UserSort";
compiler.addSource(className, sourceCode);
compiler.compile(className);
Class<?> clazz = compiler.getClassByName(className);
System.out.println("无参构造及重置分数-----------------------");
Object object = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getDeclaredMethod("reset");
method.invoke(object);
System.out.println("有参构造及重置分数-----------------------");
object = clazz.getDeclaredConstructor(String.class,int.class).newInstance("张三",0);
method.invoke(object);
System.out.println("加分-----------------------------------");
method = clazz.getDeclaredMethod("insert",int.class);
method.invoke(object,10);
System.out.println("减分-----------------------------------");
method = clazz.getDeclaredMethod("reduce",int.class);
method.invoke(object,2);
}
2.接口实现类
定义一个处理器接口,用于处理数据
package org.example.demo.handler;
/**
* @author moon
* @date 2023-02-15 20:55
* @since 1.8
*/
public interface BaseHandler {
/**
* 处理器
* @param content
*/
void deal(String content);
}
封装一个静态方法,实现类不再单独贴出,简单加了个打印,输出【春江花月夜】
public static void interfaceClass(CustomClassCompiler compiler) throws Exception {
String sourceCode = "package com.demo.handler;\n" +
"import org.example.demo.handler.BaseHandler;\n" +
"public class DynamicHandler implements BaseHandler {\n" +
" \n" +
" @Override\n" +
" public void deal(String content) {\n" +
" System.out.println(content);\n" +
" }\n" +
"}";
String className = "com.demo.handler.DynamicHandler";
compiler.addSource(className, sourceCode);
compiler.compile(className);
BaseHandler handler = (BaseHandler) compiler.getClassByName(className).getDeclaredConstructor().newInstance();
handler.deal("春江花月夜");
}
3.测试
在 App 类内直接定义一个 main 方法,调用上面两个静态方法
package org.example.demo;
import org.example.demo.handler.BaseHandler;
import org.example.demo.util.CustomClassCompiler;
import java.lang.reflect.Method;
/**
* @author moon
* @date 2023-02-15 20:42
* @since 1.8
*/
public class App {
public static void main(String[] args) throws Exception {
CustomClassCompiler compiler = CustomClassCompiler.newInstance(null);
commonClass(compiler);
System.out.println("\n--------------------------------------------------\n");
interfaceClass(compiler);
}
//TODO 静态方法 commonClass
//TODO 静态方法 interfaceClass
}
调用效果如下:
四.用动态编译 Class 替换 SpringBoot 的 Bean(未完)
未完待续 . . .