文档

Java™ 教程-Java Tutorials 中文版
创建可扩展的应用程序
Trail: The Extension Mechanism
Lesson: Creating and Using Extensions

创建可扩展的应用程序

涵盖以下主题:

介绍

extensible (可扩展) 应用程序是你可以在不修改其原始代码库的情况下扩展的应用程序。你可以使用新的插件或模块增强其功能。开发人员,软件供应商和客户可以通过将新的 Java Archive(JAR)文件添加到应用程序类路径或特定于应用程序的扩展目录中来添加新功能或应用程序编程接口(API)。

本节介绍如何使用可扩展服务创建应用程序,使你或其他人能够提供不需要修改原始应用程序的服务实现。通过设计可扩展的应用程序,你可以在不更改核心应用程序的情况下提供升级或增强产品特定部分的方法。

可扩展应用程序的一个示例是允许终端用户添加新字典或拼写检查器的单词处理器。在此示例中,单词处理器提供字典或拼写功能,其他开发人员甚至客户可以通过提供他们自己的功能实现来扩展。

以下是了解可扩展应用程序的重要术语和定义:

服务
一组编程接口和类,提供对某些特定应用程序功能或特性的访问。该服务可以定义功能的接口和获取实现的方法。在单词处理器示例中,字典服务可以定义获取字典和单词定义的方法,但它不实现底层特征集。相反,它依赖于 service provider (服务提供者) 来实现该功能。
服务提供者接口(SPI)
服务定义的公共接口和抽象类的集合。SPI 定义了应用程序可用的类和方法。
服务提供者
实现 SPI。具有可扩展服务的应用程序使你,供应商和客户能够在不修改原始应用程序的情况下添加服务提供者。

字典服务示例

考虑如何在单词处理器或编辑器中设计字典服务。一种方法是定义由名为 DictionaryService 的类和名为 Dictionary 的服务提供者接口表示的服务。DictionaryService 提供单例 DictionaryService 对象。(有关详细信息,请参阅 The Singleton Design Pattern 一节。)此对象从 Dictionary 提供者中获取单词的定义。字典服务客户端 - 你的应用程序代码 - 获取此服务的实例,该服务将搜索,实例化和使用 Dictionary 服务提供者。

虽然单词处理开发人员很可能会提供原始产品的基本通用字典,但客户可能需要专门的字典,可能包含法律或技术术语。理想情况下,客户可以创建或购买新字典并将其添加到现有应用程序中。

DictionaryServiceDemo 示例演示如何实现 Dictionary 服务,创建添加其他字典的 Dictionary 服务提供者,以及创建一个简单的 Dictionary 服务客户端来测试服务。此示例包装在 zip 文件 DictionaryServiceDemo.zip 中,包含以下文件:

注意:build 目录包含同一级别的 src 目录中包含的 Java 源文件的已编译类文件。

运行 DictionaryServiceDemo 示例

由于 zip 文件 DictionaryServiceDemo.zip 包含已编译的类文件,因此你可以通过以下步骤将此文件解压缩到你的计算机并运行示例而不用编译它:

  1. 下载并解压缩示例代码:将文件 DictionaryServiceDemo.zip 下载并解压缩到你的计算机。这些步骤假设你将此文件的内容解压缩到目录 C:\DictionaryServiceDemo 中。

  2. 将当前目录更改为 C:\DictionaryServiceDemo\DictionaryDemo,然后执行步骤 Run the Client

编译并运行 DictionaryServiceDemo 示例

DictionaryServiceDemo 示例包含 Apache Ant 构建文件,这些文件都名为 build.xml。以下步骤说明如何使用 Apache Ant 编译,构建和运行 DictionaryServiceDemo 示例:

  1. 安装 Apache Ant:转到以下链接下载并安装 Apache Ant:

    http://ant.apache.org/

    确保包含 Apache Ant 可执行文件的目录位于 PATH 环境变量中,以便可以从任何目录运行它。此外,确保你的 JDK 的 bin 目录,其中包含 javajavac 可执行文件(java.exejavac.exe 用于 Microsoft Windows)。在你的 PATH 环境变量中。有关设置 PATH 环境变量的信息,请参见 PATH and CLASSPATH

  2. 下载并解压缩示例代码:将文件 DictionaryServiceDemo.zip 下载并解压缩到你的计算机。这些步骤假定你将此文件的内容解压缩到目录 C:\DictionaryServiceDemo 中。

  3. 编译代码:将当前目录更改为 C:\DictionaryServiceDemo 并运行以下命令:

    ant compile-all

    此命令编译目录 DictionaryDemoDictionaryServiceProviderExtendedDictionary 以及 GeneralDictionary 中包含的 src 目录中的源代码,并将生成的 class 文件放在相应的 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. 定义服务提供者接口

DictionaryServiceDemo 示例定义了一个 SPI,Dictionary.java 接口。它只包含一种方法:


package dictionary.spi;

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

该示例将已编译的类文件存储在目录 DictionaryServiceProvider/build 中。

2. 定义获取服务提供者实现的服务

DictionaryService.java 类代表字典服务客户端加载和访问可用的 Dictionary 服务提供者:


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 类的单个实例。有关详细信息,请参阅 The Singleton Design Pattern 部分。

DictionaryService 类是字典服务客户端使用任何已安装的 Dictionary 服务提供者的入口点。使用 ServiceLoader.load 方法获取私有静态成员 DictionaryService.service,即单例服务入口点。然后应用程序可以调用 getDefinition 方法,该方法迭代可用的 Dictionary 提供程序,直到找到目标单词。如果没有 Dictionary 实例包含指定的单词定义,则 getDefinition 方法返回 null。

字典服务使用 ServiceLoader.load 方法来查找目标类。SPI 由接口 dictionary.spi.Dictionary 定义,因此该示例使用此类作为加载方法的参数。默认的 load 方法使用默认的类加载器搜索应用程序类路径。

但是,如果你愿意,可以使用此方法的重载版本来指定自定义类加载器。这使你可以进行更复杂的类搜索。例如,一个特别热心的程序员可能会创建一个 ClassLoader 实例,该实例可以在特定于应用程序的子目录中搜索,该子目录包含在运行时添加的提供程序 JAR。结果是一个不需要重新启动来访问新提供程序类的应用程序。

存在此类的加载器后,你可以使用其迭代器方法来访问和使用它找到的每个提供程序。getDefinition 方法使用 Dictionary 迭代器遍历提供程序,直到找到指定单词的定义。迭代器方法缓存 Dictionary 实例,因此连续调用几乎不需要额外的处理时间。如果自上次调用以来已将新提供程序置于服务中,则迭代器方法会将它们添加到列表中。

DictionaryDemo.java 类使用此服务。要使用该服务,应用程序将获取 DictionaryService 实例并调用 getDefinition 方法。如果定义可用,应用程序将打印它。如果定义不可用,应用程序将打印一条消息,指出没有可用的字典包含该单词。

单例设计模式

设计模式是软件设计中常见问题的通用解决方案。我们的想法是将解决方案转换为代码,并且该代码可以应用于发生问题的不同情况。单例模式描述了一种技术,以确保只创建一个类的单个实例。本质上,该技术采用以下方法:不要让类外的任何人创建对象的实例。

例如,DictionaryService 类实现单例模式,如下所示:

3. 实现服务提供者

要提供此服务,你必须创建 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.DictionaryServicedictionary.spi.Dictionary

此示例的 GeneralDictionary 提供者仅定义了两个单词:bookeditor。显然,更可用的字典将提供更实质的常用词汇表。

为了演示多个提供者如何实现相同的 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.DictionaryServicedictionary.spi.Dictionary

很容易想象客户使用一整套 Dictionary 提供者来满足他们自己的特殊需求。服务加载器 API 使他们能够根据需要或首选项更改为其应用程序添加新字典。由于底层单词处理器应用程序是可扩展的,因此客户无需额外编码即可使用新提供者。

4. 注册服务提供者

要注册服务提供者,请创建提供者配置文件,该文件存储在服务提供者的 JAR 文件的 META-INF/services 目录中。配置文件的名称是服务提供者的完全限定类名,其中名称的每个组件用句点(.)分隔,嵌套类用美元符号分隔($)。

提供者配置文件包含服务提供者的完全限定类名,每行一个名称。该文件必须是 UTF-8 编码的。此外,你可以在文件中包含注释,通过在行首使用数字符号(#)注释行。

例如,要注册服务提供者 GeneralDictionary,请创建名为 dictionary.spi.Dictionary 的文本文件。该文件包含一行:

dictionary.GeneralDictionary

同样,要注册服务提供者 ExtendedDictionary,请创建名为 dictionary.spi.Dictionary 的文本文件。该文件包含一行:

dictionary.ExtendedDictionary

5. 创建使用服务和服务提供者的客户端

由于开发完整的单词处理器应用程序是一项重大任务,因此本教程提供了一个使用 DictionaryServiceDictionary SPI 的更简单的应用程序。DictionaryDemo 示例搜索单词 bookeditorxmlREST 从来自类路径上任何 Dictionary 提供者并获取它们的定义。

以下是 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.DictionaryServicedictionary.spi.Dictionary

6. 在 JAR 文件中打包服务提供者,服务和服务客户​​端

有关如何创建 JAR 文件的信息,请参阅 Packaging Programs in JAR Files 课程。

将服务提供者打包到 JAR 文件中

要打包 GeneralDictionary 服务提供者,请创建一个名为 GeneralDictionary/dist/GeneralDictionary.jar 的 JAR 文件,其中包含此服务提供者的已编译类文件和配置文件,按以下目录结构:

同样,要打包 ExtendedDictionary 服务提供者,请创建一个名为 ExtendedDictionary/dist/ExtendedDictionary.jar 的 JAR 文件,其中包含此服务提供者的已编译类文件和配置文件,按以下目录结构:

请注意,提供者配置文件必须位于 JAR 文件的 META-INF/services 目录中。

将字典 SPI 和字典服务打包在 JAR 文件中

创建一个名为 DictionaryServiceProvider/dist/DictionaryServiceProvider.jar 的 JAR 文件,其中包含以下文件:

将客户端打包在 JAR 文件中

创建一个名为 DictionaryDemo/dist/DictionaryDemo.jar 的 JAR 文件,其中包含以下文件:

7. 运行客户端

以下命令使用 GeneralDictionary 服务提供者运行 DictionaryDemo 示例:

Linux 和 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

使用此命令时,假定以下内容:

该命令打印以下内容:

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 和 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 类可以轻松实例化它找到的服务提供者。

提供者按需定位和实例化。服务加载程序维护已加载的提供者的缓存。每次调用加载器的 iterator 方法都会返回一个迭代器,该迭代器首先以实例化的顺序生成缓存的所有元素。然后,服务加载器定位并实例化任何新提供者,依次将每个提供者添加到缓存中。你可以使用 reload 方法清除提供者缓存。

要为特定类创建加载器,请将类本身提供给 loadloadInstalled 方法。你可以使用默认的类加载器或提供你自己的 ClassLoader 子类。

loadInstalled 方法搜索运行时环境的已安装运行时提供者的扩展目录。默认扩展位置是运行时环境的 jre/lib/ext 目录。你应该仅将扩展位置用于众所周知的受信任提供者,因为此位置将成为所有应用程序的类路径的一部分。在本文中,提供者不使用扩展目录,而是依赖于特定于应用程序的类路径。

ServiceLoader API 的局限性

ServiceLoader API 很有用,但它有局限性。例如,无法从 ServiceLoader 类派生类,因此你无法修改其行为。你可以使用自定义 ClassLoader 子类来更改类的查找方式,但无法扩展 ServiceLoader 本身。此外,当运行时新的提供者可用时,当前的 ServiceLoader 类无法告诉你的应用程序。此外,你无法将更改监听器添加到加载程序以查明是否将新提供者放入特定于应用程序的扩展目录中。

Java SE 6 中提供了公共 ServiceLoader API。虽然加载器服务早在 JDK 1.3 中就已存在,但 API 是私有的,仅适用于内部 Java 运行时代码。

总结

可扩展应用程序提供可由服务提供者扩展的服务点。创建可扩展应用程序的最简单方法是使用 ServiceLoader,它可用于 Java SE 6 及更高版本。使用此类,你可以将提供者实现添加到应用程序类路径以使新功能可用。ServiceLoader 类是 final,因此你无法修改其功能。


Previous page: Understanding Extension Class Loading
Next page: Making Extensions Secure