背景
最近在在项目中遇到了一个类加载的问题,几经周折没有找到合适的解决方式,只能怪自己学艺不精。没办法只好重拾曾今丢掉的 java 知识,尝试从源头开始分析问题。
环境
- Win 10 企业版
java version “1.8.0_251”
Java™ SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot™ 64-Bit Server VM (build 25.251-b08, mixed mode)
问题描述:
我所遇到的是一个类重复加载的问题,但是这个问题目前并不能完美复现。因此我希望能够通过查看jvm的类加载过程来判断类重复加载的原因,可以在运行时结束一下参数:
java -verbose:class -cp *.jar com.example.Application
但是此时我又遇到问题了,我的 -cp *.jar
参数并没有生效。我的初衷是希望能够加载到当前目录下的所有jar包,但是实际情况是并没有加载到。经过多次试验,在这里记录一下我所犯下的错误以及错误的原因。
问题1. java -verbose:class -cp *.jar com.example.Application
加载失败
可以从图片看到,jvm只是简单的加载了一些启动所需要的类,而我指定的 jar 包一个也没有加载到。从而导致错误 找不到或无法加载主类 com.example.Application
,这是因为在 windows 环境下 *.jar
的通配符并没有生效,正确的通配符的使用方式为:
java -verbose:class -cp * com.example.Application
可以看到,需要移除*.jar
中的.jar
,只剩下通配符即可。
问题2. java -verbose:class -cp . com.example.Application
-cp .
,小朋友,你是否有很多问号,在初学安装配置 java 时,教程里往往会要求你在环境变量下配置一个 CALSSPATH
的环境变量,并且要求值为:
.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;
还小心翼翼的嘱咐你不要忘记加前面的 .
,还会告诉你.
代表当前目录,不知道有没有思考过JVM到底从当前目录下加载了什么?反正我是没有思考过,总之是在从网上查-cp
测资料时,我在这里碰壁了。
经过试验,我可以简单的总结为.
所代表的是 JVM
会加载当前目录下的文件,但是并不是全部文件,而是相对应的字节码文件。也就是 JVM
会把当前路径作为 class
的 path
,因此会解析当前path
下的 class
,至于当前 path
下的 jar
包嘛,你不指定的话JVM
肯定是当做没看见了。
示例
当前我有如下目录
D:\demo01
│ hutool-all-5.4.3.jar
│
└─com
└─example
Main.class
Main.java
可以看到 demo01 文件夹下有一个至尊神器 hutool-all
的 jar 包,然后对应有一个 com.example.Main.java
java 源码文件,还有一个经过命令编译得到的 com.example.Main.class
的 java 字节码文件,编译命令如下:
javac -cp hutool-all-5.4.3.jar com/example/Main.java
对应的com.example.Main.java
的源码:
package com.example;
import cn.hutool.core.lang.Console;
public class Main{
public static void main(String[] args){
System.out.println("Hello World");
Console.log("hello world by hutool!");
}
}
可以看到我在 Main.java
中引入了hutool-all
jar包中的一个类,当我执行如下命令时:
D:\demo01>java -cp . com.example.Main
Hello World
Exception in thread "main" java.lang.NoClassDefFoundError: cn/hutool/core/lang/Console
at com.example.Main.main(Main.java:7)
Caused by: java.lang.ClassNotFoundException: cn.hutool.core.lang.Console
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 1 more
可以看到在执行 Console.log("hello world by hutool!")
时发生了找不到类的错误,这是因为在指定 classpath时只指定了当前目录,但没有指定 hutool-all
的 jar 包导致的。在这种情况下 JVM
只会读取当前目录下所有对应的编译后的类文件并且执行。
而当我换一种方式指定 classpath
时:
D:\demo01>java -cp * com.example.Main
错误: 找不到或无法加载主类 com.example.Main
可以看到,此时JVM
处于找不到我的 com.example.Main.class
类的状态,由此可见 -cp *
的命令并没有让 JVM
读取到当前目录下的字节码文件,而且由于没有找到入口类,所以也不清楚JVM
是否有加载 hutool-all
的jar
包。
至此,可以看到,我们一次也没有让代码正常执行过。现在,我们退而求其次,先不考虑通配符的问题,而是先尝试让我们的代码跑起来。从上面的代码执行可以看到-cp .
可以让JVM
读取当前目录下的字节码文件。现在我们只需添加指定一个 hutool-all.jar
即可让我们的代码跑起来。在 windows 下,分隔不同 jar 包的分隔符是 ;
D:\demo01>java -cp .;hutool-all-5.4.3.jar com.example.Main
Hello World
hello world by hutool!
可以看到我们的代码正常执行了。
现在,我们再回过头来试试我们的通配符*
D:\demo01>java -cp ".;*.jar" com.example.Main
Hello World
Exception in thread "main" java.lang.NoClassDefFoundError: cn/hutool/core/lang/Console
at com.example.Main.main(Main.java:7)
Caused by: java.lang.ClassNotFoundException: cn.hutool.core.lang.Console
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 1 more
D:\demo01>java -cp ".;*" com.example.Main
Hello World
hello world by hutool!
可以看到,当我们指定为-cp ".;*.jar"
时,会发现*.jar
并没有解析到我们对应的jar
包,但是响应的 -cp .;*
却解析到的对应的jar
包,因此通配符*.jar
是无效的,这里可能是因为JDK版本的原因或者操作系统的原因吧,不太明白,这里暂且估算一个原因,下面是 java 命令的帮助说明截取:
-cp <目录和 zip/jar 文件的类搜索路径>
-classpath <目录和 zip/jar 文件的类搜索路径>
用 ; 分隔的目录, JAR 档案
和 ZIP 档案列表, 用于搜索类文件。
- 当指定为目录时(如
.
)- 搜索目录下的所有字节码文件并解析为类
- 当指定为 jar 或 zip 压缩包时
- 读取压缩包并解析其中所有的字节码文件并解析为类
- 当指定为通配符
*
时- 搜索当前或者目录下的所有以
zip/jar
结尾的压缩包并解析(不会递归解析子目录)
- 搜索当前或者目录下的所有以
我猜JVM在解析的过程中,当指定为文件或者文件的通配符时,本身只会解析jar
包和zip
压缩包;当指定为目录时,则会解析目录下的所有字节码文件并加载为类。因此我们不在需要特别指定.jar
为文件的后缀了,而如果指定的 .jar
作为文件的后缀时 JVM
反而会去尝试解析 hutool-all-5.4.3.jar.jar
文件,但由于没有这个文件,自然而然解析不到了。
问题3. 通配符*
会不会解析当前目录的子目录下的 jar包?
不会,*
只会解析当前目录下的 jar 包,至于子目录下的 jar 包还需要另外指定。
问题4. -cp
参数 与 -jar
参数能否一起使用?
简单来说,是不能一起使用的,两者加载 classpath
是不一致的,前者 -cp
选项在加载 jar 包和 class 类文件时,是通过后面拼接的参数来加载的,相对的 -jar
选项在加载 jar
包时则是获取配置文件中设置的 classpath
从而进行加载。
当指定了 -jar
选项后,JVM不再从 -cp
选项中指定的jar包路径和 类路径 中加载 jar 包,因此同时设置 -cp
参数 和-jar
参数的结果是 -cp
参数相当于没有设置。
问题5.-jar
参数是如何加载 jar
包的?
以一个命令为例:
java -jar main.jar
执行该命令时,JVM 会从 main.jar 包中的检索 META-INF\MANIFEST.MF
文件,然后在该文件中,有一个叫Class-Path
的参数,它指定了java -jar
命令执行的类路径。
以下是一个META-INF.MF清单文件
Manifest-Version: 1.0
Class-Path: . cmd_lib/commons-lang3-3.7.jar
Main-Class: com.yveshe.PackageClass
Name: java/util/
Manifest-Version:
用来定义manifest文件的版本,例如:Manifest-Version: 1.0
Main-Class
定义jar文件的入口类,该类必须是一个可执行的类(包含main方法的类),一旦定义了该属性即可通过 java -jar x.jar来运行该jar文件。
运行Jar: java -jar yveshe.jar
当运行上述命令时JVM将在yveshe.jar文件中的MANIFEST.MF文件中查找Main-Class属性的值,并尝试运行该类。如果在yveshe.jar文件中未包含Main-Class属性,则上述命令将生成错误。
Class-Path
指定
jar
包的依赖关系,classLoader
会依据这个路径来搜索class
。
默认是相对路径,相对该jar所在的父文件夹.
可以在其manifest 文件中为JAR文件设置CLASSPATH。属性名称叫作类路径,必须在自定义清单文件中指定。 它是一个空格分隔的jar文件,zip文件和目录的列表。(不区分系统都是以空格来分隔多个jar文件)以下是一个属性配置例子:
Class-Path: . hutool-core.jar file:/c:/hutool/hutool-json.jar http://www.example.com/hutool-date.jar
这条命令配置了该
main.jar
依赖如下jar
包:
.
---- 表示当前目录下的所有类文件hutool-core
---- 表示当前jar包目录下的hutool-core.jar
jar包;file:/c:/hutool/hutool.-json.jar
一个使用文件协议文件指定的 jar 包;http://www.example.com/hutool-date.jar
使用HTTP协议的下载的 jar包;
注意: 当使用
java
命令使用-jar
(比如java -jar main.jar
)选项运行JAR
文件时
将忽略jar
中manifest
文件之外的任何CLASSPATH
设置。
书写注意
- 每行的
:
(冒号)用来分隔键值对,冒号后边一定要跟一个空格。 MANIFEST.MF
清单文件必须以一个空白行结束。Class-Path
里边的内容用空格
分隔而不是逗号
或者分号
。- 每行不能超过七十多的字符。
参考资料
关于Java -cp引用jar是否支持通配符
java命令 : java -jar 和 java -cp
Setting the class path(官方文档)
关于Java -cp引用jar是否支持通配符
classpath和jar