为什么这个ClassLoader会被使用两次?

问题描述 投票:0回答:1

背景

我想在运行的程序中执行一个尚未加载到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”

流程

  1. 在本地机器上使用javac编译任务类AnyTask。
  2. 使用编译好的类来调用暴露的接口
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);
,我发现该日志被调用了两次:

  1. 类 [com.wosai.it.oms.task.AnyTask] 将被加载。
  2. 类 [com.wosai.it.oms.util.SpringContextUtil] 将被加载。 然后第二次调用的时候就出现了异常,当然那一定是异常,因为我自定义的类加载器无法加载SpringContextUtil!

但是在我的本地机器上,只打印了一条日志:

  1. 类 [com.wosai.it.oms.task.AnyTask] 将被加载。 所以,它运行正常。

我想知道为什么同样的代码在本地和云端的性能不同? 正如我所料,它应该像本地一样正常执行。

java spring jvm
1个回答
0
投票

当类加载器定义一个类时,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

© www.soinside.com 2019 - 2024. All rights reserved.