日常撸代码,肯定会遇见这样的情况:
-classpath
这个命令行命令不生效针对如上的问题, 本文将系统的进行一次解答。
如下信息皆摘录并缩减于 Java doc
java启动进程有两种命令, 命令组成如下:
他们分别对应着类文件和Jar文件的启动方式。
进程启动命令与其它的命令无差别,都是可执行程序 + 传参, 其中, [options] class 是java
的传参, * [arguments] *是class
的传参。
Java的Options有如下六类:
-D/-version/-jar
等-Xdebug/-Xmn/-Xms
等-XX:MaxDirectMemorySize/-XX:+PerfDataSaveToFile
等Java
在每个不同的平台都有不同的实现, 但是总体执行方式是类似的。源码参考OpenJDK
日常编码中都会引用一些第三方的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,有这么几种解决方案
-Xbootclasspath:
/ -Xbootclasspath/a:
/ -Xbootclasspath/p
,其中只有第二个被Java推荐{Java_home}\jre\lib\ext
下, 会自动被Extension ClassLoader
加载Class-Path
Manifest扩展(主要应对于maven构建)-classpath
命令, 可执行选项变更为类文件
(推荐)Java 类加载可以参见这篇文章:图解Java类加载机制
Java的类加载有一个很显著的特点,便是必须显式的指定ClassLoader的加载区域
${JAVA_HOME}/jre/lib
部分jar包${JAVA_HOME}/jre/lib/ext
下面的jar包-classpath
或者Jar包的Class-Path
定义的第三方包其中BootstrapClassLoader
为C语言编写的加载器, 它会负责加载ExtClassLoader
和AppClassLoader
在内的一系列java.*
和sun.*
的类文件。
而包含ExtClassLoader
和AppClassLoader
在内的类加载器, 实质性的类加载也需要依托于Java的 JNI 机制, 源码参见 OpenJDK的hotspot/src/share/vm
。
目前直观意义上的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/lib
和BOOT-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打包之后的类文件可以知道, Manifest 中的启动类是Spring Boot的自定义代码org.springframework.boot.loader.JarLauncher
。 它所处的位置位于Jar包的根目录,刚好被Java类加载机制所支持。
而位于其中的 BOOT/class
目录往往是不可用的,毕竟一般人编码包名不会叫做 BOOT.*
其加载原理则是通过自定义类加载器LaunchedURLClassLoader
实现类加载。 且看下述流程图:
其中Spring Boot Maven插件
会重写 JarFile
和 JarEntry
等一系列相关的类。 其中的根Jar包为打包之后的Jar包, 修饰方案依然是Java原生的JarFile, 其内部的一级JarEntry同为原生。
但是其二级Jar包(根Jar包里面的Jar包)的修饰方案则为Spring Boot Maven插件
提供的实现方案,使用 !/
来链接一级和二级Jar包, 实现文件的访问及类加载。
因此, 使用Spring Boot Maven插件
启动的Java代码的类加载器都是LaunchedURLClassLoader
, 加载路径则都在根Jar包之下。通过将根Jar包里面的每个一级文件的URL添加到自己的Classpath下令自己可以直接访问, 通过将每个一级文件Jar包构造成JarFile使得他的类加载机制可以延续Java本身的类加载方案。
如何直接访问位于Jar包中的Jar包。以及访问其中的class文件和类文件。
这样的环境编码往往只能自行添加Jar包进行服务。 此时最佳的方案不是自己构建Manifest, 而是将所有依赖的Jar包打成一个zip包, 需要用的时候解压, 然后使用 “-cp 或者 -classpath
“命令将所有的Jar包、资源包括在一起,后跟Main方法。
直接使用Spring Boot Maven插件
, 省去一切烦恼。
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<executions>
<execution>
<goals>
<goal>repackagegoal>
goals>
execution>
executions>
plugin>