我想在运行的程序中执行一个尚未加载到JVM中的函数。
我的程序在云端运行,我想动态执行一个函数而不需要重新部署应用程序。
我的意思是编写一个自定义的类加载器,从我公开的接口接收类文件,自定义的类加载器在这里:
@Slf4j
public class ClassFileClassLoader extends ClassLoader {
private final InputStream classFileInputStream;
public ClassFileClassLoader(InputStream classFileInputStream) {
this.classFileInputStream = classFileInputStream;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
log.info("Class [{}] is going to be loaded.", name);
//check if this class has been loaded by super ClassLoader
Class<?> clazz = super.findLoadedClass(name);
if (clazz == null) {
byte[] classData = getClassData();
if (classData == null) {
throw new ClassNotFoundException();
}
clazz = defineClass(name, classData, 0, classData.length);
}
return clazz;
}
private byte[] getClassData() {
try {
byte[] buff = new byte[1024 * 4];
int len;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while ((len = classFileInputStream.read(buff)) != -1) {
baos.write(buff, 0, len);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (classFileInputStream != null) {
try {
classFileInputStream.close();
} catch(IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
然后我创建一个带有函数的任务类:
public class AnyTask {
public void afterSaleGoodsOfZD(String acc) {
AfterSalesDao afterSalesDao = (AfterSalesDao) SpringContextUtil.getBean("afterSalesDao");
// ... some business logic
}
}
我暴露的界面是这样的:
@PostMapping("/dynamicLoading")
public void dynamicLoading(@RequestParam("class") MultipartFile classFile, @RequestParam String packagePath,
@RequestParam String methodName, @RequestParam Object[] args)
throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException {
ClassFileClassLoader classLoader = new ClassFileClassLoader(classFile.getInputStream());
String className = classFile.getOriginalFilename().split("\\.")[0];
Class<?> clazz = classLoader.findClass(packagePath + "." + className);
Method[] methods = clazz.getMethods();
for (Method m : methods) {
if (m.getName().equals(methodName)) {
Object o = clazz.newInstance();
m.invoke(o, args);
return;
}
}
}
JVM 版本:1.8.0_392 (arm64)“Amazon”-“Amazon Corretto 8”
curl --location 'http://xxxx/api/task/dynamicLoading' \
--form 'class=@"/xx/xx/xx/AnyTask.class"' \
--form 'packagePath="com.wosai.it.oms.task"' \
--form 'methodName="afterSaleGoodsOfZD"' \
--form 'args="acc"
这个过程在我本地机器上完全正常,但是当到了云端时,问题就出现了。它会抛出一个异常“java.lang.reflect.InitationTargetException”
通过 ClassFileClassLoader 中的日志
log.info("Class [{}] is going to be loaded.", name);
,我发现该日志被调用了两次:
但是在我的本地机器上,只打印了一条日志:
我想知道为什么同样的代码在本地和云端的性能不同? 正如我所料,它应该像本地一样正常执行。
当类加载器定义一个类时,JVM 将使用这个定义的类加载器来解析所有符号引用。到目前为止您可能还没有注意到,因为如果标准委托失败,继承的
loadClass
实现只会委托给 findClass
。
由于您没有指定父加载器,系统类加载器将用作委托目标,这可能在您的本地环境中工作,但在云环境中失败。您应该将您自己代码的定义类加载器作为目标传递。
因此,正确的
findClass
方法只会在请求的类名匹配时返回特定的类。但更好的方法是根本不重写findClass
,将所有解析保留为默认值,而是首先实现一个执行预期操作的方法,以定义特定的类:
class ClassFileClassLoader extends ClassLoader {
ClassFileClassLoader() {
super(ClassFileClassLoader.class.getClassLoader());
}
Class<?> defineClass(String name, InputStream source) throws IOException {
byte[] definition;
try(InputStream toReadAndClose = source) {
byte[] buff = new byte[1024 * 4];
int len;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while((len = toReadAndClose.read(buff)) != -1) {
baos.write(buff, 0, len);
}
definition = baos.toByteArray();
}
return super.defineClass(name, definition, 0, definition.length);
}
}
如果该类与使用代码不在同一个包中,您可能需要添加
public
修饰符,但我会将可见性保持在最低水平。
然后,你就可以像这样使用它了
ClassFileClassLoader classLoader = new ClassFileClassLoader();
String className = classFile.getOriginalFilename().split("\\.")[0];
Class<?> clazz = classLoader.defineClass(
packagePath + "." + className, classFile.getInputStream());
Method[] methods = clazz.getMethods();
for (Method m : methods) {
if (m.getName().equals(methodName)) {
Object o = clazz.getConstructor().newInstance();
m.invoke(o, args);
return;
}
}
请注意,不鼓励直接在
newInstance()
上调用 Class
(Java 9 及更高版本已弃用)。应通过在 newInstance()
上调用 Constructor
创建实例,如上所示。
此外,阅读代码已改进为使用try-with-resources。