Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
涵盖以下主题:
extensible (可扩展) 应用程序是你可以在不修改其原始代码库的情况下扩展的应用程序。你可以使用新的插件或模块增强其功能。开发人员,软件供应商和客户可以通过将新的 Java Archive(JAR)文件添加到应用程序类路径或特定于应用程序的扩展目录中来添加新功能或应用程序编程接口(API)。
本节介绍如何使用可扩展服务创建应用程序,使你或其他人能够提供不需要修改原始应用程序的服务实现。通过设计可扩展的应用程序,你可以在不更改核心应用程序的情况下提供升级或增强产品特定部分的方法。
可扩展应用程序的一个示例是允许终端用户添加新字典或拼写检查器的单词处理器。在此示例中,单词处理器提供字典或拼写功能,其他开发人员甚至客户可以通过提供他们自己的功能实现来扩展。
以下是了解可扩展应用程序的重要术语和定义:
考虑如何在单词处理器或编辑器中设计字典服务。一种方法是定义由名为 DictionaryService
的类和名为 Dictionary
的服务提供者接口表示的服务。DictionaryService
提供单例 DictionaryService
对象。(有关详细信息,请参阅 The Singleton Design Pattern 一节。)此对象从 Dictionary
提供者中获取单词的定义。字典服务客户端 - 你的应用程序代码 - 获取此服务的实例,该服务将搜索,实例化和使用 Dictionary
服务提供者。
虽然单词处理开发人员很可能会提供原始产品的基本通用字典,但客户可能需要专门的字典,可能包含法律或技术术语。理想情况下,客户可以创建或购买新字典并将其添加到现有应用程序中。
DictionaryServiceDemo
示例演示如何实现 Dictionary
服务,创建添加其他字典的 Dictionary
服务提供者,以及创建一个简单的 Dictionary
服务客户端来测试服务。此示例包装在 zip 文件 DictionaryServiceDemo.zip
中,包含以下文件:
build.xml
DictionaryDemo
build.xml
build
dist
DictionaryDemo.jar
src
dictionary
DictionaryServiceProvider
build.xml
build
dist
DictionaryServiceProvider.jar
src
dictionary
ExtendedDictionary
build.xml
build
dist
ExtendedDictionary.jar
src
dictionary
META-INF
services
GeneralDictionary
build.xml
build
dist
GeneralDictionary.jar
src
dictionary
META-INF
services
注意:build
目录包含同一级别的 src
目录中包含的 Java 源文件的已编译类文件。
由于 zip 文件 DictionaryServiceDemo.zip
包含已编译的类文件,因此你可以通过以下步骤将此文件解压缩到你的计算机并运行示例而不用编译它:
下载并解压缩示例代码:将文件 DictionaryServiceDemo.zip
下载并解压缩到你的计算机。这些步骤假设你将此文件的内容解压缩到目录 C:\DictionaryServiceDemo
中。
将当前目录更改为 C:\DictionaryServiceDemo\DictionaryDemo
,然后执行步骤 Run the Client。
DictionaryServiceDemo
示例包含 Apache Ant 构建文件,这些文件都名为 build.xml
。以下步骤说明如何使用 Apache Ant 编译,构建和运行 DictionaryServiceDemo
示例:
安装 Apache Ant:转到以下链接下载并安装 Apache Ant:
确保包含 Apache Ant 可执行文件的目录位于 PATH
环境变量中,以便可以从任何目录运行它。此外,确保你的 JDK 的 bin
目录,其中包含 java
和 javac
可执行文件(java.exe
和 javac.exe
用于 Microsoft Windows)。在你的 PATH
环境变量中。有关设置 PATH
环境变量的信息,请参见 PATH and CLASSPATH。
下载并解压缩示例代码:将文件 DictionaryServiceDemo.zip
下载并解压缩到你的计算机。这些步骤假定你将此文件的内容解压缩到目录 C:\DictionaryServiceDemo
中。
编译代码:将当前目录更改为 C:\DictionaryServiceDemo
并运行以下命令:
ant compile-all
此命令编译目录 DictionaryDemo
,DictionaryServiceProvider
,ExtendedDictionary
以及 GeneralDictionary
中包含的 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
环境变量中。有关更多信息,请参阅 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.zip
的内容。这些步骤向你展示了示例的工作原理以及如何运行它。
DictionaryServiceDemo
示例定义了一个 SPI,
接口。它只包含一种方法:Dictionary.java
package dictionary.spi; public interface Dictionary { public String getDefinition(String word); }
该示例将已编译的类文件存储在目录 DictionaryServiceProvider/build
中。
类代表字典服务客户端加载和访问可用的 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
DictionaryService
构造函数声明为 private
,这会阻止除 DictionaryService
之外的所有其他类创建它的实例。DictionaryService
成员变量 service
定义为 static
,这样可确保只存在 DictionaryService
的一个实例。getInstance
,它允许其他类控制访问 DictionaryService
成员变量 service
。要提供此服务,你必须创建
实现。为了简单起见,创建一个只定义几个单词的通用字典。你可以使用数据库,一组属性文件或任何其他技术来实现字典。演示提供者模式的最简单方法是在单个文件中包含所有单词和定义。 Dictionary.java
以下代码显示了 Dictionary
SPI 的实现,
类。请注意,它提供了无参构造函数,并实现了 SPI 定义的 GeneralDictionary.java
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
。
很容易想象客户使用一整套 Dictionary
提供者来满足他们自己的特殊需求。服务加载器 API 使他们能够根据需要或首选项更改为其应用程序添加新字典。由于底层单词处理器应用程序是可扩展的,因此客户无需额外编码即可使用新提供者。
要注册服务提供者,请创建提供者配置文件,该文件存储在服务提供者的 JAR 文件的 META-INF/services
目录中。配置文件的名称是服务提供者的完全限定类名,其中名称的每个组件用句点(.
)分隔,嵌套类用美元符号分隔($
)。
提供者配置文件包含服务提供者的完全限定类名,每行一个名称。该文件必须是 UTF-8 编码的。此外,你可以在文件中包含注释,通过在行首使用数字符号(#
)注释行。
例如,要注册服务提供者 GeneralDictionary
,请创建名为
的文本文件。该文件包含一行:dictionary.spi.Dictionary
dictionary.GeneralDictionary
同样,要注册服务提供者 ExtendedDictionary
,请创建名为
的文本文件。该文件包含一行:dictionary.spi.Dictionary
dictionary.ExtendedDictionary
由于开发完整的单词处理器应用程序是一项重大任务,因此本教程提供了一个使用 DictionaryService
和 Dictionary
SPI 的更简单的应用程序。DictionaryDemo
示例搜索单词 book,editor,xml 和 REST 从来自类路径上任何 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.DictionaryService
和 dictionary.spi.Dictionary
。
有关如何创建 JAR 文件的信息,请参阅 Packaging Programs in JAR Files 课程。
要打包 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
目录中。
创建一个名为 DictionaryServiceProvider/dist/DictionaryServiceProvider.jar
的 JAR 文件,其中包含以下文件:
dictionary
DictionaryService.class
spi
Dictionary.class
创建一个名为 DictionaryDemo/dist/DictionaryDemo.jar
的 JAR 文件,其中包含以下文件:
dictionary
DictionaryDemo.class
以下命令使用 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
。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
java.util.ServiceLoader
类可帮助你查找,加载和使用服务提供者。它在应用程序的类路径或运行时环境的扩展目录中搜索服务提供者。它加载它们并使你的应用程序能够使用提供者的 API。如果将新提供者添加到类路径或运行时扩展目录,则 ServiceLoader
类将找到它们。如果你的应用程序知道提供者接口,则可以找到并使用该接口的不同实现。你可以使用接口的第一个可加载实例,也可以遍历所有可用接口。
ServiceLoader
类是 final,这意味着你不能将其作为子类或覆盖其加载算法。例如,你无法更改其算法以从其他位置搜索服务。
从 ServiceLoader
类的角度来看,所有服务都有一个类型,通常是单个接口或抽象类。提供者本身包含一个或多个具体类,这些类使用特定于其目的的实现来扩展服务类型。ServiceLoader
类要求单个公开的提供者类型具有默认构造函数,该构造函数不需要参数。这使 ServiceLoader
类可以轻松实例化它找到的服务提供者。
提供者按需定位和实例化。服务加载程序维护已加载的提供者的缓存。每次调用加载器的 iterator
方法都会返回一个迭代器,该迭代器首先以实例化的顺序生成缓存的所有元素。然后,服务加载器定位并实例化任何新提供者,依次将每个提供者添加到缓存中。你可以使用 reload
方法清除提供者缓存。
要为特定类创建加载器,请将类本身提供给 load
或 loadInstalled
方法。你可以使用默认的类加载器或提供你自己的 ClassLoader
子类。
loadInstalled
方法搜索运行时环境的已安装运行时提供者的扩展目录。默认扩展位置是运行时环境的 jre/lib/ext
目录。你应该仅将扩展位置用于众所周知的受信任提供者,因为此位置将成为所有应用程序的类路径的一部分。在本文中,提供者不使用扩展目录,而是依赖于特定于应用程序的类路径。
ServiceLoader
API 很有用,但它有局限性。例如,无法从 ServiceLoader
类派生类,因此你无法修改其行为。你可以使用自定义 ClassLoader
子类来更改类的查找方式,但无法扩展 ServiceLoader
本身。此外,当运行时新的提供者可用时,当前的 ServiceLoader
类无法告诉你的应用程序。此外,你无法将更改监听器添加到加载程序以查明是否将新提供者放入特定于应用程序的扩展目录中。
Java SE 6 中提供了公共 ServiceLoader
API。虽然加载器服务早在 JDK 1.3 中就已存在,但 API 是私有的,仅适用于内部 Java 运行时代码。
可扩展应用程序提供可由服务提供者扩展的服务点。创建可扩展应用程序的最简单方法是使用 ServiceLoader
,它可用于 Java SE 6 及更高版本。使用此类,你可以将提供者实现添加到应用程序类路径以使新功能可用。ServiceLoader
类是 final,因此你无法修改其功能。