Java平臺擴展機制

本文翻譯自Oracle官網(原文地址html

擴展機制提供了一種標準的、可擴展的方式,使 Java 平臺上運行的全部應用程序均可以使用自定義 API。 Java 擴展也稱爲可選包。java

擴展是一組包和類,它們經過擴展機制加強 Java 平臺。擴展機制使運行時環境可以查找和加載擴展類,而沒必要在類路徑上命名擴展類。在這方面,擴展類相似於 Java 平臺的核心類。這也是擴展名的由來——它們實際上擴展了平臺的核心 API。程序員

因爲此機制擴展了平臺的核心 API,所以應謹慎使用它。它最經常使用於標準化的接口,例如 Java Community Process 定義的接口,儘管它也可能適用於站點範圍的接口。web

擴展機制

如圖所示,擴展充當 Java 平臺的「附加」模塊。它們的類和公共 API 可自動用於平臺上運行的任何應用程序。算法

擴展機制還提供了一種從遠程位置下載擴展類以供小程序使用的方法。數據庫

擴展被捆綁爲 Java 存檔 (JAR) 文件,若是您不瞭解 JAR 文件,您可能須要在繼續本教程中的課程以前查看一些 JAR 文件文檔:apache

1. 建立、使用擴展(Extensions)

任何一組包或類均可以很容易地扮演擴展的角色。將一組類轉換爲擴展的第一步是將它們捆綁在一個 JAR 文件中。完成後,您能夠經過兩種方式將軟件變成擴展:編程

  • 經過將 JAR 文件放在 Java 運行時環境的目錄結構中的特殊位置,在這種狀況下,它被稱爲已安裝的擴展(Installed Extensions)。
  • 經過以指定方式從另外一個 JAR 文件的清單中引用 JAR 文件,在這種狀況下,它被稱爲下載擴展(Download Extensions)。

1.1 Installed Extensions

安裝的擴展是 Java Runtime Environment (JRE™) 軟件 lib/ext 目錄中的 JAR 文件。顧名思義,JRE 是 Java 開發工具包的運行時部分,包含平臺的核心 API,但不包含編譯器和調試器等開發工具。 JRE 能夠單獨使用,也能夠做爲 Java 開發工具包的一部分使用。小程序

JRE 是 JDK 軟件的嚴格子集。 JDK 軟件目錄樹的子集以下所示:設計模式

JDK software directory tree

JRE 由圖中突出顯示框中的那些目錄組成。不管您的 JRE 是獨立的仍是 JDK 軟件的一部分,JRE 目錄的 lib/ext 中的任何 JAR 文件都會被運行時環境自動視爲擴展名。

因爲已安裝的Extensions擴展了平臺的核心 API,所以請謹慎使用它們。它們不多適用於由單個或一小組應用程序使用的接口。

此外,因爲已安裝擴展定義的符號在全部 Java 進程中都是可見的,所以應注意確保全部可見符號都遵循適當的「反向域名」和「類層次結構」約定。例如,com.mycompany.MyClass。

從 Java 6 開始,擴展 JAR 文件也能夠放置在獨立於任何特定 JRE 的位置,以便系統上安裝的全部 JRE 能夠共享擴展。在 Java 6 以前, java.ext.dirs 的值指的是單個目錄,但在 Java 6 中,它是一個目錄列表(如 CLASSPATH),指定搜索擴展的位置。路徑的第一個元素始終是 JRE 的 lib/ext 目錄。第二個元素是 JRE 以外的目錄。這個其餘位置容許安裝擴展 JAR 文件一次,並由安裝在該系統上的多個 JRE 使用。位置因操做系統而異:

  • Solaris™ Operating System: /usr/jdk/packages/lib/ext
  • Linux: /usr/java/packages/lib/ext
  • Microsoft Windows: %SystemRoot%\Sun\Java\lib\ext

請注意,放置在上述目錄之一中的已安裝擴展擴展了該系統上每一個 JRE(Java 6 或更高版本)的平臺。

一個簡單的示例

讓咱們建立一個簡單的安裝擴展。咱們的擴展由一個類 RectangleArea 組成,它計算矩形的面積:

public final class RectangleArea {
    public static int area(java.awt.Rectangle r) {
        return r.width * r.height;
    }
}

這個類有一個方法area,它接受java.awt.Rectangle 的一個實例並返回矩形的面積。

假設您要使用名爲 AreaApp 的應用程序測試 RectangleArea:

import java.awt.*;

public class AreaApp {
    public static void main(String[] args) {
        int width = 10;
        int height = 5;

        Rectangle r = new Rectangle(width, height);
        System.out.println("The rectangle's area is " 
                           + RectangleArea.area(r));
    }
}

此應用程序實例化一個 10 x 5 的矩形,而後使用 RectangleArea.area 方法打印出矩形的面積。

無擴展機制運行AreaApp

讓咱們首先回顧一下如何在不使用擴展機制的狀況下運行 AreaApp 應用程序。咱們假設 RectangleArea 類捆綁在名爲 area.jar 的 JAR 文件中。

RectangleArea 類固然不是 Java 平臺的一部分,所以您須要將 area.jar 文件放在類路徑上,以便運行 AreaApp 而不會出現運行時異常。例如,若是 area.jar 在目錄 /home/user 中,則可使用如下命令:

java -classpath .:/home/user/area.jar AreaApp

此命令中指定的類路徑既包含當前目錄(包含 AreaApp.class),也包含指向包含 RectangleArea 包的 JAR 文件的路徑。命令輸出結果:

The rectangle's area is 50

使用擴展機制運行AreaApp

如今讓咱們看看如何使用 RectangleArea 類做爲擴展來運行 AreaApp。

要使 RectangleArea 類成爲擴展,請將文件 area.jar 放在 JRE 的 lib/ext 目錄中。這樣作會自動使 RectangleArea 處於已安裝擴展的狀態。

將 area.jar 做爲擴展安裝後,您能夠運行 AreaApp 而無需指定類路徑:

java AreaApp

由於您使用 area.jar 做爲已安裝的擴展,因此運行時環境將可以找到並加載 RectangleArea 類,即便您沒有在類路徑中指定它。一樣,系統上任何用戶運行的任何小程序或應用程序都可以找到並使用 RectangleArea 類。

若是系統上安裝了多個 JRE(Java 6 或更高版本)並但願 RectangleArea 類可用做全部這些 JRE 的擴展,而不是將其安裝在特定 JRE 的 lib/ext 目錄中,請將其安裝在全局的系統位置上。例如,在運行Linux 的系統上,將area.jar 安裝在目錄/usr/java/packages/lib/ext 中。而後,AreaApp 可使用安裝在該系統上的不一樣 JRE 運行,例如,不一樣的瀏覽器配置爲使用不一樣的 JRE。

1.2 Download Extensions

下載擴展是 JAR 文件中的類集(和相關資源)。 JAR 文件的清單能夠包含引用一個或多個下載擴展的標頭。能夠經過如下兩種方式之一引用擴展:

  • 使用 Class-Path header
  • 使用 Extension-List header

請注意,清單中最多容許兩者中的一個出現。由 Class-Path 標頭指示的下載擴展僅在下載它們的應用程序(例如 Web 瀏覽器)的生命週期內下載。它們的優勢是客戶端沒有安裝任何東西;它們的缺點是每次須要時都會下載它們。由 Extension-List 標頭下載的下載擴展安裝到下載它們的 JRE 的 /lib/ext 目錄中。它們的優勢是在第一次須要它們時就下載它們;隨後它們能夠在不下載的狀況下使用。可是,如本教程後面所示,它們的部署更加複雜。

因爲使用 Class-Path 標頭的下載擴展更簡單,讓咱們首先考慮它們。例如,假設 a.jar 和 b.jar 是同一目錄中的兩個 JAR 文件,而且 a.jar 的清單包含如下標頭:

Class-Path: b.jar

而後 b.jar 中的類用做 a.jar 中類的擴展類。 a.jar 中的類能夠調用 b.jar 中的類,而沒必要在類路徑上命名 b.jar 的類。 a.jar 自己多是也可能不是擴展。若是 b.jar 與 a.jar 不在同一目錄中,則 Class-Path 標頭的值應設置爲 b.jar 的相對路徑名。

扮演下載擴展角色的類沒有什麼特別之處。它們被視爲擴展,僅僅是由於它們被其餘一些 JAR 文件的清單引用。

爲了更好地瞭解下載擴展的工做原理,讓咱們建立一個並使用它。

使用示例

假設您要建立一個使用上一節中的 RectangleArea 類的小程序(Java Applet):

public final class RectangleArea {  
    public static int area(java.awt.Rectangle r) {
        return r.width * r.height;
    }
}

在上一節中,您經過將包含它的 JAR 文件放入 JRE 的 lib/ext 目錄中,將 RectangleArea 類變成了一個已安裝的擴展。經過使其成爲已安裝的擴展,您使任何應用程序均可以使用 RectangleArea 類,就好像它是 Java 平臺的一部分同樣。

若是您但願可以從小程序使用 RectangleArea 類,狀況就有點不一樣了。例如,假設您有一個使用 RectangleArea 類的小程序 AreaApplet:

import java.applet.Applet;
import java.awt.*;

public class AreaApplet extends Applet {
    Rectangle r;

    public void init() {    
        int width = 10;
        int height = 5;

        r = new Rectangle(width, height);
    }

    public void paint(Graphics g) {
        g.drawString("The rectangle's area is " 
                      + RectangleArea.area(r), 10, 10);
    }
}

這個小程序實例化一個 10 x 5 的矩形,而後使用 RectangleArea.area 方法顯示矩形的區域。

可是,您不能假設下載和使用您的小程序的每一個人都在他們的系統上使用 RectangleArea 類做爲已安裝的擴展。解決該問題的一種方法是使 RectangleArea 類從服務器端可用,您能夠經過將其用做下載擴展來實現這一點。

要了解這是如何完成的,讓咱們假設您已將 AreaApplet 捆綁在名爲 AreaApplet.jar 的 JAR 文件中,而且類 RectangleArea 已捆綁在 RectangleArea.jar 中。爲了將 RectangleArea.jar 視爲下載擴展,必須在 AreaApplet.jar 清單的 Class-Path 標頭中列出 RectangleArea.jar。 AreaApplet.jar 的清單可能以下所示,例如:

Manifest-Version: 1.0
Class-Path: RectangleArea.jar

此清單中的 Class-Path 標頭的值爲 RectangleArea.jar,未指定路徑,代表 RectangleArea.jar 與小程序的 JAR 文件位於同一目錄中。

有關類路徑標頭的更多信息

若是小程序或應用程序使用多個擴展程序,您能夠在清單中列出多個 URL。例如,如下是一個有效的標頭:

Class-Path: area.jar servlet.jar images/

在 Class-Path 標頭中,列出的任何不以「/」結尾的 URL 都被假定爲 JAR 文件。以「/」結尾的 URL 表示目錄。在前面的示例中,images/ 多是一個包含小程序或應用程序所需資源的目錄。

請注意,清單文件中只容許使用一個 Class-Path 標頭,而且清單中的每一行長度不得超過 72 個字符。若是您須要指定的類路徑條目多於一行,您能夠將它們擴展到後續的延續行。每一個續行以兩個空格開始。例如:

Class-Path: area.jar servlet.jar monitor.jar datasource.jar
  provider.jar gui.ja

未來的版本可能會取消每一個標題只有一個實例的限制,以及將行限制爲僅 72 個字符的限制。

下載擴展能夠是「菊花鏈」,這意味着一個下載擴展的清單能夠有一個類路徑標頭,它引用第二個擴展,第二個擴展又能夠引用第三個擴展,依此類推。

安裝下載擴展

在上面的例子中,小程序下載的擴展程序只有在加載小程序的瀏覽器仍在運行時纔可用。可是,若是小程序和擴展程序的清單中都包含附加信息,小程序能夠觸發擴展程序的安裝。

因爲此機制擴展了平臺的核心 API,所以應謹慎使用它。它不多適用於由單個或一小組應用程序使用的接口。全部可見符號都應遵循反向域名和類層次結構約定。

基本要求是小程序和它使用的擴展都在其清單中提供版本信息,而且必須對其進行簽名。版本信息容許 Java Plug-in 確保擴展代碼具備小程序指望的版本。例如,AreaApplet 能夠在其清單中指定一個 areatest 擴展:

Manifest-Version: 1.0
Extension-List: areatest
areatest-Extension-Name: area
areatest-Specification-Version: 1.1
areatest-Implementation-Version: 1.1.2
areatest-Implementation-Vendor-Id: com.example
areatest-Implementation-URL: http://www.example.com/test/area.jar

area.jar 中的 manifest 會提供相應的信息:

Manifest-Version: 1.0
Extension-Name: area
Specification-Vendor: Example Tech, Inc
Specification-Version: 1.1
Implementation-Vendor-Id: com.example
Implementation-Vendor: Example Tech, Inc
Implementation-Version: 1.1.2

小程序和擴展都必須由同一簽名者簽名。對 jar 文件進行簽名將就地修改它們,在其清單文件中提供更多信息。簽名有助於確保只安裝受信任的代碼。簽署 jar 文件的一種簡單方法是首先建立一個keystore,而後使用它來保存小程序和擴展的證書。例如:

keytool -genkey -dname "cn=Fred" -alias test  -validity 180

系統將提示您輸入keystore 和 key passwords。生成密鑰後,能夠對 jar 文件進行簽名:

jarsigner AreaApplet.jar test
jarsigner area.jar test

有關 keytool、jarsigner 和其餘安全工具的更多信息,請參見 Summary of Tools for the Java 2 Platform Security.

這裏是 AreaDemo.html,它加載小程序並致使下載和安裝擴展代碼:

<html>
<body>
  <applet code="AreaApplet.class" archive="AreaApplet.jar"/>
</body>
</html>

當頁面第一次加載時,用戶被告知小程序須要安裝擴展,隨後的對話框將通知用戶有關已簽名小程序的信息。用戶贊成後會在 JRE 的 lib/ext 文件夾中安裝擴展並運行小程序。

從新啓動瀏覽器並加載相同的網頁後,只顯示關於小程序簽名者的對話框,由於area.jar 已經安裝。若是在不一樣的 Web 瀏覽器中打開 AreaDemo.html(假設兩個瀏覽器使用相同的 JRE),狀況也是如此。

1.3 擴展類加載機制

擴展框架利用了類加載委託機制。當運行時環境須要爲應用程序加載一個新類時,它會按順序在如下位置查找該類:

  1. Bootstrap 類:rt.jar 中的運行時類、i18n.jar 中的國際化類等。
  2. 已安裝的擴展(Installed extensions):JRE 的 lib/ext 目錄中的 JAR 文件中的類,以及系統範圍的、特定於平臺的擴展目錄(例如 Solaris™ 操做系統上的 /usr/jdk/packages/lib/ext,但請注意,此目錄的使用僅適用於 Java™ 6 及更高版本)。
  3. 類路徑(The class path):類,包括 JAR 文件中的類,位於系統屬性 java.class.path 指定的路徑上。若是類路徑上的 JAR 文件具備帶有 Class-Path 屬性的清單,則還將搜索由 Class-Path 屬性指定的 JAR 文件。默認狀況下,java.class.path 屬性的值爲 .,即當前目錄。您能夠經過使用 -classpath 或 -cp 命令行選項或設置 CLASSPATH 環境變量來更改該值。命令行選項會覆蓋 CLASSPATH 環境變量的設置。

例如,優先級列表會告訴您,僅當在 rt.jar、i18n.jar 或已安裝的擴展中的類中未找到要加載的類時,纔會搜索類路徑。

除非您的軟件出於特殊目的實例化本身的類加載器,不然您真的不須要了解更多信息,只需記住這個優先級列表便可。特別是,您應該注意可能存在的任何類名衝突。例如,若是您在類路徑上列出一個類,若是運行時環境加載另外一個與它在已安裝擴展中找到的同名類,您將獲得意想不到的結果。

Java 類加載機制

Java 平臺使用委託模型來加載類。基本思想是每一個類加載器都有一個「父」類加載器。加載類時,類加載器首先將對該類的搜索「委託」給其父類加載器,而後再嘗試查找該類自己。

如下是類加載 API 的一些亮點:

  • java.lang.ClassLoader 及其子類中的構造函數容許您在實例化新類加載器時指定父類。若是您沒有明確指定父級,虛擬機的系統類加載器將被分配爲默認父級。

  • ClassLoader 中的 loadClass 方法在調用加載類時按順序執行這些任務:

    1. 若是一個類已經被加載,它會返回它
    2. 不然,它將對新類的搜索委託給父類加載器
    3. 若是父類加載器沒有找到該類,則 loadClass調用 findClass方法查找並加載該類
  • 若是父類加載器未找到該類,則 ClassLoader 的 findClass 方法會在當前類加載器中搜索該類。當您在應用程序中實例化類加載器子類時,您可能但願覆蓋此方法。

  • 類 java.net.URLClassLoader 做爲擴展和其餘 JAR 文件的基本類加載器,覆蓋 java.lang.ClassLoader 的 findClass 方法來搜索一個或多個指定 URL 的類和資源。

要查看使用一些與 JAR 文件相關的 API 的示例應用程序,請參考Using JAR-related APIs

類加載和 java 命令

Java 平臺的類加載機制體如今 java 命令中。

  • 在 java 工具中,-classpath 選項是設置 java.class.path 屬性的一種簡寫方式。
  • -cp 和 -classpath 選項是等效的。
  • -jar 選項運行打包在 JAR 文件中的應用程序。有關此選項的說明和示例,請參考Running JAR-Packaged Software

1.4 建立可擴展的應用程序

可擴展應用程序是一種無需修改其原始代碼庫便可擴展的應用程序。您可使用新插件或模塊加強其功能。開發人員、軟件供應商和客戶能夠經過將新的 Java 存檔 (JAR) 文件添加到應用程序類路徑或特定於應用程序的擴展目錄中來添加新功能或應用程序編程接口 (API)。

本節介紹如何建立具備可擴展服務的應用程序,使您或其餘人可以提供不須要修改原始應用程序的服務實現。經過設計可擴展的應用程序,您能夠在不更改核心應用程序的狀況下升級或加強產品的特定部分。

可擴展應用程序的一個示例是文字處理器,它容許終端用戶添加新詞典或拼寫檢查器。在這個例子中,文字處理器提供了一個字典或拼寫功能,其餘開發人員,甚至客戶,能夠經過提供他們本身的功能實現來擴展。

如下是對理解可擴展應用程序很重要的術語和定義:

Service:

一組提供對某些特定應用程序功能或特性的訪問的編程接口和類。服務能夠定義功能的接口和檢索實現的方法。在字處理器示例中,字典服務能夠定義檢索字典和單詞定義的方法,但它沒有實現底層功能集。相反,它依賴於服務提供者來實現該功能。

Service provider interface (SPI):

服務定義的一組公共接口和抽象類。 SPI 定義了可用於您的應用程序的類和方法。

Service Provider:

實現 SPI。具備可擴展服務的應用程序,使您、供應商和客戶可以在不修改原始應用程序的狀況下添加服務提供商。

Dictionary Service示例程序

考慮如何在文字處理器或編輯器中設計字典服務。一種方法是定義由名爲 DictionaryService 的類和名爲 Dictionary 的服務提供者接口表示的服務。 DictionaryService 提供了一個單獨的 DictionaryService 對象。 (有關更多信息,請參閱單例設計模式部分。)此對象從 Dictionary 提供程序中檢索單詞的定義。詞典服務客戶端——您的應用程序代碼——檢索該服務的一個實例,該服務將搜索、實例化和使用詞典服務提供者。

儘管文字處理器開發人員極可能會提供原始產品的基本通用詞典,但客戶可能須要專門的詞典,其中可能包含法律或技術術語。理想狀況下,客戶可以建立或購買新詞典並將其添加到現有應用程序中。

DictionaryServiceDemo 示例向您展現瞭如何實現字典服務、建立添加附加字典的字典服務提供者,以及建立測試服務的簡單字典服務客戶端。此示例打包在 zip 文件DictionaryServiceDemo.zip中,包含如下文件:

可擴展應用示例程序目錄結構.png

注:build目錄包含同級src目錄下Java源文件的編譯類文件。

運行DictionaryServiceDemo示例程序

因爲 zip 文件 DictionaryServiceDemo.zip 包含已編譯的類文件,所以您能夠將此文件解壓縮到您的計算機並按照如下步驟運行示例,而無需編譯它:

  1. 下載並解壓示例代碼: 下載並解壓文件 DictionaryServiceDemo.zip 到您的計算機。這些步驟假定您將此文件的內容解壓縮到目錄 C:\DictionaryServiceDemo 中。
  2. 將當前目錄更改成 C:\DictionaryServiceDemo\DictionaryDemo 並按照運行客戶端進行操做。

編譯運行DictionaryServiceDemo示例程序

DictionaryServiceDemo 示例包括 Apache Ant 構建文件,這些文件都命名爲 build.xml。如下步驟向您展現瞭如何使用 Apache Ant 編譯、構建和運行 DictionaryServiceDemo 示例:

  1. 安裝 Apache Ant:轉到如下連接下載並安裝 Apache Ant:http://ant.apache.org/

    確保包含 Apache Ant 可執行文件的目錄在您的 PATH 環境變量中,以便您能夠從任何目錄運行它。此外,請確保您的 JDK 的 bin 目錄包含 java 和 javac 可執行文件(對於 Microsoft Windows,java.exe 和 javac.exe),位於您的 PATH 環境變量中。有關設置 PATH 環境變量的信息,請參閱 PATH and CLASSPATH

  2. 下載並解壓示例代碼: 下載並解壓文件 DictionaryServiceDemo.zip 到您的計算機。這些步驟假定您將此文件的內容解壓縮到目錄 C:\DictionaryServiceDemo 中。

  3. 編譯代碼: 將當前目錄更改成 C:\DictionaryServiceDemo 並運行命令:ant compile-all

    該命令編譯DictionaryDemo、DictionaryServiceProvider、ExtendedDictionary和GeneralDictionary目錄下src目錄下的源代碼,並將生成的類文件放到對應的build目錄下。

  4. 將編譯好的Java文件打包成JAR文件:確保當前目錄爲C:\DictionaryServiceDemo,運行命令:ant jar

    此命令會建立如下 JAR 文件:

    • DictionaryDemo/dist/DictionaryDemo.jar
    • DictionaryServiceProvider/dist/DictionaryServiceProvider.jar
    • GeneralDictionary/dist/GeneralDictionary.jar
    • ExtendedDictionary/dist/ExtendedDictionary.jar
  5. 運行示例:確保包含 java 可執行文件的目錄在您的 PATH 環境變量中。有關更多信息,請參閱PATH and CLASSPATH

    將當前目錄更改成 C:\DictionaryServiceDemo\DictionaryDemo 並運行如下命令:ant run

    該示例打印如下內容:

    book: a set of written or printed pages, usually bound with a protective cover
    editor: a person who edits
    xml: a document standard often used in web services, among other things
    REST: an architecture style for creating, reading, updating, and deleting data that attempts to use the common vocabulary of the HTTP protocol; Representational State Transfer

解析DictionaryServiceDemo工做原理

如下步驟向您展現如何從新建立文件 DictionaryServiceDemo.zip 的內容。這些步驟向您展現了示例的工做原理以及如何運行它。

(1). 定義Service Provider接口

DictionaryServiceDemo 示例定義了一個 SPI,即 Dictionary.java 接口。它只包含一種方法:

package dictionary.spi;

public interface Dictionary {
    public String getDefinition(String word);
}

該示例將編譯後的類文件存儲在目錄 DictionaryServiceProvider/build 中。

(2). 定義獲取Service Provider實現類的服務Service

DictionaryService.java 類表明字典服務客戶端,加載和訪問可用的字典服務提供者:

package dictionary;

import dictionary.spi.Dictionary;
import java.util.Iterator;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;

public class DictionaryService {

    private static DictionaryService service;
    private ServiceLoader<Dictionary> loader;

    private DictionaryService() {
        loader = ServiceLoader.load(Dictionary.class);
    }

    public static synchronized DictionaryService getInstance() {
        if (service == null) {
            service = new DictionaryService();
        }
        return service;
    }


    public String getDefinition(String word) {
        String definition = null;

        try {
            Iterator<Dictionary> dictionaries = loader.iterator();
            while (definition == null && dictionaries.hasNext()) {
                Dictionary d = dictionaries.next();
                definition = d.getDefinition(word);
            }
        } catch (ServiceConfigurationError serviceError) {
            definition = null;
            serviceError.printStackTrace();

        }
        return definition;
    }
}

該示例將編譯後的類文件存儲在目錄 DictionaryServiceProvider/build 中。

DictionaryService 類實現了單例設計模式。這意味着只建立了 DictionaryService 類的一個實例。有關更多信息,請參閱單例設計模式部分。

DictionaryService 類是字典服務客戶端使用任何已安裝的字典服務提供者的入口點。使用 ServiceLoader.load 方法檢索私有靜態成員 DictionaryService.service,即單例服務入口點。而後應用程序能夠調用 getDefinition 方法,該方法遍歷可用的字典提供程序,直到找到目標詞。若是沒有 Dictionary 實例包含該詞的定義,則 getDefinition 方法返回 null。

字典服務使用 ServiceLoader.load 方法來查找目標類。 SPI 由接口 dictionary.spi.Dictionary 定義,所以該示例使用此類做爲加載方法的參數。默認加載方法使用默認類加載器搜索應用程序類路徑。

可是,此方法的重載版本使您能夠根據須要指定自定義類加載器。這使您可以進行更復雜的類搜索。例如,一個特別熱情的程序員可能會建立一個 ClassLoader 實例,該實例能夠在特定於應用程序的子目錄中進行搜索,該子目錄包含在運行時添加的提供程序 JAR。結果是應用程序不須要從新啓動便可訪問新的提供程序類。

有了類加載器,您可使用它的迭代器方法找到的每一個提供程序。 getDefinition 方法使用 Dictionary 迭代器遍歷提供程序,直到找到指定單詞的定義。迭代器方法緩存 Dictionary 實例,所以連續調用幾乎不須要額外的處理時間。若是自上次調用以來已將新的提供者置於服務中,則迭代器方法會將它們添加到列表中。

DictionaryDemo.java 類使用此服務。爲了使用該服務,應用程序獲取一個 DictionaryService 實例並調用 getDefinition 方法。若是定義可用,應用程序將打印它。若是定義不可用,應用程序會打印一條消息,指出沒有可用的字典帶有該詞。

單例模式

設計模式是軟件設計中常見問題的通用解決方案。這個想法是將解決方案轉換爲代碼,而且該代碼能夠應用於出現問題的不一樣狀況。單例模式描述了一種確保只建立一個類的單個實例的技術。本質上,該技術採用如下方法:不要讓類以外的任何人建立對象的實例。

例如, DictionaryService 類實現單例模式以下:

  • 將 DictionaryService 構造函數聲明爲私有,這會阻止除 DictionaryService 以外的全部其餘類建立它的實例。
  • 將 DictionaryService 成員變量 service 定義爲靜態,以確保僅存在 DictionaryService 的一個實例。
  • 定義方法 getInstance,該方法容許其餘類對 DictionaryService 成員變量服務進行受控訪問。

(3). 實現Service Provider

要提供此服務,您必須建立一個 Dictionary.java 實現。爲簡單起見,建立一個僅定義幾個單詞的通用詞典。您可使用數據庫、一組屬性文件或任何其餘技術來實現字典。演示提供者模式的最簡單方法是在單個文件中包含全部單詞和定義。

如下代碼顯示了 Dictionary SPI 的實現,即 GeneralDictionary.java 類。請注意,它提供了一個無參構造函數並實現了 SPI 定義的 getDefinition 方法。

package dictionary;

import dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;

public class GeneralDictionary implements Dictionary {

    private SortedMap<String, String> map;
    
    public GeneralDictionary() {
        map = new TreeMap<String, String>();
        map.put(
            "book",
            "a set of written or printed pages, usually bound with " +
                "a protective cover");
        map.put(
            "editor",
            "a person who edits");
    }

    @Override
    public String getDefinition(String word) {
        return map.get(word);
    }

}

該示例將編譯後的類文件存儲在 GeneralDictionary/build 目錄中。注意:您必須在類 GeneralDictionary 以前編譯 dictionary.DictionaryService 和 dictionary.spi.Dictionary 類。

本示例的 GeneralDictionary 提供程序只定義了兩個詞:book 和 editor。顯然,更實用的詞典將提供更大量的經常使用詞彙表。

爲了演示多個提供程序如何實現相同的 SPI,如下代碼顯示了另外一個可能的提供程序。ExtendedDictionary.java 服務提供者是一個擴展字典,其中包含大多數軟件開發人員熟悉的技術術語。

package dictionary;

import dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;

public class ExtendedDictionary implements Dictionary {

        private SortedMap<String, String> map;

    public ExtendedDictionary() {
        map = new TreeMap<String, String>();
        map.put(
            "xml",
            "a document standard often used in web services, among other " +
                "things");
        map.put(
            "REST",
            "an architecture style for creating, reading, updating, " +
                "and deleting data that attempts to use the common " +
                "vocabulary of the HTTP protocol; Representational State " +
                "Transfer");
    }

    @Override
    public String getDefinition(String word) {
        return map.get(word);
    }

}

該示例將編譯後的類文件存儲在目錄 ExtendedDictionary/build 中。注意:您必須在類 ExtendedDictionary 以前編譯 dictionary.DictionaryService 和 dictionary.spi.Dictionary 類。

很容易想象客戶使用一套完整的字典提供程序來知足他們本身的特殊需求。服務加載器 API 使他們可以在他們的需求或偏好發生變化時將新詞典添加到他們的應用程序中。由於底層的文字處理器應用程序是可擴展的,因此客戶使用新的提供程序不須要額外的編碼。

(4). 註冊Service Providers

要註冊您的服務提供者,您須要建立一個提供者配置文件,該文件存儲在服務提供者的 JAR 文件的 META-INF/services 目錄中。配置文件的名稱是服務提供者的全限定類名,其中名稱的每一個組成部分用句點(.)分隔,嵌套的類用美圓符號($)分隔。

提供者配置文件包含服務提供者的徹底限定類名,每行一個名稱。該文件必須採用 UTF-8 編碼。此外,您能夠經過以數字符號 (#) 開頭的註釋行來在文件中包含註釋。

例如,要註冊服務提供者 GeneralDictionary,請建立一個名爲 dictionary.spi.Dictionary 的文本文件。該文件包含一行:

dictionary.GeneralDictionary

一樣,要註冊服務提供者 ExtendedDictionary,請建立一個名爲 dictionary.spi.Dictionary 的文本文件。該文件包含一行:

dictionary.ExtendedDictionary

(5). 建立一個使用Service和Service Provider的客戶端

由於開發一個完整的文字處理器應用程序是一項重要的任務,因此本教程提供了一個使用 DictionaryService 和 Dictionary SPI 的更簡單的應用程序。 DictionaryDemo 示例從類路徑上的任何 Dictionary 提供程序中搜索詞 book、editor、xml 和 REST 詞並檢索它們的定義。

如下是 DictionaryDemo 示例。它從 DictionaryService 實例請求目標詞的定義,該實例將請求傳遞給其已知的 Dictionary 提供程序。

package dictionary;

import dictionary.DictionaryService;

public class DictionaryDemo {

  public static void main(String[] args) {

    DictionaryService dictionary = DictionaryService.getInstance();
    System.out.println(DictionaryDemo.lookup(dictionary, "book"));
    System.out.println(DictionaryDemo.lookup(dictionary, "editor"));
    System.out.println(DictionaryDemo.lookup(dictionary, "xml"));
    System.out.println(DictionaryDemo.lookup(dictionary, "REST"));
  }

  public static String lookup(DictionaryService dictionary, String word) {
    String outputString = word + ": ";
    String definition = dictionary.getDefinition(word);
    if (definition == null) {
      return outputString + "Cannot find definition for this word.";
    } else {
      return outputString + definition;
    }
  }
}

示例將編譯後的類文件存放在目錄 DictionaryDemo/build 中。注意:您必須在類 DictionaryDemo 以前編譯類 dictionary.DictionaryService 和 dictionary.spi.Dictionary。

(6). 將Service Provider, Service, ServiceClient打包成JAR文件

有關如何建立 JAR 文件的信息,請參閱Packaging Programs in JAR Files

打包Service Provider

打包GeneralDictionary 服務提供者,建立一個名爲GeneralDictionary/dist/GeneralDictionary.jar 的JAR 文件,其中包含該服務提供者的編譯類文件和如下目錄結構中的配置文件:

GeneralDictionary

同理,打包ExtendedDictionary服務提供者,建立一個名爲ExtendedDictionary/dist/ExtendedDictionary.jar的JAR文件,其中包含該服務提供者編譯後的類文件和配置文件,目錄結構以下:

image.png

請注意,提供程序配置文件必須位於 JAR 文件中的 META-INF/services 目錄中。

打包 Dictionary SPI 和 Dictionary Service

建立一個名爲 DictionaryServiceProvider/dist/DictionaryServiceProvider.jar 的 JAR 文件,其中包含如下文件:

image.png

打包客戶端

建立一個名爲 DictionaryDemo/dist/DictionaryDemo.jar 的 JAR 文件,其中包含如下文件:

image.png

(7). 運行客戶端

Linux and Solaris:

java -Djava.ext.dirs=../DictionaryServiceProvider/dist:../GeneralDictionary/dist -cp dist/DictionaryDemo.jar dictionary.DictionaryDemo

Windows:

java -Djava.ext.dirs=..\DictionaryServiceProvider\dist;..\GeneralDictionary\dist -cp dist\DictionaryDemo.jar dictionary.DictionaryDemo

使用此命令時,假設以下:

  • 當前目錄是 DictionaryDemo。
  • 存在如下 JAR 文件:
    • DictionaryDemo/dist/DictionaryDemo.jar: 包含 DictionaryDemo
    • DictionaryServiceProvider/dist/DictionaryServiceProvider.jar: 包含 Dictionary SPI and the DictionaryService
    • GeneralDictionary/dist/GeneralDictionary.jar: 包含 GeneralDictionary service provider 和配置文件

該命令打印如下內容:

book: a set of written or printed pages, usually bound with a protective cover
editor: a person who edits
xml: Cannot find definition for this word.
REST: Cannot find definition for this word.

假設您運行如下命令而且 ExtendedDictionary/dist/ExtendedDictionary.jar 存在:

Linux and Solaris:

java -Djava.ext.dirs=../DictionaryServiceProvider/dist:../ExtendedDictionary/dist -cp dist/DictionaryDemo.jar dictionary.DictionaryDemo

Windows:

java -Djava.ext.dirs=..\DictionaryServiceProvider\dist;..\ExtendedDictionary\dist -cp dist\DictionaryDemo.jar dictionary.DictionaryDemo

該命令打印如下內容:

book: Cannot find definition for this word.
editor: Cannot find definition for this word.
xml: a document standard often used in web services, among other things
REST: an architecture style for creating, reading, updating, and deleting data that attempts to use the common vocabulary of the HTTP protocol; Representational State Transfer

ServiceLoader類

java.util.ServiceLoader 類可幫助您查找、加載和使用服務提供者。它在應用程序的類路徑或運行時環境的擴展目錄中搜索服務提供者。它加載它們並使您的應用程序可以使用提供者的 API。若是將新提供程序添加到類路徑或運行時擴展目錄,ServiceLoader 類會找到它們。若是您的應用程序知道提供者接口,它就能夠找到並使用該接口的不一樣實現。您可使用接口的第一個可加載實例或遍歷全部可用接口。

ServiceLoader 類是final類,這意味着您不能將其設爲子類或覆蓋其加載算法。例如,您不能更改其算法以從不一樣位置搜索服務。

從 ServiceLoader 類的角度來看,全部服務都有一個類型,一般是單個接口或抽象類。提供者自己包含一個或多個具體類,這些類使用特定於其目的的實現來擴展服務類型。 ServiceLoader 類要求單個公開的提供程序類型具備默認構造函數,該構造函數不須要參數。這使 ServiceLoader 類可以輕鬆實例化它找到的服務提供者。

提供者按需定位和實例化。ServiceLoader維護已加載的提供程序的緩存。加載器的迭代器方法的每次調用都會返回一個迭代器,該迭代器首先按實例化順序生成緩存的全部元素。而後ServiceLoader定位並實例化任何新的提供者,依次將每一個提供者添加到緩存中。您可使用 reload 方法清除提供程序緩存。

要爲特定類建立加載器,請將類自己提供給 load 或 loadInstalled 方法。您可使用默認類加載器或提供您本身的 ClassLoader 子類。

loadInstalled 方法搜索已安裝的運行時提供程序的運行時環境的擴展目錄。默認擴展位置是運行時環境的 jre/lib/ext 目錄。您應該僅將擴展位置用於衆所周知的、受信任的提供程序,由於此位置成爲全部應用程序類路徑的一部分。在本文中,提供程序不使用擴展目錄,而是依賴於特定於應用程序的類路徑。

ServiceLoader API的限制

ServiceLoader API 頗有用,但它有侷限性。例如,沒法從 ServiceLoader 類派生類,所以您沒法修改其行爲。您可使用自定義 ClassLoader 子類來更改查找類的方式,但 ServiceLoader 自己沒法擴展。此外,當前的 ServiceLoader 類沒法告訴您的應用程序什麼時候有新的提供程序在運行時可用。此外,您沒法向加載程序添加更改偵聽器以查明是否將新提供程序放入特定於應用程序的擴展目錄中。

公共 ServiceLoader API 在 Java SE 6 中可用。儘管加載器服務早在 JDK 1.3 中就存在,但該 API 是私有的,僅對內部 Java 運行時代碼可用。

總結

可擴展應用程序提供可由服務提供商擴展的服務點。建立可擴展應用程序的最簡單方法是使用 ServiceLoader,它可用於 Java SE 6 及更高版本。使用此類,您能夠將提供程序實現添加到應用程序類路徑,以使新功能可用。 ServiceLoader 類被定義成final,因此你不能修改它的能力。

2. 擴展Extensions的安全性

如今您已經瞭解瞭如何使用擴展,您可能想知道擴展具備哪些安全權限。例如,若是您正在開發執行文件 I/O 的擴展,您須要知道您的擴展如何被授予讀取和寫入文件的適當權限。相反,若是您正在考慮使用其餘人開發的擴展程序,您須要清楚地瞭解該擴展程序具備哪些安全權限,以及若是您但願這樣作,如何更改這些權限。

本節課程向您展現 Java™ 平臺的安全架構如何處理擴展。您將看到如何告訴擴展軟件被授予了哪些權限,而且您將經過一些簡單的步驟學習如何修改擴展權限。此外,您將學習如何在擴展中密封包以限制對代碼指定部分的訪問。

有關安全性的完整信息,您能夠參考如下內容:

2.1 爲擴展設置權限

若是安全管理器Security Manager 生效,則必須知足如下條件才能使任何軟件(包括擴展軟件)執行安全敏感操做:

  • 擴展中的安全敏感代碼必須包裝在 PrivilegedAction 對象中。
  • 安全管理器實施的安全策略必須授予擴展適當的權限。默認狀況下,已安裝的擴展被授予全部安全權限,就好像它們是核心平臺 API 的一部分同樣。安全策略授予的權限僅適用於封裝在 PrivilegedAction 實例中的代碼。

讓咱們經過一些示例更詳細地瞭解這些條件。

使用 PrivilegedAction 類

假設您要修改上一課擴展現例中的 RectangleArea 類,將矩形區域寫入文件而不是標準輸出。然而,寫入文件是一項安全敏感的操做,所以若是您的軟件在安全管理器下運行,您須要將您的代碼標記爲有特權的。爲此,您須要執行兩個步驟:

  1. 您須要將執行安全敏感操做的代碼放置在 java.security.PrivilegedAction 類型的對象的 run 方法中
  2. 您必須使用該 PrivilegedAction 對象做爲調用 java.security.AccessController 的 doPrivileged 方法的參數

若是咱們將這些準則應用於 RectangleArea 類,咱們的類定義將以下所示:

import java.io.*;
import java.security.*;

public final class RectangleArea {
    public static void
    writeArea(final java.awt.Rectangle r) {
        AccessController.
          doPrivileged(new PrivilegedAction() {
            public Object run() {
                try { 
                    int area = r.width * r.height;
                    String userHome = System.getProperty("user.home");
                    FileWriter fw = new FileWriter( userHome + File.separator
                        + "test" + File.separator + "area.txt");
                    fw.write("The rectangle's area is " + area);
                    fw.flush();
                    fw.close();
                } catch(IOException ioe) {
                    System.err.println(ioe);
                }
                return null;
            }
        });
    }
}

此類中的單個方法 writeArea 計算矩形的面積,並將該面積寫入用戶主目錄下 test 目錄中名爲 area.txt 的文件中。

處理輸出文件的安全敏感語句放置在 PrivilegedAction 實例的 run 方法中。(請注意,run 要求返回一個 Object 實例,返回的對象能夠爲 null)而後將新的 PrivilegedAction 實例做爲參數傳遞給 AccessController.doPrivileged。

有關使用 doPrivileged 的更多信息,請參閱 JDK™ 文檔中的 API for Privileged Blocks

以這種方式將安全敏感代碼包裝在 PrivilegedAction 對象中是啓用擴展執行安全敏感操做的第一個要求,第二個要求是:讓安全管理器授予特權代碼適當的權限。

使用安全策略指定權限

運行時有效的安全策略由策略文件指定,默認策略由 JRE 軟件中的文件 lib/security/java.policy 設置。

策略文件經過使用受權條目爲軟件分配安全權限。策略文件能夠包含任意數量的受權條目。對於已安裝的擴展,默認策略文件具備如下受權條目:

grant codeBase "file:${{java.ext.dirs}}/*" {
    permission java.security.AllPermission;
};

此項指定由 file:${{java.ext.dirs}}/* 指定的目錄中的文件將被授予名爲 java.security.AllPermission 的權限。 (請注意,從 Java 6 開始,java.ext.dirs 指的是類路徑類的目錄路徑,每一個目錄均可以保存已安裝的擴展。)不難猜想 java.security.AllPermission 爲已安裝的擴展授予全部安全性能夠授予的特權。

默認狀況下,已安裝的擴展沒有安全限制。擴展軟件能夠執行安全敏感操做,就像沒有安裝安全管理器同樣,前提是安全敏感代碼包含在做爲 doPrivileged 調用中的參數傳遞的 PrivilegedAction 實例中。

要限制授予擴展的權限,您須要修改策略文件。要拒絕全部擴展的全部權限,您能夠簡單地刪除上述受權條目。

並不是全部權限都像默認授予的 java.security.AllPermission 同樣全面。刪除默認受權條目後,您能夠爲特定權限輸入新的受權條目,包括:

  • java.awt.AWTPermission
  • java.io.FilePermission
  • java.net.NetPermission
  • java.util.PropertyPermission
  • java.lang.reflect.ReflectPermission
  • java.lang.RuntimePermission
  • java.security.SecurityPermission
  • java.io.SerializablePermission
  • java.net.SocketPermission

JDK 文檔中的權限( Permissions in the JDK)提供了有關每一個權限的詳細信息。讓咱們看一下使用 RectangleArea 做爲擴展所需的那些。

RectangleArea.writeArea 方法須要兩種權限:一種用於肯定用戶主目錄的路徑,另外一種用於寫入文件。假設 RectangleArea 類捆綁在文件 area.jar 中,您能夠經過將此條目添加到策略文件來授予寫入權限:

grant codeBase "file:${java.home}/lib/ext/area.jar" {
    permission java.io.PropertyPermission "user.home",
        "read";
    permission java.io.FilePermission
        "${user.home}${/}test${/}*", "write";
};

此條目的代碼庫file:${java.home}/lib/ext/area.jar部分保證此條目指定的任何權限僅適用於 area.jar。 java.io.PropertyPermission 容許訪問屬性,第一個參數user.home爲屬性命名,第二個參數read表示能夠讀取該屬性(另外一個選擇是write

java.io.FilePermission 容許訪問文件,第一個參數${user.home}${/}test${/}*表示 area.jar 被授予訪問用戶主目錄中 test 目錄中全部文件的權限 (請注意,${/} 是與平臺無關的文件分隔符。),第二個參數表示授予的文件訪問權限僅用於寫入(第二個參數的其餘選擇是讀取刪除執行)。

簽名擴展

您可使用策略文件對授予擴展的權限施加額外限制,要求它們由受信任的實體簽名(有關簽名和驗證 JAR 文件的評論,請參閱本教程中的簽名 JAR 文件課程)

爲了容許在授予權限的同時對擴展或其餘軟件進行簽名驗證,策略文件必須包含一個密鑰庫(keystore)條目,密鑰庫(keystore)條目指定在驗證中使用哪一個密鑰庫,密鑰庫條目具備如下形式

keystore "keystore_url";

URL keystore_url 是絕對的或相對的。若是是相對的,則 URL 與策略文件的位置相關。例如,要使用 keytool 使用的默認密鑰庫,請將此條目添加到 java.policy

keystore "file://${user.home}/.keystore";

要指示擴展必須簽名才能被授予安全權限,請使用 signedBy 字段。例如,如下條目指示擴展 area.jar 僅在由別名 Robert 和 Rita 在密鑰庫中標識的用戶簽名時才被授予列出的權限:

grant signedBy "Robert,Rita",
    codeBase "file:${java.home}/lib/ext/area.jar" {
        permission java.io.PropertyPermission
            "user.home", "read";
        permission java.io.FilePermission
            "${user.home}${/}test${/}*", "write";
};

若是省略 codeBase 字段,以下面的「grant」所示,權限將授予任何由「Robert」或「Rita」簽名的軟件,包括已安裝或下載的擴展:

grant signedBy "Robert,Rita" {
    permission java.io.FilePermission "*", "write";  
};

有關策略文件格式的更多詳細信息,請參閱 JDK 文檔中安全架構規範(Security Architecture Specification) 的 3.3.1 節。

2.2 擴展中的密封包

您能夠選擇在擴展 JAR 文件中密封包做爲額外的安全措施,若是包是密封的,則該包中定義的全部類都必須源自單個 JAR 文件。

若是沒有密封,「敵對」程序能夠建立一個類並將其定義爲您的擴展包之一的成員。而後,惡意軟件能夠免費訪問擴展包中受包保護的成員。

在擴展中密封包與密封任何 JAR 打包的類沒有什麼不一樣。要密封您的擴展包,您必須將 Sealed 標頭添加到包含您的擴展的 JAR 文件的清單中,您能夠經過將 Sealed 標頭與包的 Name 標頭相關聯來密封單個包。與存檔中的單個包無關的 Sealed 標頭表示全部包都已密封,這種「全局」密封標頭被與單個包關聯的任何密封標頭覆蓋,與 Sealed 標頭關聯的值是 true 或 false。

示例

讓咱們看一些示例清單文件。對於這些示例,假設 JAR 文件包含如下包:

com/myCompany/package_1/
com/myCompany/package_2/
com/myCompany/package_3/
com/myCompany/package_4/

假設您要密封全部package。您能夠經過簡單地向清單添加一個存檔級別的 Sealed 標頭來實現,以下所示:

Manifest-Version: 1.0
Sealed: true

具備此清單的任何 JAR 文件中的全部包都將被密封。

若是您只想密封 com.myCompany.package_3,您可使用如下清單:

Manifest-Version: 1.0

Name: com/myCompany/package_3/
Sealed: true

在此示例中,惟一的 Sealed 標頭與包 com.myCompany.package_3 的 Name 標頭相關聯,所以僅密封該包。 (密封標頭與名稱標頭相關聯,由於它們之間沒有空行。)

最後一個示例,假設您要密封除 com.myCompany.package_2 以外的全部包,你能夠用這樣的清單來完成:

Manifest-Version: 1.0
Sealed: true

Name: com/myCompany/package_2/
Sealed: false

在此示例中,存檔級別 Sealed: true 標頭表示 JAR 文件中的全部包都將被密封,可是清單還有一個 Sealed: false 標頭與包 com.myCompany.package_2 相關聯,而且該標頭會覆蓋該包的存檔級密封。所以,此清單將致使除 com.myCompany.package_2 以外的全部包都被密封。

相關文章
相關標籤/搜索