I want to execute an function that has not been loaded in JVM in a running program.
My program is running on cloud, and I want to dynamically execute an function without redeploying the application.
My mean is writing a customized ClassLoader receiving Class file from my exposed interface, the customized classloader is here:
@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;
}
}
And then I make a task class with a function:
public class AnyTask {
public void afterSaleGoodsOfZD(String acc) {
AfterSalesDao afterSalesDao = (AfterSalesDao) SpringContextUtil.getBean("afterSalesDao");
// ... some business logic
}
}
And my exposed interface like this:
@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 version: 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"
The process is totally normal on my local machine, but when it comes to cloud, the problem appears. It will throw an exception "java.lang.reflect.InvocationTargetException"
With my log log.info("Class [{}] is going to be loaded.", name);
in ClassFileClassLoader I found that this log was invoked twice:
But on my local machine, there is only one log printed:
I want to know why the same code has different performance in local and cloud? As my expected, it should be executed normally like local.
When a class loader defines a class, the JVM will use this defining class loader for resolving all symbolic references. You might not have noticed so far, as the inherited loadClass
implementation will only delegate to findClass
if the standard delegation failed.
Since you do not specify a parent loader, the system class loader will be used as delegation target, which might work in your local environment but fail in the cloud environment. You should pass the defining class loader of your own code as target.
So, a correct findClass
method would only return the specific class when the requested class name matches. But the better approach is not to override findClass
at all, leave all resolving to the default, but implement a method doing the intended operation in the first place, to define a particular class:
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);
}
}
You may have to add public
modifiers if the class does not reside in the same package as the using code but I’d keep the visibility at minimum.
Then, you may use it like
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;
}
}
Note that calling newInstance()
directly on a Class
is discouraged (and deprecated with Java 9 and newer). The instance should be created by calling newInstance()
on a Constructor
as demonstrated above.
Further, the reading code has been improved to use try-with-resources.