本文翻译自Oracle官网(原文地址)
扩展机制提供了一种标准的、可扩展的方式,使 Java 平台上运行的所有应用程序都可以使用自定义 API。 Java 扩展也称为可选包。
扩展是一组包和类,它们通过扩展机制增强 Java 平台。扩展机制使运行时环境能够查找和加载扩展类,而不必在类路径上命名扩展类。在这方面,扩展类类似于 Java 平台的核心类。这也是扩展名的由来——它们实际上扩展了平台的核心 API。
由于此机制扩展了平台的核心 API,因此应谨慎使用它。它最常用于标准化的接口,例如 Java Community Process 定义的接口,尽管它也可能适用于站点范围的接口。
如图所示,扩展充当 Java 平台的“附加”模块。它们的类和公共 API 可自动用于平台上运行的任何应用程序。
扩展机制还提供了一种从远程位置下载扩展类以供小程序使用的方法。
扩展被捆绑为 Java 存档 (JAR) 文件,如果您不了解 JAR 文件,您可能需要在继续本教程中的课程之前查看一些 JAR 文件文档:
- The Packaging Programs in JAR Files lesson in this tutorial.
- The JAR Guide in the JDK™ documentation.
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 软件目录树的子集如下所示:
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%SunJavalibext
请注意,放置在上述目录之一中的已安装扩展扩展了该系统上每个 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 扩展类加载机制
扩展框架利用了类加载委托机制。当运行时环境需要为应用程序加载一个新类时,它会按顺序在以下位置查找该类:
- Bootstrap 类:rt.jar 中的运行时类、i18n.jar 中的国际化类等。
- 已安装的扩展(Installed extensions):JRE 的 lib/ext 目录中的 JAR 文件中的类,以及系统范围的、特定于平台的扩展目录(例如 Solaris™ 操作系统上的 /usr/jdk/packages/lib/ext,但请注意,此目录的使用仅适用于 Java™ 6 及更高版本)。
- 类路径(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 方法在调用加载类时按顺序执行这些任务:
- 如果一个类已经被加载,它会返回它
- 否则,它将对新类的搜索委托给父类加载器
- 如果父类加载器没有找到该类,则
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中,包含以下文件:
注:build目录包含同级src目录下Java源文件的编译类文件。
运行DictionaryServiceDemo示例程序
由于 zip 文件 DictionaryServiceDemo.zip 包含已编译的类文件,因此您可以将此文件解压缩到您的计算机并按照以下步骤运行示例,而无需编译它:
- 下载并解压示例代码: 下载并解压文件 DictionaryServiceDemo.zip 到您的计算机。这些步骤假定您将此文件的内容解压缩到目录 C:DictionaryServiceDemo 中。
- 将当前目录更改为 C:DictionaryServiceDemoDictionaryDemo 并按照运行客户端进行操作。
编译运行DictionaryServiceDemo示例程序
DictionaryServiceDemo 示例包括 Apache Ant 构建文件,这些文件都命名为 build.xml。以下步骤向您展示了如何使用 Apache Ant 编译、构建和运行 DictionaryServiceDemo 示例:
-
安装 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
-
下载并解压示例代码: 下载并解压文件 DictionaryServiceDemo.zip 到您的计算机。这些步骤假定您将此文件的内容解压缩到目录 C:DictionaryServiceDemo 中。
-
编译代码: 将当前目录更改为 C:DictionaryServiceDemo 并运行命令:
ant compile-all
该命令编译DictionaryDemo、DictionaryServiceProvider、ExtendedDictionary和GeneralDictionary目录下src目录下的源代码,并将生成的类文件放到对应的build目录下。
-
将编译好的Java文件打包成JAR文件:确保当前目录为C:DictionaryServiceDemo,运行命令:
ant jar
此命令会创建以下 JAR 文件:
DictionaryDemo/dist/DictionaryDemo.jar
DictionaryServiceProvider/dist/DictionaryServiceProvider.jar
GeneralDictionary/dist/GeneralDictionary.jar
ExtendedDictionary/dist/ExtendedDictionary.jar
-
运行示例:确保包含 java 可执行文件的目录在您的 PATH 环境变量中。有关更多信息,请参阅PATH and CLASSPATH
将当前目录更改为 C:DictionaryServiceDemoDictionaryDemo 并运行以下命令:
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 文件,其中包含该服务提供者的编译类文件和以下目录结构中的配置文件:
同理,打包ExtendedDictionary服务提供者,创建一个名为ExtendedDictionary/dist/ExtendedDictionary.jar的JAR文件,其中包含该服务提供者编译后的类文件和配置文件,目录结构如下:
请注意,提供程序配置文件必须位于 JAR 文件中的 META-INF/services 目录中。
打包 Dictionary SPI 和 Dictionary Service
创建一个名为 DictionaryServiceProvider/dist/DictionaryServiceProvider.jar 的 JAR 文件,其中包含以下文件:
打包客户端
创建一个名为 DictionaryDemo/dist/DictionaryDemo.jar 的 JAR 文件,其中包含以下文件:
(7). 运行客户端
Linux and Solaris:
java -Djava.ext.dirs=../DictionaryServiceProvider/dist:../GeneralDictionary/dist -cp dist/DictionaryDemo.jar dictionary.DictionaryDemo
Windows:
java -Djava.ext.dirs=..DictionaryServiceProviderdist;..GeneralDictionarydist -cp distDictionaryDemo.jar dictionary.DictionaryDemo
使用此命令时,假设如下:
- 当前目录是 DictionaryDemo。
- 存在以下 JAR 文件:
DictionaryDemo/dist/DictionaryDemo.jar
: 包含DictionaryDemo
类DictionaryServiceProvider/dist/DictionaryServiceProvider.jar
: 包含Dictionary
SPI and theDictionaryService
类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=..DictionaryServiceProviderdist;..ExtendedDictionarydist -cp distDictionaryDemo.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™ 平台的安全架构如何处理扩展。您将看到如何告诉扩展软件被授予了哪些权限,并且您将通过一些简单的步骤学习如何修改扩展权限。此外,您将学习如何在扩展中密封包以限制对代码指定部分的访问。
有关安全性的完整信息,您可以参考以下内容:
- Security Features in Java SE trail (in this tutorial)
- Security guide
2.1 为扩展设置权限
如果安全管理器Security Manager 生效,则必须满足以下条件才能使任何软件(包括扩展软件)执行安全敏感操作:
- 扩展中的安全敏感代码必须包装在 PrivilegedAction 对象中。
- 安全管理器实施的安全策略必须授予扩展适当的权限。默认情况下,已安装的扩展被授予所有安全权限,就好像它们是核心平台 API 的一部分一样。安全策略授予的权限仅适用于封装在 PrivilegedAction 实例中的代码。
让我们通过一些示例更详细地了解这些条件。
使用 PrivilegedAction 类
假设您要修改上一课扩展示例中的 RectangleArea 类,将矩形区域写入文件而不是标准输出。然而,写入文件是一项安全敏感的操作,因此如果您的软件在安全管理器下运行,您需要将您的代码标记为有特权的。为此,您需要执行两个步骤:
- 您需要将执行安全敏感操作的代码放置在 java.security.PrivilegedAction 类型的对象的 run 方法中
- 您必须使用该 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 之外的所有包都被密封。