在java中,运行一个应用程序很简单,只要
java XXX
即可将程序跑起来。如果复杂一点,可能你的程序会依赖某些第三方库或者自己实现的库,你只需要
java -classpath(-cp) .:a.jar:b.jar app (unix)
java -classpath(-cp) .;a.jar;b.jar app (windows)
为什么要指定classpath呢?因为不指定classpath虚拟机去哪里找你想要依赖的库呀,所以不指定classpath, 编译器一定会给你一个ClassNotFound的Error。
当你更加深入了解Java技术,开始做一些企业级的项目的时候,你会发现老是直接到一个目录下运行一个class文件显得十分低端,没有档次。于是开始构建属于自己的库,将这个应用程序打成一个jar包,并且写一点自己的脚本语言,然后形成一个高端大气上档次的应用包,这个包里面会有:
MyApp
- bin
-----startup.sh
- lib
-----app.jar
-----a.jar
-----b.jar
- conf
-----conf.xml
- license ( 这个很酷)
然后在startup.sh脚本中,你开始将classpath构建好,然后使用
java -classpath .:a.jar:b.jar -jar app
将程序运行起来。一切看起来都是那么的顺利。
但是很不幸,程序意外地给了你一个错误,一个让你很无语的错误:
Exception in thread “main” java.lang.NoClassDefFoundError: com/example/a/A
at com.example.app.App.main(Server.java:14)
Caused by: java.lang.ClassNotFoundException: com.zhuowenfeng.util.StringUtil
at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
at java.net.URLClassLoader
1.
r
u
n
(
U
R
L
C
l
a
s
s
L
o
a
d
e
r
.
j
a
v
a
:
355
)
a
t
j
a
v
a
.
s
e
c
u
r
i
t
y
.
A
c
c
e
s
s
C
o
n
t
r
o
l
l
e
r
.
d
o
P
r
i
v
i
l
e
g
e
d
(
N
a
t
i
v
e
M
e
t
h
o
d
)
a
t
j
a
v
a
.
n
e
t
.
U
R
L
C
l
a
s
s
L
o
a
d
e
r
.
f
i
n
d
C
l
a
s
s
(
U
R
L
C
l
a
s
s
L
o
a
d
e
r
.
j
a
v
a
:
354
)
a
t
j
a
v
a
.
l
a
n
g
.
C
l
a
s
s
L
o
a
d
e
r
.
l
o
a
d
C
l
a
s
s
(
C
l
a
s
s
L
o
a
d
e
r
.
j
a
v
a
:
423
)
a
t
s
u
n
.
m
i
s
c
.
L
a
u
n
c
h
e
r
1.run(URLClassLoader.java:355) at java.security.AccessController.doPrivileged(Native Method) at java.net.URLClassLoader.findClass(URLClassLoader.java:354) at java.lang.ClassLoader.loadClass(ClassLoader.java:423) at sun.misc.Launcher
1.run(URLClassLoader.java:355)atjava.security.AccessController.doPrivileged(NativeMethod)atjava.net.URLClassLoader.findClass(URLClassLoader.java:354)atjava.lang.ClassLoader.loadClass(ClassLoader.java:423)atsun.misc.LauncherAppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:356)
… 1 more
你无语是因为你检查了n遍脚本输出,看到所有的依赖都已经加到classpath里面去了,这怎么可能会出现这个错误,有点颠覆你的世界观了。
这时候我需要告诉你,这里的java参数-classpath根本没有起到任何作用,换句话说,你辛辛苦苦用脚本构建出来的classpath在这里一点作用都没有。
Java在运行可执行的jar包时,它会忽略你设置的classpath以及所有环境变量,然后编译器只会以你的jar包为根路径去找class文件。这时候自然而然的蹦出了一个疑问,为什么要这样呢?甚至还可能爆一句粗口,程序员最喜欢用的词,字母F开头的。
sun的人(现在应该叫oracle了,习惯说sun)难道不会想到这点吗?我其实也在探究这个问题,有一些猜想,你可以试着去判断对错。
假设在运行可执行jar包时,-classpath是有用的,可以开开心心的加入很多依赖库,很方便省事。试想,现在你的应用程序app.jar里面的class文件是这样的:
app.jar
- com
— example
------ App.class
然后,你也写了另一个库util.jar,作为app.jar的一个依赖库,提供一些实用类什么的,它的结构是这样的:
util.jar
- com
— example
------ App.class
图个方便很多jar包都这么一个结构,或许一个第三方库,它也有这么一个类,也是这么一个结构。你想想,当java运行起来的时候,还是你想要运行的那段代码吗?要知道java的classloader很傲娇,加载了一个类,就不会去加载第二个同名(全名)的类了。所以说开发java的人也是有苦衷的。
明白了道理,是时候想想该怎么解决这个问题。
一、在java里,提供了一个启动参数的设置:
-Xbootclasspath
当你使用这个参数来设置你的classpath的时候,你发现能够成功运行你的jar包了,皆大欢喜。这个参数指定的classpath中的类,都会使用JVM 的 Boot Classloader来进行加载,这个classloader是JVM中所有classloader的老大,因此它不是由java实现的,而是使用C++/C来实现的。因此当你运行你的程序的时候,你的依赖类都会被load的。小心不要犯上面举的这个例子就好。
当然事情还没有完。
二、jar包中的Manifest.mf文件
当你开始使用一些框架来构建的应用程序时,你对java又有了更深一步的认识。比如你开始使用spring了。这是一个让你欲罢不能的框架,几乎是无所不能。spring的Annotation无所不在,也无所不能,利用注解这个有利工具,spring可以做到AOP,可以做到依赖注入,可以做到各种对象的初始化。你会好奇它是怎么实现的呢?我想其中有一个很重要的角色–Classloader。
正是因为classloader,spring才能轻轻松松的使用注解来管理应用程序。那么这个跟我们这篇文章有什么关系呢?关系大着。
当你使用spring构建了应用程序,也像上面的App一样进行了打包,构建脚本。然后使用-xbootclasspath指定了路径。你觉得万无一失,然后运行了脚本,可是程序却扔给了你一个错误:
bean cannot be initialized。。。
我觉得你应该已经知道了原因。对,没错,就是因为-Xbootclasspath提前把你的依赖类load进了JVM,才让你的spring的classloader再也不能去初始化已经用Annotation声明过的Component,Controller and so on。是不是接近崩溃了。
但是,任何事情都是有解决办法的。jar包它提供了一个Manifest.mf文件,用来描述这个jar包的属性等信息。比如Main-class,比如Class-Path,对!就是这个Class-Path能够解决以上所有问题,让你不再烦恼jar包的classpath。你只要在Manifest.mf中指定你所依赖的jar包名称,然后将这些jar包置于应用程序jar包的同一层目录或者子目录,然后就轻轻松松的可以运行程序啦。
Class-Path: a.jar b.jar lib/c.jar
至此我想说的就到这里了。
三、其实还有一种很silly的方法,将依赖包copy到{Java_home}\jre\lib\ext目录下,然后使用系统的extension classloader来加载这些类。
你会这么做吗?估计不会。
————————————————
版权声明:本文为CSDN博主「wenfengzhuo」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wenfengzhuo/article/details/10741825