自定义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的日志发现:
Class [com.wosai.it.oms.task.AnyTask] is going to be loaded.Class [com.wosai.it.oms.util.SpringContextUtil] is going to be loaded.
第二次加载时触发异常,因为自定义ClassLoader无法加载SpringContextUtil类,但本地仅打印第一条日志,执行正常。
原因分析
类加载器层级差异:
- 本地运行时,应用类(包括
SpringContextUtil)由系统类加载器(AppClassLoader)加载,自定义ClassLoader的默认父加载器就是系统类加载器,因此加载AnyTask时,对SpringContextUtil的依赖会由父加载器直接提供,无需自定义ClassLoader处理。 - 云端部署在Web容器(如Tomcat)中,应用类由Web容器的类加载器(WebappClassLoader)加载,该类加载器是系统类加载器的子加载器。自定义ClassLoader的默认父加载器是系统类加载器,无法访问WebappClassLoader中的类,因此委托父加载器加载
SpringContextUtil失败,触发自定义ClassLoader的findClass方法。
- 本地运行时,应用类(包括
自定义ClassLoader逻辑缺陷:
- 直接调用
findClass方法跳过了JVM的双亲委派流程,没有先委托父加载器尝试加载类。 - 每个自定义ClassLoader实例仅绑定一个Class文件的输入流,第一次加载
AnyTask时输入流已被关闭,第二次加载SpringContextUtil时无法读取数据,且该ClassLoader本身也不具备加载依赖类的能力。
- 直接调用
解决方案
指定正确的父加载器:
修改自定义ClassLoader的构造函数,将当前应用的类加载器作为父加载器传入,确保能访问Web容器类加载器中的已有类:public ClassFileClassLoader(InputStream classFileInputStream, ClassLoader parent) { super(parent); this.classFileInputStream = classFileInputStream; }在接口中创建ClassLoader时,传入当前类的类加载器:
ClassFileClassLoader classLoader = new ClassFileClassLoader(classFile.getInputStream(), this.getClass().getClassLoader());使用
loadClass替代findClass:
遵循双亲委派机制,调用loadClass方法而不是直接调用findClass,让父加载器优先尝试加载类:Class<?> clazz = classLoader.loadClass(packagePath + "." + className);修正ClassLoader的类加载逻辑:
确保每个ClassLoader实例仅负责加载传入的单个类,避免输入流被重复使用。原findClass方法中,getClassData会关闭输入流,因此该ClassLoader实例只能加载一次类,符合当前场景需求,但需确保不会被用于加载其他类。
内容的提问来源于stack exchange,提问作者Sinclair
相关产品推荐
相关产品推荐

