如何像CraftBukkit加载插件一样加载多Jar中的外部化Module类
核心实现思路:基于URLClassLoader的同JVM类加载
你目前用java -jar启动模块的逻辑本质是开启了独立JVM进程,天然存在类隔离问题,才需要拷贝主类代码。类似CraftBukkit的插件加载逻辑完全在同一个JVM进程内实现,通过自定义类加载器加载外部jar类即可解决依赖问题。
具体实现步骤
- 第一步:抽离公共API包
将所有模块依赖的公共定义(比如Module抽象类、事件接口、通用工具类等)单独抽成独立的API jar包,主程序和所有模块都仅依赖这个API包即可。只要API不做破坏性更新,模块完全不需要绑定主程序的具体版本,彻底解决重复拷贝主类代码的问题。 - 第二步:重写模块加载逻辑,废弃独立进程启动方案
主程序直接扫描modules目录下的jar文件,通过URLClassLoader在当前JVM进程内加载模块类,参考代码如下:
// 扫描modules目录下的所有jar包 File moduleDir = new File("modules"); File[] jarFiles = moduleDir.listFiles((dir, name) -> name.endsWith(".jar")); if (jarFiles == null) return; List<Module> loadedModules = new ArrayList<>(); for (File jar : jarFiles) { URL jarUrl = jar.toURI().toURL(); // 传入主程序的类加载器作为父加载器,模块可直接访问主程序/公共API的所有类 URLClassLoader moduleClassLoader = new URLClassLoader(new URL[]{jarUrl}, this.getClass().getClassLoader()); // 方案1:约定所有模块主实现类的全限定名,比如固定为com.xxx.module.Main // 方案2:参考Bukkit的plugin.yml,在模块jar的META-INF下新增配置文件,写入模块主类路径,加载时先读配置获取类名 Class<?> moduleImplClazz = moduleClassLoader.loadClass("com.xxx.module.Autorole"); // 实例化后转为公共抽象类Module Module moduleInstance = (Module) moduleImplClazz.getDeclaredConstructor().newInstance(); loadedModules.add(moduleInstance); } // 加载完成后将模块实例放入主程序的调用列表即可
- 第三步:实现热更新逻辑
热更时先将旧模块实例从主程序调用列表中移除,调用Module抽象类约定的destroy()方法释放旧模块占用的资源,关闭旧的URLClassLoader,删除旧jar包放入新jar包,再重新执行扫描加载逻辑即可生效。
依赖问题说明
创建URLClassLoader时传入了主程序的类加载器作为父加载器,遵循JDK双亲委派模型,模块加载类时会优先由父加载器(主程序类加载器)加载,所以模块中用到的Module抽象类、主程序公共类都能直接找到,模块编译期只要依赖公共API包即可,不需要引入主程序的代码。
内容的提问来源于stack exchange,提问作者Stormer
相关产品推荐
相关产品推荐

