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 以外的全部包都被密封。

相关文章
相关标签/搜索