文档

Java™ 教程
隐藏目录
创建可扩展应用程序
教程: 扩展机制
课程: 创建和使用扩展

创建可扩展应用程序

本教程包括以下主题:

介绍

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

本节介绍如何创建具有可扩展服务的应用程序,这样您或其他人可以提供不需要修改原始应用程序的服务实现。通过设计可扩展的应用程序,您提供了一种升级或增强产品特定部分而不改变核心应用程序的方式。

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

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

服务(Service)
一组编程接口和类,提供对特定应用功能或特性的访问。服务可以定义功能的接口和实现的获取方式。在文字处理器的例子中,字典服务可以定义获取字典和单词定义的方式,但它不实现底层功能集。相反,它依赖于一个"服务提供者"来实现该功能。
服务提供者接口(SPI)
服务定义的一组公共接口和抽象类。SPI定义了可供应用程序使用的类和方法。
服务提供者(Service Provider)
实现了SPI的类。具有可扩展服务的应用程序允许您、供应商和客户添加服务提供者,而无需修改原始应用程序。

字典服务示例

考虑一下在文字处理器或编辑器中如何设计字典服务。一种方式是定义一个由名为DictionaryService的类和名为Dictionary的服务提供者接口表示的服务。 DictionaryService提供了一个单例DictionaryService对象。 (有关更多信息,请参见单例设计模式部分。)该对象从Dictionary提供者获取单词的定义。字典服务客户端 - 即应用程序代码 - 检索此服务的一个实例,并且该服务将搜索、实例化和使用Dictionary服务提供者。

尽管文字处理器开发人员很可能会提供一个基本的通用字典作为原始产品,但客户可能需要一个包含法律或技术术语的专业字典。理想情况下,客户能够创建或购买新的字典并将它们添加到现有的应用程序中。

DictionaryServiceDemo示例向您展示了如何实现一个Dictionary服务,创建添加额外字典的Dictionary服务提供者,并创建一个简单的Dictionary服务客户端来测试该服务。这个示例被打包在DictionaryServiceDemo.zip压缩文件中,包含以下文件:

注意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目录,其中包含javajavac可执行文件(对于Microsoft Windows而言是java.exejavac.exe),也在你的PATH环境变量中。请参考PATH和CLASSPATH了解设置PATH环境变量的信息。

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

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

    ant compile-all

    该命令编译了目录DictionaryDemoDictionaryServiceProviderExtendedDictionaryGeneralDictionary中的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和CLASSPATH

    将当前目录更改为C:\DictionaryServiceDemo\DictionaryDemo并运行以下命令:

    ant run

    示例将打印以下内容:

    book: 一本写有或印刷有页面的书,通常带有保护封面
    editor: 一位编辑
    xml: 一种常用于网络服务等的文件标准
    REST: 一种用于创建、读取、更新和删除数据的体系结构风格,试图使用HTTP协议的常见词汇;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类的一个实例。详见单例设计模式部分了解更多信息。

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

字典服务使用ServiceLoader.load方法查找目标类。SPI由接口dictionary.spi.Dictionary定义,所以示例使用此类作为load方法的参数。默认的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",
            "一套书写或印刷的页面,通常带有保护封面");
        map.put(
            "editor",
            "编辑的人");
    }

    @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",
            "一种常用于 Web 服务等场景的文档标准");
        map.put(
            "REST",
            "一种用于创建、读取、更新和删除数据的架构风格,试图使用 HTTP 协议的通用词汇;表述性状态转移");
    }

    @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示例从类路径上的任何Dictionary提供者中搜索单词bookeditorxmlREST,并获取它们的定义。

以下是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 + "找不到该单词的定义。";
    } else {
      return outputString + definition;
    }
  }
}

示例将编译后的类文件存储在目录DictionaryDemo/build中。注意:在运行DictionaryDemo类之前,您必须先编译dictionary.DictionaryServicedictionary.spi.Dictionary类。

6. 将服务提供者、服务和服务客户端打包成JAR文件

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

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

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

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

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

将Dictionary SPI和Dictionary Service打包到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:一套书写或印刷的页面,通常带有保护封面
editor:编辑的人
xml:找不到该单词的定义。
REST:找不到该单词的定义。

假设您运行以下命令并且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:找不到该单词的定义。
editor:找不到该单词的定义。
xml:一种在Web服务中经常使用的文档标准,等等
REST:一种用于创建、读取、更新和删除数据的架构样式,试图使用HTTP协议的通用词汇;表述性状态转移

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类无法告知应用程序运行时是否有新的提供者可用。此外,您无法向加载器添加更改侦听器以了解是否将新提供者放置在特定于应用程序的扩展目录中。

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

总结

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


上一页:理解扩展类加载
下一页:使扩展安全