使用Java擴充套件機制載入所有JAR包

ImportNew - 孟 冰川發表於2014-12-03

Java 擴充套件機制在Java教程中被描述為一種“通過標準可擴充套件的方式來讓Java平臺上所有應用使用自定義API”。正如在理解擴充套件機制進行類載入中描述的,“擴充套件框架充分使用了類載入代理機制”。這種機制會在rt.jar引導(boot)類載入之後,標準classpath中的類載入之前,載入擴充套件類。

擴充套件目錄的工作機制在類的載入上與classpath有點類似。對Java應用程式來說,所有擴充套件目錄下JAR檔案包含的類都可以訪問。然而,會有一些關鍵的不同點。這些區別會在下面的文字中高亮顯示。

特徵 Classpath 擴充套件機制(可選包)
作用域 典型的應用相關
  • -classpath/-cp
  • java.class.path
  • 可執行JAR Manifest的Class-Path

主機上所有可能的JRE

  • CLASSPATH環境變數
所有執行在特定JRE上的JVM
  • java.ext.dirs

各種主機上的JRE

  • Solaris: /usr/jdk/packages/lib/ext
  • Linux: /usr/java/packages/lib/ext
  • Windows: %SystemRoot%\Sun\Java\lib\ext
如何指定 .jar檔案
  • 明確的通過名字來指定(包括 .jar
  • 使用萬用字元來匹配所有的.jar擴充套件

.class Files

  • 指定目錄下的.class檔案
所有在指定目錄下的JAR檔案都會被載入(即使副檔名不是.jar或者沒有副檔名)
類載入順序 引導和擴充套件類載入之後 引導類載入之後,classpath上的類載入之前

一個最重要且值得重視的問題是,擴充套件機制會找出所有jar格式的檔案,即使檔案字尾名不是.jar。這意味著,改變classpath中的jar檔案字尾名以此逃過萬用字元的篩選,這種方法在擴充套件目錄中行不通。

我會用一些簡單的例子來展示一些上面提到的區別。接下來的兩段程式碼是一個簡單的HelloWorld類和一個main應用程式中的Main類。Main通過呼叫main方法來使用HelloWorld類。

HelloWorld.java

public class HelloWorld
{
   @Override
   public String toString()
   {
      return "Hello, World!";
   }
}

Main.java

import static java.lang.System.out;

public class Main
{
   public static void main(final String[] arguments)
   {
      out.println(new HelloWorld());
   }
}

為了展示classpath和擴充套件機制的主要區別,我將會把編譯過的HelloWorld.class檔案歸檔到一個jar包裡,命名為HelloWorld.jar。並把它放在一個跟編譯過的Main.class不同的目錄下。

為了展示傳統的classpath的使用,我把HelloWorld.jar放在一個叫做C:\hello的目錄下並且會用萬用字元訪問JAR來給Main使用。下面的兩個截圖對此進行了展示。


以上兩個截圖說明,儘管我刪掉了當前目錄下的HelloWorld.class,Java 主應用仍然能載入它。這是因為Java launcher被告知(通過-classpath這個可選引數)去C:\hello目錄下尋找。使用擴充套件機制,不需要把類放到當前目錄或者指定到 classpath下就可以載入。接下來的截圖展示了這一點。

上面的截圖說明,當某個類是在擴充套件目錄下的某個JAR裡,Java launcher甚至不需要把HelloWorld.class放到同一個目錄下或者在classpath中指定。這常常被用來說明使用擴充套件機制的優點。因為所有在這個JRE(或者可能是主機上的所有應用)上執行的程式都可以不用在classpath上指定就能看到擴充套件目錄下的類。

使用傳統classpath方式——指導應用去載入JAR中的類,包含.class檔案的JAR檔案必須以.jar結尾。接下來的截圖展示了當把在 classpath引用的目錄下的HelloWorld.jar重新命名為HelloWorld.backup之後所發生的事情。

上面這張圖展示了當classpath引用的目錄下JAR檔案沒有以.jar結尾時發生的NoClassDefFoundError異常。可能有點令人驚訝,擴充套件機制不是這樣工作的。所有在擴充套件目錄下的JAR檔案,不管字尾名是什麼甚至沒有字尾名都會被載入。接下來的截圖展示了這一點。

這張圖展示了,給在擴充套件目錄中的JAR檔案重新命名為沒有字尾的檔案並不妨礙類載入器載入JAR檔案中的類。換句話說,類載入機制是通過檔案型別而不是檔名或字尾來載入所有在擴充套件目錄中的JAR檔案的。正如可選包(Optional Package)概覽所總結的,“JAR檔案本身沒有什麼特別的地方,其中包含的class檔案也沒有讓JAR成為已安裝過的可選包。只有位於jre/lib/ext下,才可能讓JAR成為已安裝的可選包。”

在擴充套件目錄中放包含太多類定義的JAR會有一些風險和負面效果。例如,當我們看到classpath中所指定的類方法存在,還報出NoSuchMethodErrors異常,會令人非常惱火。這是我以前寫過眾多可以導致NoSuchMethodError問題的其中一個。但是忘記擴充套件目錄下JAR檔案中的過時(outdated)和廢棄的(obsolete)類是另一個潛在的原因。接下來會展示這一點。

接下來的兩段程式碼展示了Main.java和HelloWorld.java修改後的版本。特別要注意的是,HelloWorld有一個全新的方法,這個 方法會被新版本的Main呼叫。在這個例子中,我會把新編譯的HelloWorld.class檔案和Main放在同一個目錄下。這樣當我執行Main 的時候,就能證明擴充套件目錄下的JAR中過時的類會比當前目錄下的新類優先載入。

修改後的Hello World.java(新方法)

public class HelloWorld
{
   @Override
   public String toString()
   {
      return "Hello, World!";
   }

   public String directedHello(final String name)
   {
      return "Hello, " + name;
   }
}

修改後的Main.java

import static java.lang.System.out;

public class Main
{
   public static void main(final String[] arguments)
   {
      final HelloWorld helloWorld = new HelloWorld();
      out.println(helloWorld);
      out.println(helloWorld.directedHello("Dustin"));
   }
}

最後一張截圖展示了,擴充套件目錄下過時的HelloWorld類優先於同一目錄下的新定義的HelloWorld類載入。甚至當我把當前目錄寫進 classpath中,擴充套件目錄下的舊版本的類仍然優先。接下來的圖也同樣展示了擴充套件目錄下的JAR檔案“隱藏”了更新的JAR以及其中類的新方法。這些擴充套件目錄下的JAR檔案甚至都不是以.jar結尾的。

剛剛展示的這個例子,在擴充套件目錄下JAR導致的眾多問題來說不算很複雜。例子中,至少有一個NoSuchMethodError來提醒這個問 題。一個潛在的更加複雜的情況是,舊的類有和新類一樣的方法簽名但實現的方式已經過時。在這種情況下,可能沒有錯誤、異常或者throwable中任何一種,但是應用的邏輯不會像預期那樣工作。舊的方法可能會一直存在程式碼的底層直到被發現。當缺乏單元測試或其他測試時尤其如此。

使用擴充套件目錄會讓開發人員變得輕鬆。因為擴充套件目錄下JAR檔案中的類,可以被所有執行在與此擴充套件目錄(如果在作業系統上在主機範圍內啟用擴充套件目錄,那麼所有主機上的JRE都可以訪問)關聯JRE上的應用訪問。然而,隨意使用擴充套件目錄會有一定的風險。你會非常容易忘記擴充套件目錄下過時的類。這會妨礙類載入器選擇明顯應當被載入的版本。這種情況下,本來應該讓開發者感覺輕鬆的擴充套件機制會讓他們非常痛苦。

Elliotte Rusty Harold提對擴充套件機制有一個警告:“儘管這些看上去很方便,從長遠來看也是引入了一個隱患,遲早你會從一個你根本沒想過的地方載入一個錯誤的類版本。這會浪費你不少時間除錯”。Java教程同樣提出警告(我在這裡也著重強調):“儘管這個機制擴充套件了平臺的核心API,但是應該審慎使用。大部分情況,它是用於像JCP這樣標準化比較好的介面,同時也適用於整個站點的介面”。

儘管擴充套件(可選包)機制與classpath機制很像,並且它們都用於部分的類載入,兩者之間的區別也是非常值得注意的。特別的,記住所有的在擴充套件目錄下的JAR檔案(即使它們沒有以.jar結尾)都會被載入是很重要的。給那些JARs重新命名甚至改變他們的檔案字尾名都不足以讓類載入器忽略它們。另一方面,使用classpath的時候,重新命名classpath中指定的JAR檔案會使該JAR無法載入,改變字尾名後,即使在classpath中使用萬用字元也無法載入所有目錄中的JAR。

一些情況下,擴充套件機制是比較好的選擇,但是這種情況相當少。當處理預期以外的NoSuchMethodErrors問題時,記住擴充套件機制使很重要的。這樣就會去檢檢視看是否問題就出在擴充套件的目錄中。

相關文章