部署可执行jar为何需两处提供依赖,能否直接读取jar内部依赖?
解决方案
Java默认的系统类加载器不支持直接读取Jar包内部嵌套的Jar文件,要实现单Jar部署,有以下两种成熟的编程层面实现方案:
方案1:自定义类加载器实现嵌套Jar加载
这是最底层的实现方式,核心逻辑如下:
- 继承
ClassLoader重写类加载逻辑,在findClass方法中优先读取当前可执行Jar内部的lib/路径下的内嵌Jar资源 - 读取内嵌Jar的字节流后,调用
defineClass完成类的定义加载 - 同时需要重写资源加载相关方法(
getResource、getResourceAsStream等),适配内嵌资源的读取路径
核心实现的关键代码示例:
public class NestedJarClassLoader extends ClassLoader { private final Map<String, byte[]> classCache = new HashMap<>(); public NestedJarClassLoader() throws IOException { // 读取当前可执行Jar内的所有内嵌Jar try (JarFile mainJar = new JarFile(new File(getClass().getProtectionDomain().getCodeSource().getLocation().toURI()))) { Enumeration<JarEntry> entries = mainJar.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); // 匹配lib目录下的Jar文件 if (entry.getName().startsWith("lib/") && entry.getName().endsWith(".jar")) { try (InputStream is = mainJar.getInputStream(entry); JarFile nestedJar = new JarFile(new TempFile(entry.getName(), is).toFile())) { // 遍历内嵌Jar的所有class文件,缓存字节码 Enumeration<JarEntry> nestedEntries = nestedJar.entries(); while (nestedEntries.hasMoreElements()) { JarEntry nestedEntry = nestedEntries.nextElement(); if (nestedEntry.getName().endsWith(".class")) { String className = nestedEntry.getName().replace("/", ".").replace(".class", ""); classCache.put(className, nestedJar.getInputStream(nestedEntry).readAllBytes()); } } } } } } catch (URISyntaxException e) { throw new RuntimeException(e); } } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classBytes = classCache.get(name); if (classBytes == null) { return super.findClass(name); } return defineClass(name, classBytes, 0, classBytes.length); } }
之后需要在主类的main方法最开头,将当前线程的上下文类加载器替换为自定义的类加载器,再执行后续业务逻辑。
方案2:复用现有打包工具的加载逻辑
如果不需要从零实现类加载逻辑,可以直接使用成熟工具的能力:
- 如果用Maven构建,使用
maven-shade-plugin插件,打包时会把所有内嵌Jar的类直接解压合并到主Jar的根目录,不需要额外自定义代码就能直接单Jar运行 - 如果用Spring Boot生态,
spring-boot-maven-plugin打包生成的可执行Jar内置了LaunchedURLClassLoader,天生支持加载Jar内部BOOT-INF/lib/路径下的嵌套依赖,不需要额外编码即可直接单Jar部署
注意:你当前MANIFEST.MF里配置的
Class-Path: lib/library.jar默认是从执行Jar的工作目录下查找路径,不是从Jar内部查找,这也是你现在必须外置lib目录的核心原因。
内容的提问来源于stack exchange,提问作者lazyone
相关产品推荐
相关产品推荐

