深入理解JAR包

對於大多數 Java 開發人員來說,JAR 文件及其 “近親” WAR 和 EAR 都只不過是漫長的 Ant 或 Maven 流程的最終結果。標準步驟是將一個 JAR 複製到服務器(或者,少數情況下是用戶機)中的合適位置,然後忘記它。
事實上,JAR 能做的不止是存儲源代碼,您應該瞭解 JAR 還能做什麼,以及如何進行。在這一期的 5 件事 系列中,將向您展示如何最大限度地利用 Java Archive 文件(有時候也可是 WAR 和 EAR),特別是在部署時。
由於有很多 Java 開發人員使用 Spring(因爲 Spring 框架給傳統的 JAR 使用帶來一些特有的挑戰),這裏有幾個具體技巧用於在 Spring 應用程序中處理 JAR 。

我將以一個標準 Java Archive 文件產生過程的簡單示例開始,這將作爲以下技巧的基礎。
把它放在 JAR 中
通常,在源代碼被編譯之後,您需要構建一個 JAR 文件,使用 jar 命令行實用工具,或者,更常用的是 Ant jar 任務將 Java 代碼(已經被包分離)收集到一個單獨的集合中,過程簡潔易懂,我不想在這做過多的說明,稍後將繼續說明如何構建 JAR。現在,我只需要存檔 Hello,這是一個獨立控制檯實用工具,對於執行打印消息到控制檯這個任務十分有用。如例 1 所示:

例 1. 存檔控制檯實用工具
				
package com.tedneward.jars;

public class Hello
{
    public static void main(String[] args)
    {
        System.out.println("Howdy!");
    }
}

Hello 實用工具內容並不多,但是對於研究 JAR 文件卻是一個很有用的 “腳手架”,我們先從執行此代碼開始。
回頁首
1. JAR 是可執行的
.NET 和 C++ 這類語言一直是 OS 友好的,只需要在命令行(helloWorld.exe)引用其名稱,或在 GUI shell 中雙擊它的圖標就可以啓動應用程序。然而在 Java 編程中,啓動器程序 — java — 將 JVM 引導入進程中,我們需要傳遞一個命令行參數(com.tedneward.Hello)指定想要啓動的 main() 方法的類。
這些附加步驟使使用 Java 創建界面友好的應用程序更加困難。不僅終端用戶需要在命令行輸入所有參數(終端用戶寧願避開),而且極有可能使他或她操作失誤以及返回一個難以理解的錯誤。
這個解決方案使 JAR 文件 “可執行” ,以致 Java 啓動程序在執行 JAR 文件時,自動識別哪個類將要啓動。我們所要做的是,將一個入口引入 JAR 文件例文件(MANIFEST.MF 在 JAR 的 META-INF 子目錄下),像這樣:

例 2. 展示入口點!
				
Main-Class: com.tedneward.jars.Hello

這個例文件只是一個名值對。因爲有時候例文件很難處理回車和空格,然而在構建 JAR 時,使用 Ant 來生成例文件是很容易的。在例 3 中,使用 Ant jar 任務的 manifest 元素來指定例文件:

例 3. 構建我的入口點!
				
    <target name="jar" depends="build">
        <jar destfile="outapp.jar" basedir="classes">
            <manifest>
                <attribute name="Main-Class" value="com.tedneward.jars.Hello" />
            </manifest>
        </jar>
    </target>

現在用戶在執行 JAR 文件時需要做的就是通過 java -jar outapp.jar 在命令行上指定其文件名。就 GUI shell 來說,雙擊 JAR 文件即可。
回頁首
2. JAR 可以包括依賴關係信息
似乎 Hello 實用工具已經展開,改變實現的需求已經出現。Spring 或 Guice 這類依賴項注入(DI)容器可以爲我們處理許多細節,但是仍然有點小問題:修改代碼使其含有 DI 容器的用法可能導致例 4 所示的結果,如:

例 4. Hello、Spring world!
				
package com.tedneward.jars;

import org.springframework.context.*;
import org.springframework.context.support.*;

public class Hello
{
    public static void main(String[] args)
    {
        ApplicationContext appContext =
            new FileSystemXmlApplicationContext("./app.xml");
        ISpeak speaker = (ISpeak) appContext.getBean("speaker");
        System.out.println(speaker.sayHello());
    }
}

關於 Spring 的更多信息
這個技巧將幫助您熟悉依賴項注入和 Spring 框架。
由於啓動程序的 -jar 選項將覆蓋 -classpath 命令行選項中的所有內容,因此運行這些代碼時,Spring 必須是在 CLASSPATH 和 環境變量中。幸運的是,JAR 允許在例文件中出現其他的 JAR 依賴項聲明,這使得無需聲明就可以隱式創建 CLASSPATH,如例 5 所示:

例 5. Hello、Spring CLASSPATH!
				
 <target name="jar" depends="build">
        <jar destfile="outapp.jar" basedir="classes">
            <manifest>
                <attribute name="Main-Class" value="com.tedneward.jars.Hello" />
                <attribute name="Class-Path" 
                    value="./lib/org.springframework.context-3.0.1.RELEASE-A.jar 
                      ./lib/org.springframework.core-3.0.1.RELEASE-A.jar 
                      ./lib/org.springframework.asm-3.0.1.RELEASE-A.jar 
                      ./lib/org.springframework.beans-3.0.1.RELEASE-A.jar 
                      ./lib/org.springframework.expression-3.0.1.RELEASE-A.jar 
                      ./lib/commons-logging-1.0.4.jar" />
            </manifest>
        </jar>
    </target>

注意 Class-Path 屬性包含一個與應用程序所依賴的 JAR 文件相關的引用。您可以將它寫成一個絕對引用或者完全沒有前綴。這種情況下,我們假設 JAR 文件同應用程序 JAR 在同一個目錄下。
不幸的是,value 屬性和 Ant Class-Path 屬性必須出現在同一行,因爲 JAR 例文件不能處理多個 Class-Path 屬性。因此,所有這些依賴項在例文件中必須出現在一行。當然,這很難看,但爲了使 java -jar outapp.jar 可用,還是值得的!
回頁首
3. JAR 可以被隱式引用
如果有幾個不同的命令行實用工具(或其他的應用程序)在使用 Spring 框架,可能更容易將 Spring JAR 文件放在公共位置,使所有實用工具能夠引用。這樣就避免了文件系統中到處都有 JAR 副本。Java 運行時 JAR 的公共位置,衆所周知是 “擴展目錄” ,默認位於 lib/ext 子目錄,在 JRE 的安裝位置之下。
JRE 是一個可定製的位置,但是在一個給定的 Java 環境中很少定製,以至於可以完全假設 lib/ext 是存儲 JAR 的一個安全地方,以及它們將隱式地用於 Java 環境的 CLASSPATH 上。
回頁首
4. Java 6 允許類路徑通配符
爲了避免龐大的 CLASSPATH 環境變量(Java 開發人員幾年前就應該拋棄的)和/或命令行 -classpath 參數,Java 6 引入了類路徑通配符 的概念。與其不得不啓動參數中明確列出的每個 JAR 文件,還不如自己指定 lib/*,讓所有 JAR 文件列在該目錄下(不遞歸),在類路徑中。
不幸的是,類路徑通配符不適用於之前提到的 Class-Path 屬性例入口。但是這使得它更容易啓動 Java 應用程序(包括服務器)開發人員任務,例如 code-gen 工具或分析工具。
回頁首
5. JAR 有的不只是代碼
Spring,就像許多 Java 生態系統一樣,依賴於一個描述構建環境的配置文件,前面提到過,Spring 依賴於一個 app.xml 文件,此文件同 JAR 文件位於同一目錄 — 但是開發人員在複製 JAR 文件的同時忘記複製配置文件,這太常見了!
一些配置文件可用 sysadmin 進行編輯,但是其中很大一部分(例如 Hibernate 映射)都位於 sysadmin 域之外,這將導致部署漏洞。一個合理的解決方案是將配置文件和代碼封裝在一起 — 這是可行的,因爲 JAR 從根本上來說就是一個 “喬裝的” ZIP 文件。 當構建一個 JAR 時,只需要在 Ant 任務或 jar 命令行包括一個配置文件即可。
JAR 也可以包含其他類型的文件,不僅僅是配置文件。例如,如果我的 SpeakEnglish 部件要訪問一個屬性文件,我可以進行如下設置,如例 6 所示:

例 6. 隨機響應
				
package com.tedneward.jars;

import java.util.*;

public class SpeakEnglish
    implements ISpeak
{
    Properties responses = new Properties();
    Random random = new Random();

    public String sayHello()
    {
        // Pick a response at random
        int which = random.nextInt(5);
        
        return responses.getProperty("response." + which);
    }
}

可以將 responses.properties 放入 JAR 文件,這意味着部署 JAR 文件時至少可以少考慮一個文件。這隻需要在 JAR 步驟中包含 responses.properties 文件即可。
當您在 JAR 中存儲屬性之後,您可能想知道如何將它取回。如果所需要的數據與 JAR 文件在同一位置,正如前面的例子中提到的那樣,不需要費心找出 JAR 文件的位置,使用 JarFile 對象就可將其打開。相反,可以使用類的 ClassLoader 找到它,像在 JAR 文件中尋找 “資源” 那樣,使用 ClassLoader getResourceAsStream() 方法,如例 7 所示:

例 7. ClassLoader 定位資源
				
package com.tedneward.jars;

import java.util.*;

public class SpeakEnglish
    implements ISpeak
{
    Properties responses = new Properties();
    // ...

    public SpeakEnglish()
    {
        try
        {
            ClassLoader myCL = SpeakEnglish.class.getClassLoader();
            responses.load(
                myCL.getResourceAsStream(
                    "com/tedneward/jars/responses.properties"));
        }
        catch (Exception x)
        {
            x.printStackTrace();
        }
    }
    
    // ...
}

您可以按照以上步驟尋找任何類型的資源:配置文件、審計文件、圖形文件,等等。幾乎任何文件類型都能被捆綁進 JAR 中,作爲一個 InputStream 獲取(通過 ClassLoader),並通過您喜歡的方式使用。

注意,所有的 JAR 相關技巧對於 WAR 同樣可用,一些技巧(特別是 Class-Path 和 Main-Class 屬性)對於 WAR 來說不是那麼出色,因爲 servlet 環境需要全部目錄,並且要有一個預先確定的入口點,但是,總體上來看這些技巧可以使我們擺脫 “好的,開始在該目錄下複製......” 的模式,這也使得他們部署 Java 應用程序更爲簡單。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章