背景
个人目前从事分库分表中间件的开发,用户有个需求:期望在中间件控制台提前去测试验证“逻辑表”的拆分路由情况。其中 “逻辑表” 的拆分规则配置,支持groovy方式配置,部分业务会通过groovy引用到“业务系统”代码里面的某个类#方法。但“中间件控制台系统”里面是没有“业务系统”的代码。
如果要实现上面的需求,我们需要有一种手段,让控制台系统能够动态加载到业务系统的类#方法,方便分库分表中间件加载到对应类,继而执行拆分规则。
一种方式是将整个应用jar包上传到控制台系统,该方式评估不可行,一方面是应用jar太大,本身只需要其中某一个或者几个类(一般情况下只会引用到一个类),全部加载没有必要,反而可能会导致测试效率“慢”。
另外一种方式,就是在控制台,让用户将使用到的类的code粘贴进去,我们将对应的类动态编译到中间件控制台系统,让分库分表中间件能够加载到就行,我们目前选择的就是此种方式。
无论是哪种方式都需要注意下类隔离,避免影响这中间件控制台系统,同时也需要注意类卸载的问题,避免中间件控制台系统内存爆炸。
实现
我们使用JavaCompiler来实现动态加载java code的功能,其中:MemoryJavaFileManager、MemoryFileObject代码是从github里面复制而来,核心实现如下:
public void testDynamicLoad(String fqn,String sourceCode){
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(null, null, null);
//自定义JavaFileManager,将sourceCode转换成class bytes
MemoryJavaFileManager memoryJavaFileManager = new MemoryJavaFileManager(standardFileManager);
Map<String, byte[]> classMap = new HashMap<>();
//自定义ClassLoader,用作类隔离
MemoryClassLoader memoryClassLoader = new MemoryClassLoader(classMap);
JavaFileObject javaFileObject = memoryJavaFileManager.makeStringSource(fqn, sourceCode);
StringWriter stringWriter = new StringWriter();
JavaCompiler.CompilationTask compilerTask = compiler.getTask(stringWriter, memoryJavaFileManager, null, null, null, Arrays.asList(javaFileObject));
if (compilerTask.call()) {
Map<String, byte[]> classBytes = memoryJavaFileManager.getClassBytes();
classMap.putAll(classBytes);
} else {
throw new RuntimeException(stringWriter.toString());
}
memoryClassLoader.findClass(fqn);
}
public class MemoryJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
// compiled classes in bytes:
final Map<String, byte[]> classBytes = new HashMap<String, byte[]>();
public MemoryJavaFileManager(JavaFileManager fileManager) {
super(fileManager);
}
public Map<String, byte[]> getClassBytes() {
return new HashMap<String, byte[]>(this.classBytes);
}
@Override
public void flush() throws IOException {
}
@Override
public void close() throws IOException {
classBytes.clear();
}
@Override
public JavaFileObject getJavaFileForOutput(JavaFileManager.Location location, String className,
Kind kind, FileObject sibling) throws IOException {
if (kind == Kind.CLASS) {
return new MemoryOutputJavaFileObject(className);
} else {
return super.getJavaFileForOutput(location, className, kind, sibling);
}
}
public JavaFileObject makeStringSource(String name, String code) {
return new MemoryInputJavaFileObject(name, code);
}
static class MemoryInputJavaFileObject extends SimpleJavaFileObject {
final String code;
MemoryInputJavaFileObject(String name, String code) {
super(URI.create("string:///" + name), Kind.SOURCE);
this.code = code;
}
@Override
public CharBuffer getCharContent(boolean ignoreEncodingErrors) {
return CharBuffer.wrap(code);
}
}
class MemoryOutputJavaFileObject extends SimpleJavaFileObject {
final String name;
MemoryOutputJavaFileObject(String name) {
super(URI.create("string:///" + name), Kind.CLASS);
this.name = name;
}
@Override
public OutputStream openOutputStream() {
return new FilterOutputStream(new ByteArrayOutputStream()) {
@Override
public void close() throws IOException {
out.close();
ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
classBytes.put(name, bos.toByteArray());
}
};
}
}
}
public class MemoryClassLoader extends URLClassLoader {
private Map<String, byte[]> classBytes = new HashMap<>();
public MemoryClassLoader(Map<String, byte[]> classBytes) {
super(new URL[0], MemoryClassLoader.class.getClassLoader());
this.classBytes = classBytes;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] buf = classBytes.get(name);
if (buf == null) {
return super.findClass(name);
}
classBytes.remove(name);
return defineClass(name, buf, 0, buf.length);
}
}