<转载注明出处:www.zhuowenfeng.com或blog.csdn.net/wenfengzhuo>
在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.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$AppClassLoader.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来加载这些类。
你会这么做吗?估计不会。