You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义ClassLoader云端重复加载类,本地与云端表现差异排查求助

动态加载类的本地与云端执行差异问题

背景

我需要在运行中的云端程序里动态执行未被JVM加载的方法,无需重新部署。实现思路是编写自定义ClassLoader,通过接口接收Class文件并加载执行。

自定义ClassLoader代码:

@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);
        // 检查当前类加载器是否已加载该类
        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");
        // ... 业务逻辑
    }
}

暴露的接口代码:

@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 Corretto 8

操作流程

  • 本地用javac编译任务类AnyTask;
  • 调用接口传入编译后的class文件,命令如下:
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.InvocationTargetException异常。查看自定义ClassLoader的日志发现:

  1. Class [com.wosai.it.oms.task.AnyTask] is going to be loaded.
  2. Class [com.wosai.it.oms.util.SpringContextUtil] is going to be loaded.

第二次加载时触发异常,因为自定义ClassLoader无法加载SpringContextUtil类,但本地仅打印第一条日志,执行正常。


原因分析

  1. 类加载器层级差异:

    • 本地运行时,应用类(包括SpringContextUtil)由系统类加载器(AppClassLoader)加载,自定义ClassLoader的默认父加载器就是系统类加载器,因此加载AnyTask时,对SpringContextUtil的依赖会由父加载器直接提供,无需自定义ClassLoader处理。
    • 云端部署在Web容器(如Tomcat)中,应用类由Web容器的类加载器(WebappClassLoader)加载,该类加载器是系统类加载器的子加载器。自定义ClassLoader的默认父加载器是系统类加载器,无法访问WebappClassLoader中的类,因此委托父加载器加载SpringContextUtil失败,触发自定义ClassLoader的findClass方法。
  2. 自定义ClassLoader逻辑缺陷:

    • 直接调用findClass方法跳过了JVM的双亲委派流程,没有先委托父加载器尝试加载类。
    • 每个自定义ClassLoader实例仅绑定一个Class文件的输入流,第一次加载AnyTask时输入流已被关闭,第二次加载SpringContextUtil时无法读取数据,且该ClassLoader本身也不具备加载依赖类的能力。

解决方案

  1. 指定正确的父加载器:
    修改自定义ClassLoader的构造函数,将当前应用的类加载器作为父加载器传入,确保能访问Web容器类加载器中的已有类:

    public ClassFileClassLoader(InputStream classFileInputStream, ClassLoader parent) {
        super(parent);
        this.classFileInputStream = classFileInputStream;
    }
    

    在接口中创建ClassLoader时,传入当前类的类加载器:

    ClassFileClassLoader classLoader = new ClassFileClassLoader(classFile.getInputStream(), this.getClass().getClassLoader());
    
  2. 使用loadClass替代findClass:
    遵循双亲委派机制,调用loadClass方法而不是直接调用findClass,让父加载器优先尝试加载类:

    Class<?> clazz = classLoader.loadClass(packagePath + "." + className);
    
  3. 修正ClassLoader的类加载逻辑:
    确保每个ClassLoader实例仅负责加载传入的单个类,避免输入流被重复使用。原findClass方法中,getClassData会关闭输入流,因此该ClassLoader实例只能加载一次类,符合当前场景需求,但需确保不会被用于加载其他类。

内容的提问来源于stack exchange,提问作者Sinclair

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 22:10:11