这些Java教程是针对JDK 8编写的。本页中描述的示例和实践不利用后续版本引入的改进,并可能使用不再可用的技术。
有关Java SE 9及其后续版本中更新的语言特性的摘要,请参阅Java语言变更。
有关所有JDK版本的新功能、增强功能和已删除或不推荐的选项的信息,请参阅JDK发行说明。
本教程包括以下主题:
可扩展的应用程序是指可以在不修改其原始代码的情况下进行扩展的应用程序。您可以通过将新的插件或模块添加到应用程序类路径或应用程序特定的扩展目录中,来增强其功能。开发人员、软件供应商和客户可以通过添加一个新的Java Archive (JAR)文件来添加新的功能或应用程序编程接口(API)。
本节介绍如何创建具有可扩展服务的应用程序,这样您或其他人可以提供不需要修改原始应用程序的服务实现。通过设计可扩展的应用程序,您提供了一种升级或增强产品特定部分而不改变核心应用程序的方式。
一个可扩展应用程序的示例是一个允许最终用户添加新字典或拼写检查器的文字处理器。在这个示例中,文字处理器提供了一个字典或拼写功能,其他开发人员甚至客户可以通过提供自己的功能实现来扩展该功能。
以下是理解可扩展应用程序重要的术语和定义:
考虑一下在文字处理器或编辑器中如何设计字典服务。一种方式是定义一个由名为DictionaryService
的类和名为Dictionary
的服务提供者接口表示的服务。 DictionaryService
提供了一个单例DictionaryService
对象。 (有关更多信息,请参见单例设计模式部分。)该对象从Dictionary
提供者获取单词的定义。字典服务客户端 - 即应用程序代码 - 检索此服务的一个实例,并且该服务将搜索、实例化和使用Dictionary
服务提供者。
尽管文字处理器开发人员很可能会提供一个基本的通用字典作为原始产品,但客户可能需要一个包含法律或技术术语的专业字典。理想情况下,客户能够创建或购买新的字典并将它们添加到现有的应用程序中。
DictionaryServiceDemo
示例向您展示了如何实现一个Dictionary
服务,创建添加额外字典的Dictionary
服务提供者,并创建一个简单的Dictionary
服务客户端来测试该服务。这个示例被打包在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
构建
发布
GeneralDictionary.jar
源码
dictionary
META-INF
services
注意: build
目录包含与同级目录中 src
目录中的 Java 源文件对应的编译后的类文件。
由于 zip 文件 DictionaryServiceDemo.zip
包含编译后的类文件,您可以将该文件解压到计算机上,并按照以下步骤运行示例而无需编译它:
下载并解压示例代码:下载并解压文件 DictionaryServiceDemo.zip
到计算机上。以下步骤假设您已将该文件的内容解压到目录 C:\DictionaryServiceDemo
中。
将当前目录更改为 C:\DictionaryServiceDemo\DictionaryDemo
,然后按照步骤 运行客户端。
DictionaryServiceDemo 示例包含 Apache Ant 构建文件,所有文件均命名为 build.xml
。以下步骤将向您展示如何使用 Apache Ant 来编译、构建和运行 DictionaryServiceDemo 示例:
安装Apache Ant:前往以下链接下载并安装Apache Ant:
确保包含Apache Ant可执行文件的目录在你的PATH
环境变量中,这样你就可以从任何目录运行它。另外,确保你的JDK的bin
目录,其中包含java
和javac
可执行文件(对于Microsoft Windows而言是java.exe
和javac.exe
),也在你的PATH
环境变量中。请参考PATH和CLASSPATH了解设置PATH
环境变量的信息。
下载并解压示例代码:下载并解压文件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和CLASSPATH。
将当前目录更改为C:\DictionaryServiceDemo\DictionaryDemo
并运行以下命令:
ant run
示例将打印以下内容:
book: 一本写有或印刷有页面的书,通常带有保护封面
editor: 一位编辑
xml: 一种常用于网络服务等的文件标准
REST: 一种用于创建、读取、更新和删除数据的体系结构风格,试图使用HTTP协议的常见词汇;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
类的一个实例。详见单例设计模式部分了解更多信息。
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
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", "一套书写或印刷的页面,通常带有保护封面"); map.put( "editor", "编辑的人"); } @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", "一种常用于 Web 服务等场景的文档标准"); map.put( "REST", "一种用于创建、读取、更新和删除数据的架构风格,试图使用 HTTP 协议的通用词汇;表述性状态转移"); } @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
示例从类路径上的任何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 + "找不到该单词的定义。"; } else { return outputString + definition; } } }
示例将编译后的类文件存储在目录DictionaryDemo/build
中。注意:在运行DictionaryDemo
类之前,您必须先编译dictionary.DictionaryService
和dictionary.spi.Dictionary
类。
有关如何创建JAR文件的信息,请参见课程在JAR文件中打包程序。
要将GeneralDictionary
服务提供者打包成JAR文件,创建一个名为GeneralDictionary/dist/GeneralDictionary.jar
的JAR文件,其中包含该服务提供者的编译后的类文件和以下目录结构中的配置文件:
META-INF
services
dictionary.spi.Dictionary
dictionary
GeneralDictionary.class
类似地,要将ExtendedDictionary
服务提供者打包成JAR文件,创建一个名为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:一套书写或印刷的页面,通常带有保护封面 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协议的通用词汇;表述性状态转移
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
类无法告知应用程序运行时是否有新的提供者可用。此外,您无法向加载器添加更改侦听器以了解是否将新提供者放置在特定于应用程序的扩展目录中。
公共ServiceLoader
API在Java SE 6中可用。虽然加载器服务在JDK 1.3早期就已存在,但该API是私有的,仅适用于内部Java运行时代码。
可扩展应用程序提供可以由提供者扩展的服务点。创建可扩展应用程序的最简单方法是使用ServiceLoader
,它适用于Java SE 6及更高版本。使用这个类,您可以将提供者实现添加到应用程序类路径中,以使新功能可用。ServiceLoader
类是final的,因此无法修改其功能。