Java如何访问Jar包内部的Jar包资源

  • 引言
  • Java 命令
    • Java 启动进程的命令的组成
    • Java 命令的执行过程
    • Java 传参中的 classpath 不生效
  • Java 的Jar包 的类加载
    • 如何使得Java程序读取Jar包内部的Jar包
    • Spring Boot的Jar包内类加载原理
  • Java启动命令推荐方案
    • 不处于 Maven 仓库环境下
    • 处于 Maven 仓库环境下

引言

日常撸代码,肯定会遇见这样的情况:

  • 写了一个普通Java代码,需要引用很多的第三方Jar包,但是只能把主代码和第三方代码分别存放,然后打包
  • Spring-Boot就可以把第三方Jar包打到自己的可执行包里面去
  • 无奈只能把所有的Jar包拆散,毕并不知道怎么把他们打在一起
  • 很多时候指定的 -classpath 这个命令行命令不生效

针对如上的问题, 本文将系统的进行一次解答。

Java 命令

Java 启动进程的命令的组成

如下信息皆摘录并缩减于 Java doc

java启动进程有两种命令, 命令组成如下:

  • java [ options ] class [ arguments ]
  • java [ options ] -jar file.jar [ arguments ]

他们分别对应着类文件和Jar文件的启动方式。
进程启动命令与其它的命令无差别,都是可执行程序 + 传参, 其中, [options] classjava的传参, * [arguments] *class的传参。

Java的Options有如下六类:

  • Standard Options 所有Java规范里面都支持的传参,如 -D/-version/-jar
  • Non-Standard Options Oracle官方(Java HotSpot Virtual Machine)支持的规范,如 -Xdebug/-Xmn/-Xms
  • Advanced Runtime Options:对JVM的运行约束,如 -XX:MaxDirectMemorySize/-XX:+PerfDataSaveToFile
  • Advanced JIT Compiler Options:对 JIT的运行约束
  • Advanced Serviceability Options:对系统信息的相关的参数,如日志路径等
  • Advanced Garbage Collection Options:与垃圾回收相关的约束

Java 命令的执行过程

Java在每个不同的平台都有不同的实现, 但是总体执行方式是类似的。源码参考OpenJDK

Created with Raphaël 2.1.2 开始 拆解命令(实现于 java.c) 系统类加载 将拆解的信息注入JVM,例如classpath 用户类加载 结束

Java 传参中的 classpath 不生效

日常编码中都会引用一些第三方的Jar包, 通常是使用 -classpath 命令将其加入到JVM中的, 可是有的时候会出现对于Jar包使用 -classpath 命令的时候不生效的情况。 这又是为什么呢?

这个需要参考源码片段:

// ··· 其它代码
while ((arg = *argv) != 0 && *arg == '-') {
    argv++; --argc;
    if (JLI_StrCmp(arg, "-classpath") == 0 || JLI_StrCmp(arg, "-cp") == 0) {
        ARG_CHECK (argc, ARG_ERROR1, arg);
        SetClassPath(*argv);
        mode = LM_CLASS;
        argv++; --argc;
    } else if (JLI_StrCmp(arg, "-jar") == 0) {
        ARG_CHECK (argc, ARG_ERROR2, arg);
        mode = LM_JAR;
    }
    // ··· 其它代码
}
// ··· 其它代码

Java命令会通过-classpath命令确定当前的可执行文件为类文件, 或者通过-jar命令确定当前可执行文件为Jar包。如果两者都没有被发现, 则默认为类文件

对于Jar包,如果同时出现-classpath命令, 会发生什么事情呢? 答案是 -classpath命令会被忽略。
参考:-jar参数运行应用时classpath的设置方法

为什么不生效? 我想是因为既定的Jar包, Java设计的时候,classpath就需要参考到Jar里面的manifest吧。
不生效的原因?目前还没有翻到这相关的源码。希望有知道的人介绍一下。

对于 -jar 参数中的 -classpath,有这么几种解决方案

  • java 官方提供方案: -Xbootclasspath: / -Xbootclasspath/a: / -Xbootclasspath/p,其中只有第二个被Java推荐
  • 将jar包放置在{Java_home}\jre\lib\ext下, 会自动被Extension ClassLoader加载
  • 使用 Class-Path Manifest扩展(主要应对于maven构建)
  • 不要在Manifest中包含主函数,而是依旧使用-classpath命令, 可执行选项变更为类文件推荐

Java 的Jar包 的类加载

Java 类加载可以参见这篇文章:图解Java类加载机制

Java的类加载有一个很显著的特点,便是必须显式的指定ClassLoader的加载区域

  • BootstrapClassLoader负责加载${JAVA_HOME}/jre/lib部分jar包
  • ExtClassLoader加载${JAVA_HOME}/jre/lib/ext下面的jar包
  • AppClassLoader加载用户自定义-classpath或者Jar包的Class-Path定义的第三方包

其中BootstrapClassLoader 为C语言编写的加载器, 它会负责加载ExtClassLoaderAppClassLoader在内的一系列java.*sun.*的类文件。

而包含ExtClassLoaderAppClassLoader在内的类加载器, 实质性的类加载也需要依托于Java的 JNI 机制, 源码参见 OpenJDK的hotspot/src/share/vm

如何使得Java程序读取Jar包内部的Jar包

目前直观意义上的Java是没法读取Jar包内部的Jar包, 如下图

run.jar
|——org
|  |——springframework
|     |——boot
|        |——loader
|           |——JarLauncher.class
|           |——Launcher.class
|——META-INF
|  |——MANIFEST.MF  
|——BOOT-INF
|  |——class
|     |——Main.class
|     |——Begin.class
|  |——lib
|     |——commons.jar
|     |——plugin.jar
|  |——resource
|     |——a.jpg
|     |——b.jpg

对于Java而言, classpath可搜寻区域只有org一层。在BOOT-INF/libBOOT-INF/class里面的文件不属于classloader搜素对象(如果编写了相对路径依然可以访问到内部的资源),直接访问的话会报NoClassDefDoundErr异常。

Java 本身支持访问Jar包里面的资源, 他们以 Stream 的形式存在(他们本就处于Jar包之中)。Jar文件被描述为JarFile, 里面的资源文件被描述为JarEntry。可以通过判断JarEntry的Jar属性使得直接访问Jar包内部的Jar包。

如果希望直接在自己的类里面访问引用在 Jar包中的Jar包, 可以使用Spring Boot打包插件。强烈推荐该方案, 对所有的Jar项目实用

<plugin>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-maven-pluginartifactId>
    <executions>
        <execution>
            <goals>
                <goal>repackagegoal>
            goals>
        execution>
    executions>
plugin>

Spring Boot的Jar包内类加载原理

通过访问Spring Boot打包之后的类文件可以知道, Manifest 中的启动类是Spring Boot的自定义代码org.springframework.boot.loader.JarLauncher。 它所处的位置位于Jar包的根目录,刚好被Java类加载机制所支持。

而位于其中的 BOOT/class 目录往往是不可用的,毕竟一般人编码包名不会叫做 BOOT.*

其加载原理则是通过自定义类加载器LaunchedURLClassLoader实现类加载。 且看下述流程图:

Created with Raphaël 2.1.2 JarLauncher.main 创建类加载器 LaunchedURLClassLoader 加载位置:Jar包根目录下BOOT/class;BOOT/lib class拆解方案:去掉 BOOT/class 前缀直接得到 类 lib拆解方案:提供JarFile(重写了java.util.jar.JarFile) lib类类文件拆解方案:提供JarEntry(重写了 java.util.jar.JarEntry) 用户class.main

其中Spring Boot Maven插件会重写 JarFileJarEntry 等一系列相关的类。 其中的根Jar包为打包之后的Jar包, 修饰方案依然是Java原生的JarFile, 其内部的一级JarEntry同为原生。

但是其二级Jar包(根Jar包里面的Jar包)的修饰方案则为Spring Boot Maven插件提供的实现方案,使用 !/ 来链接一级和二级Jar包, 实现文件的访问及类加载。

因此, 使用Spring Boot Maven插件启动的Java代码的类加载器都是LaunchedURLClassLoader, 加载路径则都在根Jar包之下。通过将根Jar包里面的每个一级文件的URL添加到自己的Classpath下令自己可以直接访问, 通过将每个一级文件Jar包构造成JarFile使得他的类加载机制可以延续Java本身的类加载方案。

Java启动命令推荐方案

如何直接访问位于Jar包中的Jar包。以及访问其中的class文件和类文件。

不处于 Maven 仓库环境下

这样的环境编码往往只能自行添加Jar包进行服务。 此时最佳的方案不是自己构建Manifest, 而是将所有依赖的Jar包打成一个zip包, 需要用的时候解压, 然后使用 “-cp 或者 -classpath“命令将所有的Jar包、资源包括在一起,后跟Main方法。

处于 Maven 仓库环境下

直接使用Spring Boot Maven插件, 省去一切烦恼。

<plugin>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-maven-pluginartifactId>
    <executions>
        <execution>
            <goals>
                <goal>repackagegoal>
            goals>
        execution>
    executions>
plugin>

你可能感兴趣的:(Java)