classpath 和 jar
在Java中精钮,關(guān)于什么是classpath
.classpath
是JVM用到的一個(gè)環(huán)境變量,它用來(lái)指示JVM如何搜索class
.
因?yàn)镴ava是編譯型語(yǔ)言爽待,源碼文件是.java
,而編譯后的.class
文件才是真正可以被JVM執(zhí)行的字節(jié)碼腐魂。因此咧最,JVM需要知道捂人,如何要加載一個(gè)abc.xyz.Hello
的類,應(yīng)該去哪搜索對(duì)應(yīng)的Hello.class
文件矢沿。
所以先慷,classpath
就是一組目錄的集合,它設(shè)置的搜索路徑與操作系統(tǒng)相關(guān)咨察。例如论熙,在Windows系統(tǒng)上,用;
分隔摄狱,帶空格的目錄用""
括起來(lái)脓诡,可能長(zhǎng)這樣:
C:\work\project1\bin;C:\shared;"D:\My Documents\project1\bin"
在Linux系統(tǒng)上,用:
分隔媒役,可能長(zhǎng)這樣:
/usr/shared:/usr/local/bin:/home/liaoxuefeng/bin
現(xiàn)在我們假設(shè)classpath
是.;C\work\project1\bin;C:\shared
,當(dāng)JVM加載abc.xyz.Hello
這個(gè)類時(shí)祝谚,會(huì)依次查找:
<當(dāng)前目錄>\abc\xyz\Hello.class
C:\work\project1\bin\abc\xyz\Hello.class
C:\shared\abc\xyz\Hello.class
注意到.
代表當(dāng)前目錄。如果JVM在某個(gè)路徑下找到了對(duì)應(yīng)的class
文件酣衷,就不再往后繼續(xù)搜索交惯。如果所有路徑下都沒(méi)有找到,就報(bào)錯(cuò)穿仪。
classpath
的設(shè)定方法有兩種:
在系統(tǒng)環(huán)境變量中設(shè)置classpath
環(huán)境變量席爽,不推薦;
在啟動(dòng)JVM時(shí)設(shè)置classpath
變量啊片,推薦只锻。
強(qiáng)烈不推薦在系統(tǒng)環(huán)境變量中設(shè)置classpath
,那樣會(huì)污染整個(gè)系統(tǒng)環(huán)境紫谷。在啟動(dòng)JVM時(shí)設(shè)置classpath
才是推薦的做法齐饮。實(shí)際上就是給java
命令傳入-classpath
或-cp
參數(shù):
java -classpath .;C:\work\project1\bin;C:\shared abc.xyz.Hello
或者使用-cp
的簡(jiǎn)寫:
java -cp .;C:\work\project1\bin;C:\shared abc.xyz.Hello
沒(méi)有設(shè)置系統(tǒng)環(huán)境變量捐寥,也沒(méi)有傳入-cp
參數(shù),那么JVM默認(rèn)的classpath
為.
,即當(dāng)前目錄:
java abc.xyz.Hello
上述命令告訴JVM只在當(dāng)前目錄搜索Hello.class
祖驱。
在IDE中運(yùn)行Java程序握恳,IDE自動(dòng)傳入的-cp
參數(shù)是當(dāng)前工程的bin
目錄和引入的jar包。
通常捺僻,我們?cè)谧约壕帉懙?code>class中睡互,會(huì)引用Java核心庫(kù)的class
,例如陵像,String
、ArrayList
等寇壳。這些class
應(yīng)該上哪去找醒颖?
有很多"如何設(shè)置classpath"的文章會(huì)說(shuō)明把JVM自帶的rt.jar
放入classpath
,但事實(shí)上,根本不需要告訴JVM如何去Java核心庫(kù)查找class
壳炎。
更好的做法是泞歉,不要設(shè)置
classpath
!默認(rèn)的當(dāng)前目錄.
對(duì)于絕大多數(shù)情況都?jí)蛴昧恕?br>
假設(shè)我們有一個(gè)編譯后的Hello.class
,它的包名是com.example
匿辩,當(dāng)前目錄是C:\work
,那么腰耙,目錄結(jié)構(gòu)必須如下:運(yùn)行這個(gè)
Hello.class
必須在當(dāng)前目錄下使用如下命令:
C:\work> java -cp . com.example.Hello
JVM根據(jù)classpath設(shè)置的.
在當(dāng)前目錄下查找com.example.Hello
,即實(shí)際搜索文件必須位于com/example/Hello.class
铲球。如果指定的.class
文件不存在挺庞,或者目錄結(jié)構(gòu)和包名對(duì)不上,均會(huì)報(bào)錯(cuò)稼病。
jar 包
如果有很多的.class
文件选侨,散落在各層目錄中,不便于管理然走。如果能把目錄打一個(gè)包援制,變成一個(gè)文件,就方便多了芍瑞。
jar包就是用來(lái)干這個(gè)事的晨仑,它可以把package
組織的目錄層級(jí),以及各個(gè)目錄下的所有的文件(包括.class
文件和其他文件)都打成一個(gè)jar文件拆檬,這樣一來(lái)洪己,無(wú)論是備份,還是發(fā)給客戶竟贯,就簡(jiǎn)單多了码泛。
jar 包實(shí)際上就是一個(gè)zip格式的壓縮文件,而jar包相當(dāng)于目錄澄耍。如果我們要執(zhí)行一個(gè)jar包的class
,就可以把jar包放到classpath
中:
java -cp ./hello.jar abc.xyz.Hello
這樣JVM會(huì)自動(dòng)在hello.jar
文件里去搜索某個(gè)類噪珊。
那么問(wèn)題來(lái)了:如何創(chuàng)建jar包晌缘?
因?yàn)閖ar包就是zip包,所以痢站,直接在資源管理器中磷箕,找到正確的目錄,點(diǎn)擊右鍵阵难,在彈出的快捷菜單中選擇“發(fā)送到”,“壓縮(Zipped)文件夾”,就制作了一個(gè)zip文件岳枷。然后,把后綴從.zip
改為.jar
呜叫,一個(gè)jar包就創(chuàng)建成功空繁。
假設(shè)編譯輸出的目錄結(jié)構(gòu)就是這樣:
這里需要特別注意的是,jar包里的第一層目錄朱庆,不能是
bin
,而應(yīng)該是hong
,ming
,mr
盛泡。如果在Windows的資源管理器中看,應(yīng)該長(zhǎng)這樣:說(shuō)明打包打得有問(wèn)題娱颊,JVM仍然無(wú)法從jar包中查找正確
class
傲诵,原因是hong.Person
必須按hong/Person.class
存放,而不是bin/hong/Person.class
箱硕。jar包還可以包含一個(gè)特殊的
/META-INF/MANIFEST.MF
文件拴竹,MANIFEST.MF
是純文本,可以指定Main-Class
和其它信息剧罩。JVM會(huì)自動(dòng)讀取這個(gè)MANIFEST.MF
文件栓拜,如果存在Main-Class
,我們就不必在命令行指定啟動(dòng)的類名,而是更方便的命令:
java -jar hello.jar
jar包還可以包含其它jar包惠昔,這個(gè)時(shí)候菱属,就需要在MANIFEST.MF
文件里配置classpath
了。
在大型項(xiàng)目中舰罚,不可能手動(dòng)編寫MANIFEST.MF
文件纽门,再手動(dòng)創(chuàng)建zip包。Java社區(qū)提供了大量的開(kāi)源構(gòu)建工具营罢,例如Maven,可以非常方便地創(chuàng)建jar包赏陵。
小結(jié):
JVM通過(guò)環(huán)境變量classpath
決定搜索class
的路徑和順序。
不推薦設(shè)置系統(tǒng)環(huán)境變量classpath
饲漾,始終建議通過(guò)-cp
命令傳入蝙搔;
jar包相當(dāng)于目錄,可以包含很多.class
文件考传,方便下載和使用吃型;
MANIFEST.MF
文件可以提供jar包的信息,如Main-Class
僚楞,這樣可以直接運(yùn)行jar包
模塊
從Java 9 開(kāi)始勤晚,JDK又引入了模塊(module)
.class
文件是JVM看到的最小可執(zhí)行文件枉层,而一個(gè)大型程序需要編寫很多Class,并生成一堆.class
文件赐写,很不便于管理鸟蜡,所以,jar
文件就是class
文件的容器挺邀。
在Java 9之前揉忘,一個(gè)大型Java程序會(huì)生成自己的jar文件,同時(shí)引用依賴的第三方j(luò)ar文件端铛,而JVM自帶的標(biāo)準(zhǔn)庫(kù)泣矛,實(shí)際上也是以jar文件形式存放的,這個(gè)文件叫rt.jar
禾蚕,一共有60多M您朽。
如果是自己開(kāi)發(fā)的程序,除了一個(gè)自己的app.jar
以外夕膀,還需要一堆第三方的jar包,運(yùn)行一個(gè)Java程序美侦,一般來(lái)說(shuō)产舞,命令行寫這樣:
java -cp app.jar:a.jar:b.jar:c.jar com.liaoxuefeng.sample.Main
如果漏寫了某個(gè)運(yùn)行時(shí)需要用到的jar,那么在運(yùn)行時(shí)期極有可能拋出
ClassNotFoundException
。所以菠剩,jar只是用于存放class的容器易猫,它并不關(guān)心class之間的依賴。
從Java 9 開(kāi)始引入的模塊具壮,主要是為了解決“依賴”這個(gè)問(wèn)題准颓。如果
a.jar
必須依賴另一個(gè)b.jar
才能運(yùn)行,那我們應(yīng)該給a.jar
加點(diǎn)說(shuō)明棺妓,使得程序在編譯和運(yùn)行的時(shí)候能自動(dòng)定位到b.jar
攘已,這種自帶“依賴關(guān)系”的class容器就是模塊。為了表明Java模塊化的決心怜跑,從Java 9開(kāi)始样勃,原有的Java標(biāo)準(zhǔn)庫(kù)已經(jīng)由一個(gè)單一巨大的
rt.jar
分拆成了幾十個(gè)模塊,這些模塊以.jmod
擴(kuò)展標(biāo)識(shí)性芬,可以在$JAVA_HOME/jmods
目錄下找到它們:
- java.base.jmod
- java.compiler.jmod
- java.datatransfer.jmod
- java.desktop.jmod
- ...
這些.jmod
文件每一個(gè)都是一個(gè)模塊峡眶,模塊名就是文件名。例如:模塊java.base
對(duì)應(yīng)的文件就是java.base.jmod
植锉。模塊之間的依賴關(guān)系已經(jīng)被寫入到模塊內(nèi)的module_info.class
文件了辫樱。所有的模塊都直接或間接地依賴java.base
模塊,只有java.base
模塊不依賴任何模塊俊庇,它可以被看作是“很模塊”狮暑,好比所有的類都是從Object
直接或間接繼承而來(lái)鸡挠。
把一堆class封裝為jar僅僅是一個(gè)打包的過(guò)程,而把一堆class封裝為模塊則不但需要打包心例,還需要寫入依賴關(guān)系宵凌,并且還可以包含二進(jìn)制代碼(通過(guò)JNI擴(kuò)展).此外,模塊支持多版本止后,即在同一個(gè)模塊中可以為不同的JVM提供不同的版本瞎惫。
編寫模塊
首先,創(chuàng)建模塊和原有的創(chuàng)建Java項(xiàng)目是完全一樣的译株,以oop-module
工程為例瓜喇,它的目錄結(jié)構(gòu)如下:
其中,
bin
目錄存放編譯后的class文件歉糜,src
目錄存放源碼乘寒,按包名的目錄結(jié)構(gòu)存放,僅僅在src
目錄下多了一個(gè)module-info.java
這個(gè)文件匪补,這就是模塊的描述文件.在這個(gè)模塊中伞辛,它長(zhǎng)這樣:
module hello.world {
requires java.base; // 可不寫,任何模塊都會(huì)自動(dòng)引入java.base
requires java.xml;
}
其中夯缺,module
是關(guān)鍵字蚤氏,后面的hello.world
是模塊名稱,它的命名規(guī)范與包一致踊兜「捅酰花括號(hào)的requires xxx;
表示這個(gè)模塊需要引用的其它模塊名。除了java.base
可以被自動(dòng)引入外捏境,這里我們引入了一個(gè)java.xml
模塊于游。
當(dāng)我們使用模塊聲明了依賴關(guān)系后,才能使用引入的模塊垫言。例如贰剥,Main.java
代碼如下:
package com.itranswarp.sample;
// 必須引入java.xml模塊后才能使用其中的類:
import javax.xml.XMLConstants;
public class Main {
public static void main(String[] args) {
Greeting g = new Greeting();
System.out.println(g.hello(XMLConstants.XML_NS_PREFIX));
}
}
如果把requires java.xml;
從module-info.java
中去掉,編譯將報(bào)錯(cuò)筷频○海可見(jiàn),模塊的重要作用就是聲明依賴關(guān)系截驮。
下面笑陈,我們用JDK提供的命令行工具來(lái)編譯創(chuàng)建模塊。
首先葵袭,我們把工作目錄切換到oop-module
,在當(dāng)前目錄下編譯所有的.java
文件涵妥,并存放到bin
目錄下,命令如下:
$ javac -d bin src/module-info.java src/com/itranswarp/sample/*.java
如果編譯成功∑挛現(xiàn)在項(xiàng)目結(jié)構(gòu)如下:
注意到
src
目錄下的module-info.java
被編譯到bin
目錄下的module-info.class
蓬网。下一步窒所,我們需要把bin目錄下的所有class文件先打包成jar,在打包的時(shí)候,注意傳入
--main-class
參數(shù),讓這個(gè)jar包能自己定位main
方法所在的類:
$ jar --create --file hello.jar --main-class com.itranswarp.sample.Main -C bin .
現(xiàn)在我們就在當(dāng)前目錄下得到了hello.jar
這個(gè)jar包帆锋,它和普通jar包并無(wú)區(qū)別吵取,可以直接使用命令java -jar hello.jar
來(lái)運(yùn)行它。但是我們的目標(biāo)是創(chuàng)建模塊锯厢,所以皮官,繼續(xù)使用JDK自帶的jmod
命令把一個(gè)jar包轉(zhuǎn)換成模塊:
$ jmod create --class-path hello.jar hello.jmod
于是,在當(dāng)前目錄下我們又得到了hello.jmod
這個(gè)模塊文件实辑,這就是最后打包出來(lái)的傳說(shuō)中的模塊捺氢!
運(yùn)行模塊
要運(yùn)行一個(gè)jar,我們使用java -jar xxx.jar
命令。要運(yùn)行一個(gè)模塊剪撬,我們只需要指定模塊名摄乒。試試:
$ java --module-path hello.jmod --module hello.world
結(jié)果是一個(gè)錯(cuò)誤:
Error occurred during initialization of boot layer
java.lang.module.FindException: JMOD format not supported at execution time: hello.jmod
原因是.jmod
不能被放入--module-path
中。換成.jar
就沒(méi)問(wèn)題了:
$ java --module-path hello.jar --module hello.world
Hello, xml!
那我們辛辛苦苦創(chuàng)建的hello.jmod
有什么用残黑?答案是我們可以用它來(lái)打包JRE
打包JRE
為了支持模塊化馍佑,Java 9 首先帶頭把自己的一個(gè)巨大無(wú)比的rt.jar
拆成了幾十個(gè).jmod
模塊,原因就是梨水,運(yùn)行Java程序的時(shí)候拭荤,實(shí)際上我們用到的JDK模塊,并沒(méi)有那么多冰木。不需要的模塊穷劈,完全可以刪除笼恰。
過(guò)去發(fā)布一個(gè)Java應(yīng)用程序踊沸,要運(yùn)行它,必須下載一個(gè)完整的JRE社证,再運(yùn)行jar包逼龟。而完整的JRE塊頭很大,有100多M追葡。怎么給JRE瘦身呢腺律?
現(xiàn)在,JRE自身的標(biāo)準(zhǔn)庫(kù)已經(jīng)分拆成了模塊宜肉,只需要帶上程序用到的模塊匀钧,其他的模塊就可以被裁剪掉。怎么裁剪JRE呢谬返?并不是說(shuō)把系統(tǒng)安裝的JRE給刪掉部分模塊之斯,而是“復(fù)制”一份JRE,但只帶上用到的模塊遣铝。為此佑刷,JDK提供了jlink
命令來(lái)干這件事莉擒。命令如下:
$ jlink --module-path hello.jmod --add-modules java.base,java.xml,hello.world --output jre/
我們?cè)?code>--module-path參數(shù)指定了我們自己的模塊hello.jmod
,然后瘫絮,在--add-modules參數(shù)中指定了我們用到的3個(gè)模塊java.base
涨冀、java.xml
和hello.world
,用,
分隔麦萤。最后鹿鳖,在--output
參數(shù)指定輸出目錄。
現(xiàn)在频鉴,在當(dāng)前目錄下栓辜,我們可以找到jre
目錄,這是一個(gè)完整的并且?guī)в形覀冏约?code>hello.jmod模塊的JRE垛孔。試試直接運(yùn)行這個(gè)JRE:
$ jre/bin/java --module hello.world
Hello, xml!
要分發(fā)我們自己的Java應(yīng)用程序藕甩,只需要把這個(gè)jre
目錄打個(gè)包給對(duì)方發(fā)過(guò)去,對(duì)方直接運(yùn)行上述命令即可周荐,既不用下載安裝JDK狭莱,也不用知道如何配置我們自己的模塊,極大地方便了分發(fā)和部署概作。
訪問(wèn)權(quán)限
前面講過(guò)腋妙,Java的class訪問(wèn)權(quán)限分為public、protected讯榕、private和默認(rèn)的包訪問(wèn)權(quán)限骤素。引入模塊后,這些訪問(wèn)權(quán)限的規(guī)則就要稍微做些調(diào)整愚屁。
確切地說(shuō)济竹,class的這些訪問(wèn)權(quán)限只在一個(gè)模塊內(nèi)有效,模塊和模塊之間霎槐,例如送浊,a模塊要訪問(wèn)b模塊的某個(gè)class,必要條件是b模塊明確地導(dǎo)出了可以訪問(wèn)的包丘跌。
舉個(gè)例子:我們編寫的模塊hello.world
用到了模塊java.xml
的一個(gè)類javax.xml.XMLConstants
袭景,我們之所以能直接使用這個(gè)類,是因?yàn)槟Kjava.xml
的module-info.java
中聲明了若干導(dǎo)出:
module java.xml {
exports java.xml;
exports javax.xml.catalog;
exports javax.xml.datatype;
...
}
只有它聲明的導(dǎo)出的包闭树,外部代碼才被允許訪問(wèn)耸棒。換句話說(shuō),如果外部代碼想要訪問(wèn)我們的hello.world
模塊中的com.itranswarp.sample.Greeting
類报辱,我們必須將其導(dǎo)出:
module hello.world {
exports com.itranswarp.sample;
requires java.base;
requires java.xml;
}
因此与殃,模塊進(jìn)一步隔離了代碼的權(quán)限。
小結(jié)
Java 9引入的模塊目的是為了管理依賴;
使用模塊可以按需打包JRE奈籽;
使用模塊對(duì)類的訪問(wèn)權(quán)限有了進(jìn)一步限制饥侵。