Search code examples
javaspringjvm

Why does this ClassLoader be used twice?


Background

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"

Process

  1. Compile the task class AnyTask with javac on local machine.
  2. Use the compiled class to invoke the exposed interface
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"

Question

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:

  1. Class [com.wosai.it.oms.task.AnyTask] is going to be load.
  2. Class [com.wosai.it.oms.util.SpringContextUtil] is going to be load. And it occurred an exception when invoked the second, of course, that must be an exception, because my customized classloader cannot load SpringContextUtil!

But on my local machine, there is only one log printed:

  1. Class [com.wosai.it.oms.task.AnyTask] is going to be load. So, it's running normally.

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.


Solution

  • 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.