最近在实现自定义代码进行动态编译并执行的功能,在实现过程中遇到的问题在此记录下,问题的解决方法仅供参考!
动态编译
-
构建类代码包括引入工具包等
private String fillClassHeaderAndTail(DefineCodeTemplate template) {
StringBuilder classHeader = new StringBuilder();
classHeader.append("package ").append("java代码默认包").append(";").append("\n");
// 引入java.util等工具包
classHeader.append("import java.util.*").append(";").append("\n");
classHeader.append("\n");
classHeader.append("public class ").append("java默认类名前缀").append(template.getUuid());
// 添加实现接口
classHeader.append(" implements ").append("java代码实现接口").append(" ");
classHeader.append("{").append("\n\n");
classHeader.append(template.getUserDefineCode()).append("\n");
classHeader.append("}");
return classHeader.toString();
}
-
获取全类名
String className = java代码默认包 + "." + java默认类名前缀 + 随机uuid;
-
编译代码
/**
* 存放编译之后的字节码(key:类全名,value:编译之后输出的字节码)
* 采用程序缓存,不依赖容器的热加载
*/
private volatile Map<String, ByteJavaFileObject> javaFileObjectMap = new ConcurrentHashMap<>();
/**
* 编译字符串源代码,编译失败在 diagnosticsCollector 中获取提示信息
*
* @return true:编译成功 false:编译失败
*/
private void compiler(String fullClassName, String sourceCode) {
try {
LOGGER.info("compiler-start, fullClassName={} code={}", fullClassName, sourceCode);
long startTime = System.currentTimeMillis();
// 获取java的编译器
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
// 存放编译过程中输出的信息
DiagnosticCollector<JavaFileObject> diagnosticsCollector = new DiagnosticCollector<>();
// 标准的内容管理器,更换成自己的实现,覆盖部分方法
StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnosticsCollector, null, null);
JavaFileManager javaFileManager = new StringJavaFileManage(standardFileManager);
// 构造源代码对象
JavaFileObject javaFileObject = new StringJavaFileObject(fullClassName, sourceCode);
String allJarNames = getRuntimeClassPath();
// 增加java compile可以编译第三方的的引用 --eg: import org.apache.commons.lang.StringUtils;
Iterable<String> options = Arrays.asList("-classpath", allJarNames);
LOGGER.info("compiler-options-jars {}", allJarNames);
//获取一个编译任务
JavaCompiler.CompilationTask task = compiler.getTask(null, javaFileManager, diagnosticsCollector, options, null, Collections.singletonList(javaFileObject));
//获取一个编译任务
boolean res = task.call();
//设置编译耗时
long compilerTakeTime = System.currentTimeMillis() - startTime;
LOGGER.info("compiler-finish, fullClassName={},res={},compilerTakeTime={}", fullClassName, res, compilerTakeTime);
if (!res) {
List<Diagnostic<? extends JavaFileObject>> diagnostics = diagnosticsCollector.getDiagnostics();
String compilerMessage = diagnostics.get(diagnostics.size() - 1).getMessage(Locale.getDefault());
LOGGER.error("compiler-fail,takeTime={}", diagnostics);
throw new RuntimeException("编译失败异常" + ":" + compilerMessage);
}
} catch (Exception e) {
LOGGER.error("compiler-Exception,code={}", sourceCode, e);
throw e;
}
}
/**
* 自定义一个JavaFileManage来控制编译之后字节码的输出位置
*/
private class StringJavaFileManage extends ForwardingJavaFileManager {
StringJavaFileManage(JavaFileManager fileManager) {
super(fileManager);
}
//获取输出的文件对象,它表示给定位置处指定类型的指定类。
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) throws IOException {
ByteJavaFileObject javaFileObject = new ByteJavaFileObject(className, kind);
javaFileObjectMap.put(className, javaFileObject);
return javaFileObject;
}
}
/**
* 自定义一个字符串的源码对象
*/
public static class StringJavaFileObject extends SimpleJavaFileObject {
//等待编译的源码字段
private String contents;
//java源代码 => StringJavaFileObject对象 的时候使用
public StringJavaFileObject(String className, String contents) {
super(URI.create("string:///" + className.replaceAll("\\.", "/") + Kind.SOURCE.extension), Kind.SOURCE);
this.contents = contents;
}
//字符串源码会调用该方法
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return contents;
}
}
/**
* 获取classPath路径
*
* @return
*/
private String getRuntimeClassPath() {
StringBuilder sb = new StringBuilder();
// 系统参数
RuntimeMXBean mxb = ManagementFactory.getRuntimeMXBean();
sb.append(mxb.getBootClassPath()).append(File.pathSeparator)
.append(mxb.getClassPath()).append(File.pathSeparator)
.append(mxb.getLibraryPath()).append(File.pathSeparator);
// classLoader
URLClassLoader contextClassLoader = (URLClassLoader) Thread.currentThread().getContextClassLoader();
for (URL url : contextClassLoader.getURLs()) {
sb.append(url.getFile()).append(File.pathSeparator);
}
return sb.toString();
}
/**
* 自定义一个编译之后的字节码对象
*/
public static class ByteJavaFileObject extends SimpleJavaFileObject {
//存放编译后的字节码
private ByteArrayOutputStream outPutStream;
public ByteJavaFileObject(String className, Kind kind) {
super(URI.create("string:///" + className.replaceAll("\\.", "/") + Kind.SOURCE.extension), kind);
}
//StringJavaFileManage 编译之后的字节码输出会调用该方法(把字节码输出到outputStream)
@Override
public OutputStream openOutputStream() {
outPutStream = new ByteArrayOutputStream();
return outPutStream;
}
//在类加载器加载的时候需要用到
public byte[] getCompiledBytes() {
return outPutStream.toByteArray();
}
}
// 编译完之后需要调用该方法将setUserDefineBinData的值存入数据库(blob类型),以便下次使用
public Class<?> loadClass(DefineCodeTemplate template) throws ClassNotFoundException {
// 每次需要定义新的classLoader,防止 duplicate class definition
final InnerUdCodeClassLoader[] innerUdCodeClassLoader = {null};
AccessController.doPrivileged((PrivilegedAction) () -> {
innerUdCodeClassLoader[0] = new InnerUdCodeClassLoader(Thread.currentThread().getContextClassLoader());
return innerUdCodeClassLoader[0];
});
// 获取全类名(随便写个方法或直接自己写个生成类名的方法即可)
String functionClassName = this.getUdFunctionClassName(template);
if (javaFileObjectMap.containsKey(functionClassName)) {
// 设置userDefineBinData为编译后字节码值
template.setUserDefineBinData(javaFileObjectMap.get(functionClassName).getCompiledBytes());
}
return innerUdCodeClassLoader[0].findClass(functionClassName);
}
动态执行
- 动态类加载器
/**
* @version 1.0
* @className RunTimeClassLoader
* @description 动态加载class
* @date 2022/2/9 12:04
**/
public class RunTimeClassLoader extends ClassLoader {
public RunTimeClassLoader(ClassLoader parent) {
super(parent);
}
public Class<?> defineClass(String name, byte[] bytes) {
return this.defineClass(name, bytes, 0, bytes.length);
}
}
- 加载类
RunTimeClassLoader classLoader = new
RunTimeClassLoader(Thread.currentThread().getContextClassLoader());
byte[] codeByte = null; // 需替换成ByteJavaFileObject对象getCompiledBytes方法的返回值
Class<?> clazz = classLoader.defineClass("全类名", codeByte);
Object obj = clazz.newInstance();
将obj强转成fillClassHeaderAndTail方法构建代码时实现的接口或利用反射调用具体的方法即可
问题
mxb.getClassPath()(System.getProperty("java.class.path"))类加载器无法加载项目的classes目录,因为编译时都是通过classes目录下编译后的字节码文件进行引入依赖的,所以编译时classes目录必须要加载到。
原因
经过一系列的排查和尝试后,发现是由于idea的运行配置中
shorten command line设置成红框内的选项导致的,这时我们可以将选项设置成绿框内任意一个选项即可解决编译时总是找不到某某类的问题。 none选项未尝试,不做评价。