http://www.cnblogs.com/wxf0701/archive/2008/08/23/1274579.html
1.class搜尋路徑的重要性
理解class搜尋路徑對所有Java開發人員來說都很重要,但是,IDE的廣泛使用掩蓋了這項技術,使大家普遍對它缺乏瞭解,甚至包括好多老鳥。這個問題在開發分布式應用時尤其嚴重,因為應用程式運行時的系統內容可能和開發時的大不相同。
本文詳細描述了某些Java類被其他代碼引用時,Java編譯器和JVM如何使用類搜尋路徑定位這些類。這兒用一個非常簡單的例子——同一個包
中的兩個類——來具體說明。我們將通過不同的方式來編譯這兩個類,根據classpath的設定不同,編譯可能成功也可能失敗。
為了最清楚的說明這個問題,我們將只使用命令列工具進行編譯。互動式開發工具有它們自己操作classpath的方法,這些方法因產品而異。
至於是由Java編譯器在編譯時間定位需要的類,還是由JVM在運行時來做,這兩種方法沒有本質的區別。但編譯器可以從原始碼中編譯需要的類,而JVM不行。下面的例子中我們用編譯器來做,但在運行時的實現也完全類似。
2.執行個體
本例有兩個很小的類:com.web_tomorrow.CPTest1 和 com.web_tomorrow.CPTest2,如下所示:
package com.web_tomorrow;
public class CPTest1 {
public static void main(String[] args) {
System.out.println ("Run CPTest1.main()");
}
}
package com.web_tomorrow;
public class CPTest2 {
public static void main(String[] args) {
System.out.println ("Run CPTest2.main()");
CPTest1 cpt1 = new CPTest1();
}
}
Java程式碼群組織的一個最基本規則就是`package name =
directory name'(“包名 =
目錄名”)。我們將為這兩個類建立對應的目錄結構,它們在包com.web_tomorrow中,所以我們建立目錄
com/web_tomorrow來存放原始碼:
[root] com
web_tomorrow
CPTest1.java
CPTest2.java
在本文中我用符號`[root]'來表示存放上述結構的任意目錄,也就是說,根目錄的位置由你決定。當然這會隨安裝這些檔案的方式不同而不一樣。
3.基本原理
讓我們來嘗試用命令列工具javac來編譯CPTest1.java。為了完全禁止類搜尋路徑(以防已有的設定影響本例),我們在javac上加上選項`-classpath ""'。
作為第一次實驗,我們先換到CPTest1.java所在的目錄下,並且嘗試用javac和檔案名稱進行編譯。
cd [root]/com/web_tomorrow
javac -classpath "" CPTest1.java
操作成功,因為編譯器能發現CPTest1.java
(它就在當前的工作目錄下),並且CPTest1沒有引用任何其他類。輸出檔案CPTest1.class在CPTest1.java的相同目錄下,因為
你沒有給編譯器任何資訊讓它作其它處理。到現在為止,一直都很好。現在讓我們用同樣的方式來試一下CPTest2。仍然在web_tomorrow目錄
下,執行命令:
javac -classpath "" CPTest2.java
雖然目錄還是剛才的目錄,CPTest1和CPTest2也在相同的包裡,這一次卻失敗了。
錯誤資訊可能是這樣的:
CPTest2.java:7: cannot resolve symbol
symbol : class CPTest1
location: class com.web_tomorrow.CPTest2
CPTest1 cpt1 = new CPTest1(); ^
這一次和上一次成功的情況之間的區別之一就是CPTest2中有對CPTest1的引用:
CPTest1 cpt1 = new CPTest1();
這次發生了什麼呢?當編譯器遇到對CP1Test的引用時,它會認為CP1Test類與當前編譯的CP2Test類在同一個包裡。這個假定是正
確的,於是編譯器來尋找com.web_tomorrow.CP1Test,但它沒有地方可以找,因為我們已經把類搜尋路徑
明確的指定成了“”(也就是空)。
你可能認為,只要告訴編譯器在目前的目錄下尋找就可以解決這個問題。在Unix和Windows系統中,“目前的目錄”的標準符號都是一個點號(.),也就是這樣的命令:
javac -classpath "." CPTest2.java
Fail
Againt!跟剛才的例子完全一樣,現在的問題是雖然CPTest1.java在目前的目錄下,但它實現的類不是CPTest1,而是
com.web_tomorrow.CPTest1,編譯器將在目前的目錄下尋找目錄com/web_tomorrow。也就是說,它在目錄
[root]/com/web_tomorrow/com/web_tomorrow中尋找一個Java源檔案或類檔案,它們事實上根本不存在,於是出錯。
為了讓編譯器工作正常,我們不能讓類搜尋路徑指向包含CPTest1的目錄,而是按照Java標準中`package name = directory name'的規則,給類搜尋路徑指定編譯器能找到CPTest1的根目錄。這樣才能工作,雖然不太好看:
javac -classpath "../.." CPTest2.java
在考慮如何變得不難看之前,看一下這個例子(仍在同一目錄下)
javac -classpath "" CPTest1.java CPTest2.java
儘管classpath為空白,這次卻能工作。這是因為Java編譯器會在命令列中明確列出的所有原始碼中尋找引用。如果要編譯同一目錄下的多個類,有一種很簡單的方式:
javac -classpath "" *.java
'*.java'擴充為目前的目錄下所有.java檔案的列表。這就說明為什麼一次編譯多個檔案會成功,而同樣的檔案一個一個的編譯卻失敗。
編譯CPTest2有一個更方便的方法:
cd [root]
javac -classpath "." com/web_tomorrow/CPTest2.java
這次我們為CPtest2.java指定了完整的路徑,而在-classpath選項中使用了'.'。另外,我們沒有告訴編譯器在目前的目錄下尋找檔案,而是讓它從目前的目錄開始搜尋。
因為我們要找的類是com.web_tomorrow.CPTest1,編譯器將在./com/web_tomorrow(也就是說目前的目錄下的com/web_tomorrow子目錄)下尋找。這才是CPTest1.java正確的位置。
事實上,儘管我在命令列只指定了CPTest2,但事實上CPTest1同時也會被編譯。編譯器在正確的位置找到這個.java檔案,但它不能判定這個.java檔案是否包含正確的類,所以它編譯這個檔案。但請注意如果是:
cd [root]
javac -classpath "." com/web_tomorrow/CPTest1.java
就不會導致CPTest2.java被編譯,因為編譯器編譯CPTest1時不需要知道CPTest2的任何事情。
類檔案與.java檔案分開的情況
到目前為止所有成功的例子都把輸出的.class檔案放到.java檔案同樣的位置,這是一種比較簡單的模式,應用非常廣泛。但很多開發人員喜歡
把源檔案和產生的檔案分開,因此它必須告訴編譯器為.class檔案維護不同的目錄。下面我們來看看這對類搜尋路徑有何影響。
首先我們刪除剛才幾個例子產生的所有.class檔案。我們還要有一個新的目錄來存放產生的.class檔案。命令列方式下的操作過程如下:
cd [root]
rm com/web_tomorrow/*.class
mkdir classes
如果你用Windows系統,別忘了把'/'替換成'\',現在的目錄結構如下:
[root] com
web_tomorrow
CPTest1.java
CPTest2.java classes
現在編譯CPTest1.java,指定類檔案的目標目錄(通過-d選項)
cd [root]
javac -d classes -classpath "" com/web_tomorrow/CPTest1.java
成功,但你會注意到,.class檔案根本不是直接放在classes目錄中,而是有了一個新的目錄結構:
[root] com
web_tomorrow
CPTest1.java
CPTest2.java
classes com
web_tomorrow
CPTest1.class
編譯器建立了一個符合包結構的目錄結構,這很有用,我們馬上就會看到。當開始編譯CPTest2.java時我們有兩個選擇,第一種是像上面一
樣,讓編譯器同時也編譯一次CPTest1;另一種是用-classpath選項指定剛才編譯好的.class檔案,這種方法更好一點,因為我們不必再來
編譯一遍CPTest1:
cd [root]
javac -d classes -classpath classes com/web_tomorrow/CPTest2.java
完成之後,我們的目錄結構如下:
[root] com
web_tomorrow
CPTest1.java
CPTest2.java
classes
com
web_tomorrow
CPTest1.class
CPTest2.class
當然我們也可以在同一條命令中編譯兩個.java檔案,結果完全相同。
4.classpath中的JAR打包檔案
Java編譯器和運行環境不僅可以在獨立檔案中搜尋類,還可以在JAR打包檔案中進行尋找。一個JAR打包檔案可以維護它自己的目錄結
構,Java按照跟普通目錄結構完全相同的方式,`directory name = package
name',進行搜尋。由於JAR本身就是一個目錄,所以在類搜尋路徑中包含JAR打包檔案時,路徑必須引用JAR本身,而不是它所在的目錄。如果我在目
錄/myclasses下有一個JAR myclasses.jar,我需要指定:javac -classpath
/myclasses/myclasses.jar ...而不僅僅是目錄myclasses。
5.多個類搜尋目錄
在上面的例子中,每次我們都只讓javac在一個目錄下搜尋。實際工作中,你的類搜尋路徑會包含很多目錄和JAR檔案。Javac的-classpath選項允許指定多個位置,但請注意,它的具體文法在Unix系統和Windows系統中稍有區別。
在Unix系統中是:
javac -classpath dir1:dir2:dir3 ...
而Windows系統則是:
javac -classpath dir1;dir2;dir3 ...
這是因為Windows使用冒號(:)作為檔案名稱的一部分,因此不能作為檔案名稱分隔字元。當然目錄分隔字元也不一樣:Unix的正斜杠(/)和Windows的反斜線(\)。
6.系統classpath
除了在javac命令指定類搜尋路徑外,我們還可以使用'系統'類路徑。如果命令中沒有指定特定的路徑,Java編譯器和JVM都使用系統路徑。在Unix和Windows系統中,系統路徑都通過環境變數設定。
例如在使用bash shell的Linux系統中:
CLASSPATH=/myclasses/myclasses.jar;export CLASSPATH
在Windows中:
set CLASSPATH=c:\myclasses\myclasses.jar
這是暫時改變系統變數CLASSPATH的好方法,但如果你想這些變化一直保留下來,你就需要針對你的系統做一些修改。例如在Linux系統
中,我會將這個命令放到我目錄下的檔案.bashrc中。而在Windows 2000/NT中可以通過‘控制台’來修改。
如果你有很多隨時用到的JAR,設定系統變數CLASSPATH就顯得尤為重要。比如說,如果我在用Sun的J2EE參考實現開發EJB應用,
所有EJB相關的類都發布在一個叫做“j2ee.jar”的JAR打包檔案中,我希望這個JAR一直都在類搜尋路徑中。另外,還有很多人希望無論目前的目錄
是什麼,搜尋路徑始終包含目前的目錄。所以在我的.bashrc檔案中就有這樣一行:
CLASSPATH=/usr/j2ee/j2ee.jar:.;export CLASSPATH
其中`.'是指'目前的目錄'
很容易看到命令列中的-classpath選項覆蓋預設的系統類別路徑;它
不是擴充而是覆蓋。因此如果我們既要包含預設的系統路徑,又要增加一些時怎麼處理呢?我們可以簡單的通過-classpath選項同時列出預設值和我們要
額外增加的部分。而一個更好的辦法是引用系統變數CLASSPATH。當然,這個文法對Windows系統和Unix系統是不同的。
在Unix上:
javac -classpath $CLASSPATH:dir1:dir2 ...
其中$CLASSPATH擴充為系統變數CLASSPATH。在Windows上:
javac -classpath %CLASSPATH%;dir1:dir2 ...