程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 管理Java類路徑(UNIX和Mac OS X)

管理Java類路徑(UNIX和Mac OS X)

編輯:關於JAVA

類路徑可以連接 Java 運行庫和文件系統。它定義編譯器和解釋器應該在何處查找要加載的 .class 文件。它的基本思想是:文件系統的層次結構反映了 Java 包的層次結構,而類路徑則定義了文件系統中的哪個目錄可以作為 Java 包層次結構的根。

遺憾的是,通常文件系統非常復雜並依賴於平台,而且和 Java 包也不能很好地匹配。這樣一來,不論是新用戶還是資深 Java 程序員都深感類路徑的棘手。沒錯,它的確不是 Java 平台好的一面,它讓您到了下班的時候還在忙於調試一個頑固的小問題。

當然采用 Eclipse 這樣的優秀 IDE 可以減少管理類路徑的一些困難,但只能說是一些,而且前提還必須是一切都正常(但這不大可能,因為總會有一些意外出現)。因此,每個 Java 程序員都必須要全面了解類路徑,惟有如此,才有希望調試類路徑中所出現的問題。

在本文中,我給出了您所需要了解的有關 UNIX、Linux 和 Mac OS X 中的 Java 類路徑(以及相關源路徑)的全部內容。本文的 姊妹篇 則展示了 Windows 上的類似技術。文中列出的步驟可以作為指南,並能解決出現的大多數問題。

包結構

要掌握類路徑,首先應從其源代碼入手。每個類都屬於一個包,而此包必須 遵守標准的命名約定。簡單地說,包的名稱要由顛倒的兩級域名開始,比如 com.example 或 edu.poly,之後是至少一個或多個單詞用於描述包的內容。比方說,假設有一個域名為 elharo.com,如果要創建一個 Fraction 類,可以將其放入如下包中:

com.elharo.math

com.elharo.numbers

com.elharo.math.algebra.fields

在顛倒的域名之後,需要使用單一單詞的子包名。不要使用縮寫形式,並要保證拼寫正確。如果需要,可以使用拼寫檢查器。大部分與類路徑相關的問題都是由在源代碼中使用某個單詞而在文件系統中使用的卻是與之稍有不同的拼寫或縮寫而引起的。所以最好的做法就是總是使用拼寫正確且沒有縮寫的名稱。

整個包名稱應該是小寫的,即使該名稱是在別處常采取大寫形式的一些慣用名稱和縮寫詞。Windows 通常不區分文件名中的大小寫,但 Java 和一些 UNIX 文件系統卻區分。如果需要在不同的系統間移動文件,大小寫問題肯定會帶來一些麻煩。包名稱必須要全部由 ASCII 字符組成。一些編譯器也接受用 Hebrew、Cyrillic、Greek 或其他腳本編寫的包名稱,但大多數文件系統並不接受;您稍後就會看到,這樣的包名稱必須擔負充當目錄名這樣的雙重任務。Java 包和類名是 Unicode,但很多文件系統(包括 FAT)卻不能識別 Unicode。遺憾的是,FAT 系統非常之多。如果只簡單地用不同的默認編碼將文件復制到系統將會使編譯器和解釋器無法找到正確的類。

不要試圖在包名稱方面節約成本。長遠來看,這只會有百害而無一利。如果需要域名就買一個。如果名稱太長就買個短些的(我曾經買到了 xom.nu 這樣一個域名,因而我的包前綴就只有 6 個字符)。不要將類放到默認包中(默認包是指如果未在類中包含一條包語句時系統默認給出的包)。如果包訪問不利於對象間的通信,就需要向類中添加更多的公共方法。需要多次使用的類必須要放到包中。

目錄結構

下一步要做的是組織源文件來匹配包結構。在某處創建一個干淨的空白目錄。本文中,我將其命名為 project。在這個目錄裡,再創建兩個目錄:bin 和 src。(有些人更喜歡將其分別命名為 build 和 source。)

接下來,在 src 目錄,建一個與包層次結構相匹配的層次結構。例如,如果給定類名為 com.elharo.math.Fraction,我會將 com 目錄放到 src 目錄中,然後在 com 目錄中創建一個 elharo 目錄,再在 elharo 目錄內放一個 math 目錄,最後在 math 目錄內放上 Fraction.java,如圖 1 所示:

圖 1. 目錄結構符合包結構

要點:不要在 src 目錄中放置除源代碼之外的任何內容。通常這裡放入的文件都是 .java 文件。在有些情況下,也可放置 .html 文件(用於 JavaDoc)或其他類型的源代碼。然而,決不能在此結構內放置 .class 文件或任何其他編譯並生成的工件。這樣做只會帶來麻煩。遺憾的是,如果不夠謹慎,javac 編譯器就會 “明知故犯”。在下一節,將介紹如何修復這一問題。

編譯

編譯 Java 代碼需要一些技巧,原因是必須要跟蹤如下幾方面相關但又有所不同的內容:

正在編譯的目標文件。

編譯器在其中尋找目標文件導入 .java 文件的那個目錄。

編譯器在其中尋找目標文件導入 .class 文件的那個目錄。

編譯器在其中放置編譯輸出的目錄。

默認地,javac 編譯器將上述目錄都認為是當前目錄,而這並不是您所希望的。因此,需要在編譯時顯式地指定這些元素。

要編譯的文件

指定的第一個要編譯的文件是 .java 文件,以從當前目錄到該文件的整個路徑的形式給出。比如,假設當前所在目錄是 圖 1 所示的 project 目錄。該目錄包含 src 目錄。此 src 目錄包含 com 目錄,而 com 目錄又包含 example 目錄,example 目錄下是 Fraction.java 文件。如下命令行對它進行編譯:

$ javac src/com/elharo/math/Fraction.java

如果路徑不正確,就會給出這樣的錯誤消息:

error: cannot read: src/com/example/mtah/Fraction.java

如果出現這樣的錯誤消息,就需要檢查路徑的各個部分,確保它們拼寫正確。然後再通過一個與下面類似的 ls 檢查該文件是否處於它應該出現的位置:

$ ls src/com/example/math
ls: src/com/example/math: No such file or directory

出現問題的原因通常是因為路徑拼寫錯誤,但也可能是由於當前的目錄不對。在本例中,需要檢查當前的工作目錄是不是 project 目錄。pwd 命令在這裡非常有用。例如,以下命令將告訴我,我實際上處於 project/src 而不是 project 目錄中:

$ pwd
/Users/elharo/documents/articles/classpath/project/src

在編譯之前,我需要執行 cd ..。

輸出到哪裡

假設沒有出現任何語法錯誤,javac 將編譯後的 .class 文件放到與之對應的.java 文件所在的相同目錄內。這並不是您所想要的結果。將 .class 和 .java 文件混在一起常常會使清理編譯後的文件十分困難,因為很可能會意外刪除本應保留的 .java 文件。這常會使清理構建十分困難,而且還會導致版本問題。發布一個二進制時,只對編譯後的 .class 文件進行歸檔也會十分困難。因此,需要告知編譯器將編譯後的輸出放到一個完全不同的目錄內。-d 開關用來指定輸出目錄(通常稱為 bin、build 或 class):

$ javac -d bin src/com/elharo/math/Fraction.java

現在輸出如圖 2.所示,注意 javac 已建立了完整的目錄層次結構 com/elharo/math。不需要再手動建立。

圖 2. 並行源和編譯後的層次結構

源路徑

源路徑 就是 Java 在其中尋找源文件的那個目錄。具體到本例,就是 src 目錄。該目錄必須包含源文件的層次結構,這些源文件可以被放到它們自己的目錄中。因此它不是 com 目錄也不是 src/com/elharo/math 目錄。

很多項目都使用不止一個類和包。它們通過導入語句和完整的包限定類名連接起來。例如,假設您在 com.elharo.gui 包裡面創建一個新的 MainFrame 類 如清單 1 所示:

清單 1. 一個包中的類可以導入另一個包中的類

package com.elharo.gui;
import com.elharo.math.*;
public class MainFrame {
  public static void main(String[] args) {
   Fraction f = new Fraction();
   // ...
  }
}

該類使用的是與 MainFrame 類所在的包不同的包中的 com.elharo.math.Fraction 類。源設置現在應該如圖 3 所示(我將編譯後的輸出從之前的步驟中刪除了。但這沒有關系,因為我總是能重新編譯它)。

圖 3. 幾個包的源結構

現在來看一下試著像以前一樣編譯 MainFrame.java 會出現什麼情況。

清單 2. 編譯 MainFrame.java

$ javac -d bin src/com/elharo/gui/MainFrame.java
src/com/elharo/gui/MainFrame.java:3: package com.elharo.math does not exist
import com.elharo.math.*;
^
src/com/elharo/gui/MainFrame.java:7: cannot find symbol
symbol : class Fraction
location: class com.elharo.gui.MainFrame
  private Fraction f = new Fraction();
      ^
src/com/elharo/gui/MainFrame.java:7: cannot find symbol
symbol : class Fraction
location: class com.elharo.gui.MainFrame
  private Fraction f = new Fraction();
              ^
3 errors

出現清單 2 中的錯誤的原因是,雖然 javac 知道到何處可以找到 MainFrame.java,但它卻並不知道到何處可以找到 Fraction.java(您可能覺得它應該具備足夠的智能來識別匹配的層次結構,但事實並非如此)。為了給它提供一些線索,必須指定源路徑。用源路徑指定編譯器應該到哪些目錄查找源文件的層次結構。在清單 2 中,源路徑是 src。所以我使用了 -sourcepath 選項,如下所示:

$ javac -d bin -sourcepath src src/com/elharo/gui/MainFrame.java

現在再編譯程序,就不會出現錯誤,並會產生如圖 5 所示的輸出。請注意 javac 也編譯了文件 Fraction.java,Fraction.java 被當前編譯的文件引用。

圖 4. 多類輸出

在源路徑中編譯多個目錄

在源路徑中可以有多個目錄,使用冒號分隔各目錄,但通常沒有必要這麼做。例如,若我想包括本地的 src 目錄和用來存放另一個項目的源代碼的 /Users/elharo/Projects/XOM/src 目錄,我可以這樣進行編譯:

$ javac -d bin -sourcepath src:/Users/elharo/Projects/XOM/src
  src/com/elharo/gui/MainFrame.java

該命令並不編譯在這兩個層次結構中所找到的每個文件。它只編譯由單個的 .java 文件直接或間接引用的文件,而此 .java 文件必須被編譯。

更常見的情況是,為 .java 文件用一個單一的源目錄,為類或放置了預編譯的第三方庫的 JAR 歸檔文件用多個目錄。而這正是類路徑的作用所在。

設置類路徑

在大中型項目中,每次都要對每個文件進行重編譯會非常耗時。為減少這種編譯負擔,可以在不同的 bin 目錄分別編譯和存儲相同項目的獨立部分。這些目錄被添加到類路徑。

將類添加到類路徑有幾種方法可選。但您只能使用 -classpath 命令行開關。例如,假設我想從另一個之前已經編譯到目錄 /Users/elharo/classes 的工程導入文件,那麼我會向命令行添加 -classpath /Users/elharo/classes,如下所示:

$ javac -d bin -sourcepath src -classpath /Users/elharo/classes
  src/com/elharo/gui/MainFrame.java

現在假設需要添加兩個目錄,/Users/elharo/project1/classes 和 /Users/elharo/project2/classes。那麼我將包含它們並使用冒號將它們分隔開,如下所示:

$ javac -d bin -sourcepath src
  -classpath /Users/elharo/project1/classes:/Users/elharo/project2/classes
  src/com/elharo/gui/MainFrame.java

當然,您也可以使用自己喜歡的各種相對路徑的格式。比如,如果 project1 和 project2 是當前工作目錄的同級目錄(即它們有相同的父目錄),那麼我會這樣引用它們:

$ javac -d bin -sourcepath src
  -classpath ../project1/classes:../project2/classes
  src/com/elharo/gui/MainFrame.java

到目前為止,我都一直假設程序完全獨立並且沒有使用任何單獨的編譯後的第三方庫。如果需要使用第三方庫,還必須將它們也添加到類路徑。庫通常是 JAR 文件的格式,比如 junit.jar 或 icu4j.jar。在本例中,需要向類路徑添加的只是 JAR 文件本身,而不是包含 JAR 文件的目錄(從實質上講,JAR 文件可以充當包含編譯後的 .class 文件的一種目錄)。例如,如下命令會向類路徑添加三項內容:/Users/elharo/classes 目錄,當前工作目錄裡的 icu4j.jar 文件和 /Users/elharo/lib 下的 junit.jar 文件:

$ javac -d bin -sourcepath src
  -classpath /Users/elharo/classes:icu4j.jar:/Users/elharo/lib/junit.jar
  src/com/elharo/gui/MainFrame.java

JAR 文件僅用於 .class 文件和其類路徑,不用於 .java 文件及其源路徑。

運行程序

現在您已經成功地編譯了程序,可以運行它了。運行與編譯相似但更為簡單一些。當運行程序時,只需指定兩項內容:

類路徑。

包含 main() 方法的類的完全限定包名。

無需指定源路徑。

通常這裡的類路徑與編譯程序所使用的類路徑相同,只是多了一個放置編譯後的輸出的目錄。例如,如果編譯命令如下所示:

$ javac -d bin -sourcepath src
  -classpath /Users/elharo/classes:/Users/elharo/lib/junit.jar
  src/com/elharo/gui/MainFrame.java

並且 main() 方法在 com.elharo.gui.Mainframe.java 類內,就可以像這樣運行此程序:

$ java
  -classpath /Users/elharo/classes:/Users/elharo/lib/junit.jar
  com.elharo.gui.MainFrame

請務必注意命令行的最後一項是類名。它不是一個文件名,也不是 .java 或 .class。該類必須能夠在類路徑的某處找到。

可能存在類的其他地方

我強烈建議您在編譯和運行時總是顯式地指定類路徑。也可以將文件放到其他地方,以便它們可以被添加到類路徑中,並被 javac 編譯器和 java 解釋器找到。這種做法會節省一些鍵入操作,但當(注意不是如果)您無意間將一個舊版本的類放到類路徑中時,這卻會耗費大量的調試時間。

在本節,將展示類常常隱匿其中的幾個地點,這些類很可能會出乎意料地冒到類路徑中並導致問題的出現。在不受您控制的機器上(比如服務器),這更為多見。

當前的工作目錄

編譯器總是將當前工作目錄 (.) 添加到類路徑,而不管您是否曾顯式地要求這樣做。您很容易忘記在和您所在的目錄相同的目錄中有和沒有的內容。因此,請盡量避免將任何類或層次結構放入 project 或 home 目錄。相反地,應該將 .java 文件和 .class 文件分別放入 src 目錄和 bin 目錄。

CLASSPATH

過一會,您就會發現向類路徑手工添加 bin 目錄和 JAR 歸檔文件太過繁瑣。這時您可能會想要使用 CLASSPATH 環境變量。可以只向 CLASSPATH 環境變量添加一次目錄和 JAR 歸檔文件,之後就不需要在每次運行 javac 或 java 時都要再鍵入這些路徑。

請務必抵制這種誘惑。這樣做,一旦加載了錯誤的類或錯誤版本的類,就會出問題。而且意外加載錯誤的類所帶來的調試時間常常會百倍於省下的那點鍵入時間。要避免輸入並自動處理類路徑有更好的方法。

jre/lib/ext

jre/lib/ext 目錄中的 JAR 歸檔文件會被添加到通過虛擬機運行的所有應用程序的類路徑。這看起來很方便,實際上它與向 CLASSPATH 環境變量添加目錄一樣,存在長遠的潛在問題。您遲早(通常很快)會在您想都想不到的地方加載類的一個錯誤版本的類並會為此付出大量的調試時間。

部署一個服務器端的應用程序時,問題就更為嚴峻。請確保部署到的服務器在其 jre/lib/ext 目錄沒有任何額外的 JAR。如果您不熟悉錯誤症狀,也不知道該如何查找,那麼由類路徑中的錯誤版本的 JAV 歸檔文件所帶來的問題可能會非常難於調試。為了避免這些問題的出現,一些框架甚至編寫了自己的類加載器,用來繞過 Java 代碼通常的類加載機制。

jre/lib/endorsed

jre/lib/endorsed 目錄裡的 JAR 文件 也被添加到了通過虛擬機運行的所有應用程序的類路徑。不同的是,這裡的文件被實際放入了 bootclasspath 而不是通常的類路徑,並可以代替 JDK 附帶的標准類。這種方式對於在 VM 更新 XML 解析器和修復 bug 尤其有用。

但是,如前所述,這種方法看起來十分方便,但實際上也存在長期的潛在問題,原因也一樣。如果需要替換 JDK 類,可以在運行時使用 -Xbootclasspath/p 選項來避免意外地加載錯誤版本的類。

$ java -classpath /Users/elharo/classes
    -Xbootclasspath/p:xercesImpl.jar com.elharo.gui.MainFrame

自動管理類路徑

在想要使用電動射釘槍之前要先熟練使用錘子,與此相似,在試圖采用更強大的自動管理工具之前也要先能自如地手動管理這些類。如果您掌握了命令行工具集,就可以使用另外的工具來自動處理源路徑和類路徑所需的一些繁瑣過程。這些工具大部分也需要您像本文所介紹的那樣組織文件。

IDE

像 Eclipse 和 NetBeansMost 這樣的許多開發環境都能協助類路徑的自動管理。例如,當更改包的名稱時,Eclipse 能相應地移動對應的 .java 文件,如圖 5 所示:

圖 5. 在 Eclipse 中快速修復類路徑

請記住,這些 IDE 位於文件系統的頂部,必須正確設置,尤其是當需要與其他工具和其他 IDE 集成時就更應如此。這些工具最大的貢獻是用 GUI 對話框、樹視圖和選項卡代替了命令行開關參數,但其基本的文件結構還是一樣的。

Ant

Ant 是自動化構建過程的事實上的標准工具。與將目錄放在 jre/lib/ext 或 CLASSPATH 環境變量的做法不同,Ant 真的可以讓您創建單步的構建過程。但您仍然需要在 Ant build.xml 設置類路徑並手動將源文件放到正確的目錄。但至少現在您無需在每次編譯都要重新進行指定。

Maven

Maven 在組織和自動化構建過程方面比 Ant 還要更進一步。Maven 提供一個合理的默認設置讓您可以通過添加少許幾行代碼並將源文件放到 Maven 能夠找到的位置即可構建簡單的項目。您仍然需要調整文件系統和包的層次結構。Maven 在管理第三方庫的依賴性方面也有上佳的表現,雖然它不如 Ant 那麼易於定制。

結束語

不管類路徑有多麼棘手,您都可以通過一些簡單的規則對它加以管制,尤其是要記住如下的一些原則:

將類放到包中。

嚴格遵守包和類的命名約定和大小寫約定。

確保包的層次結構與目錄的層次結構匹配。

總是對 javac 應用 -d 選項。

不要在 jre/lib/ext 內放任何東西。

不要在 jre/lib/endorsed 內放任何東西。

不要將 .java 文件與 .class 文件放在同一個目錄。

不要將任何 .java 或 .class 文件放在當前的工作目錄。

最後一點提示:很多耗時的類路徑問題的起因大都是目錄名拼寫錯誤或從錯誤目錄進行了編譯。如果您不能找到問題的所在,可以問問周圍的朋友或同事。以我的經驗,自己發現自己的錯誤總是困難的,但這些錯誤在別人看來卻顯而易見。所以尋求他人的幫助也是一種切實有效的調試技巧。

類路徑確實不是個簡單的問題,但總會有相應的應對方法,所以它是完全可管理的。些許的謹慎加上對本文所介紹的命名約定、命令行參數和目錄結構的注意,應該能夠使您在問題最少的情況下編譯和運行程序了。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved