创建可扩展的应用程序

涵盖以下主题:

Introduction

可扩展应用程序是可以扩展而无需修改其原始代码库的应用程序。您可以使用新的插件或模块来增强其功能。通过将新的 Java Archive(JAR)文件添加到应用程序 Classpath 或特定于应用程序的扩展目录中,开发人员,软件供应商和 Client 可以添加新功能或应用程序编程interface(API)。

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

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

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

  • Service

    • 一组编程interface和类,提供对某些特定应用程序功能或特性的访问。服务可以定义功能的interface以及检索实现的方式。在 Literals 处理器示例中,词典服务可以定义一种检索词典和单词定义的方法,但是它不实现基础功能集。而是依靠服务提供商来实现该功能。
  • 服务提供商interface(SPI)

    • 服务定义的一组公共interface和抽象类。 SPI 定义了可用于您的应用程序的类和方法。
  • Service Provider

    • 实现 SPI。具有可扩展服务的应用程序使您,供应商和 Client 无需修改原始应用程序即可添加服务提供商。

字典服务示例

考虑如何在 Literals 处理器或编辑器中设计字典服务。一种方法是定义由名为DictionaryService的类和名为Dictionary的服务提供程序interface表示的服务。 DictionaryService提供一个单例DictionaryService对象。 (有关更多信息,请参见单例设计 Pattern部分。)此对象从Dictionary提供程序检索单词的定义。字典服务 Client 端(您的应用程序代码)检索该服务的实例,该服务将搜索,实例化和使用Dictionary服务提供者。

尽管 Literals 处理器开发人员很可能会为原始产品提供基本的通用词典,但是 Client 可能需要专门的词典,其中可能包含法律或技术术语。理想情况下,Client 能够创建或购买新词典并将其添加到现有应用程序中。

DictionaryServiceDemo示例向您展示了如何实现Dictionary服务,创建Dictionary服务提供者以添加其他词典以及创建一个简单的Dictionary服务 Client 端以测试该服务的方法。此 samples 打包在 zip 文件DictionaryServiceDemo.zip中,由以下文件组成:

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

运行 DictionaryServiceDemo 示例

由于 zip 文件DictionaryServiceDemo.zip包含已编译的类文件,因此可以按照以下步骤将此文件解压缩到计算机上并运行示例,而无需对其进行编译:

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

  • 将当前目录更改为C:\DictionaryServiceDemo\DictionaryDemo,然后执行步骤运行 Client 端

编译并运行 DictionaryServiceDemo 示例

DictionaryServiceDemo示例包括 Apache Ant 构建文件,它们都名为build.xml。以下步骤向您展示如何使用 Apache Ant 来编译,构建和运行DictionaryServiceDemo示例:

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

http://ant.apache.org/

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

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

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

ant compile-all

该命令在DictionaryDemoDictionaryServiceProviderExtendedDictionaryGeneralDictionary目录中包含的src目录中编译源代码,并将生成的class文件放入相应的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环境变量中。有关更多信息,请参见路径和 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.定义服务提供商interface

DictionaryServiceDemo示例定义了一个 SPI,即Dictionary.javainterface。它仅包含一种方法:

package dictionary.spi;

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

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

2.定义检索服务提供者实现的服务

DictionaryService.java类代表字典服务 Client 端加载和访问可用的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类实现单例设计 Pattern。这意味着将只创建DictionaryService类的单个实例。有关更多信息,请参见单例设计 Pattern部分。

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

词典服务使用ServiceLoader.load方法查找目标类。 SPI 由interfacedictionary.spi.Dictionary定义,因此该示例将此类用作装入方法的参数。默认加载方法使用默认类加载器搜索应用程序 Classpath。

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

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

DictionaryDemo.java类使用此服务。要使用该服务,应用程序获取一个DictionaryService实例并调用getDefinition方法。如果定义可用,则应用程序将其打印。如果没有可用的定义,则应用程序将显示一条消息,指出没有可用的词典携带该单词。

单例设计 Pattern

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

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

  • DictionaryService构造函数声明为private,这将阻止DictionaryService以外的所有其他类创建其实例。

  • DictionaryService成员变量service定义为static,以确保仅存在DictionaryService的一个实例。

  • 定义方法getInstance,该方法使其他类可以控制对DictionaryService成员变量service的访问。

3.实现服务提供商

要提供此服务,您必须创建Dictionary.java实现。为了简单起见,请创建仅定义几个单词的通用词典。您可以使用数据库,一组属性文件或任何其他技术来实现字典。演示提供程序 Pattern 的最简单方法是在单个文件中包含所有单词和定义。

以下代码显示了GeneralDictionary.javaDictionary SPI 的实现。请注意,它提供了无参数的构造函数并实现了 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提供程序仅定义了两个词: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.DictionaryServicedictionary.spi.Dictionary类。

很难想象 Client 使用一套完整的Dictionary提供程序来满足自己的特殊需求。服务加载器 API 使他们可以根据需要或偏好更改将新字典添加到其应用程序中。因为基础的 Literals 处理器应用程序是可扩展的,所以 Client 不需要其他编码即可使用新的提供程序。

4.注册服务提供商

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

提供程序配置文件包含服务提供程序的完全限定的类名称,每行一个名称。该文件必须为 UTF-8 编码。另外,您可以通过以数字符号(#)开头 注解 行来在文件中包含 注解。

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

dictionary.GeneralDictionary

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

dictionary.ExtendedDictionary

5.创建使用服务和服务提供商的 Client 端

因为开发一个完整的 Literals 处理器应用程序是一项艰巨的任务,所以本教程提供了一个使用DictionaryServiceDictionary SPI 的简单应用程序。 DictionaryDemo示例从 Classpath 上的任何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.DictionaryServicedictionary.spi.Dictionary类。

6.将服务提供者,服务和服务 Client 端打包到 JAR 文件中

有关如何创建 JAR 文件的信息,请参见类JAR 文件中的打包程序

JAR 文件中的打包服务提供者

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

  • META-INF

  • services

  • dictionary.spi.Dictionary

  • dictionary

  • GeneralDictionary.class

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

  • META-INF

  • services

  • dictionary.spi.Dictionary

  • dictionary

  • ExtendedDictionary.class

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

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

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

  • dictionary

  • DictionaryService.class

    • spi
  • Dictionary.class

将 Client 端打包到 JAR 文件中

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

  • dictionary

  • DictionaryDemo.class

7.运行 Client 端

以下命令与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

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

  • 当前目录是DictionaryDemo

  • 存在以下 JAR 文件:

  • DictionaryDemo/dist/DictionaryDemo.jar:包含DictionaryDemo

    • DictionaryServiceProvider/dist/DictionaryServiceProvider.jar:包含Dictionary SPI 和DictionaryService

    • GeneralDictionary/dist/GeneralDictionary.jar:包含GeneralDictionary服务提供商和配置文件

该命令将显示以下内容:

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类可帮助您查找,加载和使用服务提供商。它在应用程序的 Classpath 或运行时环境的扩展目录中搜索服务提供者。它将加载它们,并使您的应用程序可以使用提供程序的 API。如果将新的提供程序添加到 Classpath 或运行时扩展目录,则ServiceLoader类将找到它们。如果您的应用程序知道提供者interface,则它可以找到并使用该interface的不同实现。您可以使用该interface的第一个可加载实例,或遍历所有可用interface。

ServiceLoader类是final的,这意味着您不能使其成为子类或覆盖其加载算法。例如,您不能更改其算法以从其他位置搜索服务。

ServiceLoader类的角度来看,所有服务都具有单个类型,通常是单个interface或抽象类。提供程序本身包含一个或多个具体类,这些类通过特定于其 Object 的实现来扩展服务类型。 ServiceLoader类要求单个公开的提供程序类型具有默认构造函数,该构造函数不需要任何参数。这使ServiceLoader类可以轻松实例化它找到的服务提供者。

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

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

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

ServiceLoader API 的局限性

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

Java SE 6 中提供了公共ServiceLoader API。尽管加载程序服务早在 JDK 1.3 上就已存在,但该 API 是私有的,并且仅可用于内部 Java 运行时代码。

Summary

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