可扩展 应用程序是指无需修改原有代码基础就可轻易扩展的应用程序。可以通过新插件或模块来增强其功能。开发人员,软件供应商,甚至客户只要在应用程序的类路径或特定于应用程序的扩展目录中添加一个新的 Java Archive(JAR) 文件,即可添加新的功能或应用程序编程接口(API)。
本文将介绍使用可扩展服务创建应用程序的两种方法,任何人都可以在无需修改原始应用程序的情况下提供服务实现。通过设计一个可扩展的应用程序,我们可以轻松地升级或增强产品的特定部分,同时无需修改核心应用程序。
可扩展应用程序的一个例子是文字处理程序,它允许最终用户添加新的词典或拼写检查程序。在本例中,文字处理程序将提供一个目录或拼写功能,其他开发人员或者甚至是客户,都可以通过提供自己的功能实现对其进行扩展。另一个例子是 NetBeans IDE,在很多情况下,它都允许用户添加编辑器和其他模块,同时无需重新启动应用程序。
ServiceLoader
类
Java SE 6 平台提供一个新的 API,可以帮助您查找、加载和使用服务提供程序。从 Java 平台的 1.3 版本开始, java.util.ServiceLoader
类就已经悄悄存在了,但它在 Java SE 6 中已经成为了一个公共 API。
ServiceLoader
类用于在应用程序的类路径或运行时环境的扩展目录中搜索服务提供程序。它加载这些服务提供程序,并允许应用程序使用这些提供程序的 API。如果添加了新的提供程序到类路径或运行时扩展目录中, ServiceLoader
类就可以找到它们。如果应用程序知道提供程序接口的存在,它就可以找到并使用该接口的各种实现。可以使用接口的首个可加载实例,或者甚至可以迭代所有可用的接口。
ServiceLoader
类是 final 类型的,这表示您不能继承或重载其加载算法。例如,您不能把它的算法修改为从另一个位置搜索服务。
从 ServiceLoader
类的角度而言,所有服务都有一个类型,通常为接口或抽象类。提供程序本身包含一个或多个具体类,可借助特定实现来扩展服务类型。 ServiceLoader
类要求已公开的提供程序类型有一个默认构造函数,可以不带参数。这样, ServiceLoader
类便可以方便地实例化所找到的服务提供程序。
定义服务提供程序的方法是实现服务提供程序 API。通常,您会创建一个 JAR 文件来保存提供程序。要注册提供程序,必须在 JAR 文件的 META-INF/services
目录中创建一个提供程序配置文件。配置文件的名称应该是服务类型的完全限定二进制名称。 二进制名称 就是完全限定的类名,名称的每个组成部分由 .
字符分隔,而嵌套类则由 $
字符分隔。
例如,如果实现了 com.example.dictionary.spi.Dictionary
服务类型,您应该创建一个 META-INF/services/com.example.dictionary.spi.Dictionary
文件。该文件中将在单独的一行中列出具体实现的完全限定二进制名称。该文件必须为 UTF-8 编码。另外,您还可以在文件中包含注释行,只要在注释行的开始处加上 #
字符即可。
服务加载程序将会忽略相同配置文件或其他配置文件中重复的提供程序类名。尽管您极有可能把配置文件与提供程序类本身放在同一个 JAR 文件中,这并没有限制为必须这样做。然而,在开始用于定位配置文件的同一个类加载程序中,必须能够访问提供程序。
提供程序是随需定位和实例化的。服务加载程序为已加载提供程序维护了一块缓存。加载程序的 iterator
方法的每次调用都会返回一个迭代器,它会首先生成缓存的所有元素。接着,它会找到并实例化任何新的提供程序,并依次把它们添加到缓存中。使用 reload
方法可以清除提供程序缓存。
要为特定类创建加载程序,将类本身提供给 load
或 loadInstalled
方法。您可以使用默认的类加载程序或提供自己的 ClassLoader
子类。.
loadInstalled
方法用于搜索已安装运行时提供程序的运行时环境目录。默认的扩展位置是运行时环境的 jre/lib/ext
目录。应该只对知名的、受信任的提供程序使用扩展位置,因为这个位置将成为所有应用程序的类路径。在本文中,提供程序不会使用扩展目录,但会依赖一个特定于应用程序的类路径作为代替。
本节描述了如何实现本文开始提及的 DictionaryService
和 Dictionary
提供程序类。提供程序并非始终由原始应用程序的供应商实现。事实上,任何人都可以创建一个服务提供程序,只要他拥有能够指明要实现接口的 SPI 应用程序。示例的文字处理程序应用程序提供了一个 DictionaryService
并定义了一个 Dictionary
SPI。发布后的 SPI 定义了一个 Dictionary
接口,该接口只有一个方法。整个接口如下所示:
package com.example.dictionary.spi;
public interface Dictionary {
String getDefinition(String word);
} |
要提供这个服务,必须创建一个 Dictionary
实现。为了让眼前的事情保持简单,我们首先创建只含有少量单词定义的通用词典。您可以通过一个数据库、一组属性文件或任意其他技术来实现这个词典。演示提供程序模式最容易的方式是在一个文件中包含所有单词和定义。
以下代码显示了这个 SPI 的一种可行的实现。注意,它提供了一个无参数的构造函数,并实现了 SPI 定义的 getDefinition
方法。
package com.example.dictionary;
import com.example.dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;
public class GeneralDictionary implements Dictionary {
private SortedMap<String, String> map;
/** Creates a new instance of GeneralDictionary */
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");
}
public String getDefinition(String word) {
return map.get(word);
}
} |
在编译和创建这个提供程序的 JAR 文件之前,还有最后一项任务没有完成。必须遵照提供程序注册要求,在项目和 JAR 文件的 META-INF/services
目录中创建一个配置文件。因为这个例子实现了 com.example.dictionary.spi.Dictionary
接口,因此还需要在目录中创建一个具有相同名称的文件。其内容应该包含列出实现的具体类名的一行。在本例中,文件内容如下所示:
com.example.dictionary.GeneralDictionary |
最后的 JAR 内容将包含的文件如图 2 所示。
|
这个例子的 GeneralDictionary
提供程序只定义了两个单词: book 和 editor。显然,更有用的词典会真正提供常用词汇的一个列表。
要使用 GeneralDictionary
,应该将它的部署 JAR 文件 GeneralDictionary.jar
放入应用程序的类路径中。
为了演示多个提供程序如何实现同一个 SPI,以下代码显示了另一种可行的提供程序。这个提供程序是一个经过扩展的词典,其中包含了大多数软件开发人员所熟悉的技术术语。
package com.example.dictionary;
import com.example.dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;
public class ExtendedDictionary implements Dictionary {
private SortedMap<String, String> map;
/**
* Creates a new instance of ExtendedDictionary
*/
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");
}
public String getDefinition(String word) {
return map.get(word);
}
} |
这个 ExtendedDictionary
所遵照的模式与 GeneralDictionary
相同:必须为它创建一个配置文件,并将 JAR 文件放入应用程序的类路径中。应该再次将配置文件命名 SPI 类名 com.example.dictionary.spi.Dictionary
。然而这次,文件内容与 GeneralDictionary
实现有所不同。对于 ExtendedDictionary
提供程序,文件包含下面这行代码,用于声明 SPI 的具体类实现:
com.example.dictionary.ExtendedDictionary |
这个 Dictionary
实现的文件和结构如图 3 所示。
|
很容易想像,客户使用一组完整的 Dictionary
提供程序来满足他们自己的特殊需求。服务加载程序 API 允许他们在需求或选择变化之后,将新的词典添加到应用程序中。此外,因为底层的文字处理程序应用程序是可扩展的,客户无需进行额外的编码就可以使用新的提供程序。
因为开发一个完整的文字处理程序应用程序是一项艰巨的任务,作者将会提供一个更加简单的应用程序,该应用程序将定义和使用 DictionaryService
和 Dictionary
SPI。Dictionary User 应用程序允许用户在键入一个单词后,从类路径上的任意 Dictionary
提供程序获得该单词的定义。
DictionaryService
类本身将位于所有 Dictionary
实现之前。应用程序将访问 DictionaryService
来获得定义。 DictionaryService
实例将会代表应用程序加载和访问可用的 Dictionary
提供程序。 DictionaryService
类源代码如下所示:
package com.example.dictionary;
import com.example.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;
/**
* Creates a new instance of DictionaryService
*/
private DictionaryService() {
loader = ServiceLoader.load(Dictionary.class);
}
/**
* Retrieve the singleton static instance of DictionaryService.
*/
public static synchronized DictionaryService getInstance() {
if (service == null) {
service = new DictionaryService();
}
return service;
}
/**
* Retrieve definitions from the first provider
* that contains the word.
*/
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;
}
} |
DictionaryService
实例是应用程序使用任意已安装 Dictionary
的入口点。可以使用 getInstance
方法获得惟一的服务入口点。接下来,应用程序就可以调用 getDefinition
方法,该方法通过可用的 Dictionary
提供程序进行迭代,直到它找到目标单词为止。如果没有 Dictionary
实例包含单词的指定定义, getDefinition
方法就会返回 null。
词典服务使用 ServiceLoader.load
方法查找目标类。SPI 是由接口 com.example.dictionary.spi.Dictionary
定义的,因此本例使用这个类作为加载方法的参数。默认的加载方法使用默认的类加载程序搜索应用程序的类路径。
然而,如果愿意,此方法的一个重载版本允许您指定自定义的类加载程序。这将允许您进行更加复杂的类搜索。例如,一名工作热情很高的程序员可能会创建创建这样一个 ClassLoader
实例,它能够在一个包含运行时期间加入的提供程序 JAR、特定于应用程序的子目录中进行搜索。结果,应用程序无需重新启动便可访问新的提供程序类。
一旦用于此类的一个加载程序存在,您可以使用它的迭代器方法访问和使用它找到的每个提供程序。 getDefinition
方法使用一个 Dictionary
迭代器,通过提供程序进行循环,直到它找到特定单词的定义为止。迭代器方法将 Dictionary
实例存入缓存,所以连续调用所需的另外处理时间不多。如果自从上次调用以来已将新的提供程序放入服务中,迭代器方法会将它们添加到列表中。
DictionaryUser
类使用这个服务。要使用该服务,应用程序只要创建一个 DictionaryService
,并在用户键入可搜索单词时调用 getDefinition
方法。如果有可用定义,应用程序就会显示它。如果没有可用定义,应用程序就会显示一条消息,指出没有可用词典涵盖了这个单词。
以下代码列表显示了大部分的 DictionaryUser
实现。一些用户界面布局代码已被删除,以便使代码更加容易阅读。首要的重点是 txtWordActionPerformed
方法。当用户在应用程序的文本区域内按下 Enter 键时,这个方法就会运行。接着,该方法会从 DictionaryService
对象请求目标单词的定义,而这个对象又会把请求传递给其已知的 Dictionary
提供程序。
package com.example.demo;
import com.example.dictionary.DictionaryService;
import javax.swing.JOptionPane;
public class DictionaryUser extends javax.swing.JFrame {
/** Creates new form DictionaryUser */
public DictionaryUser() {
dictionary = DictionaryService.getInstance();
initComponents();
}
/** This method is called from within the constructor to
* initialize the form.
*/
private void initComponents() {
// ...
txtWord.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
txtWordActionPerformed(evt);
}
});
// ...
}
private void txtWordActionPerformed(java.awt.event.ActionEvent evt) {
String searchText = txtWord.getText();
String definition = dictionary.getDefinition(searchText);
txtDefinition.setText(definition);
if (definition == null) {
JOptionPane.showMessageDialog(this,
"Word not found in dictionary set",
"Oops", JOptionPane.WARNING_MESSAGE);
}
}
/**
* @param args the command line arguments
*/
public static void main(String[] args) {
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
new DictionaryUser().setVisible(true);
}
});
}
// Variables declaration - do not modify
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JLabel lblDefinition;
private javax.swing.JLabel lblSearch;
private javax.swing.JTextArea txtDefinition;
private javax.swing.JTextField txtWord;
// End of variables declaration
private DictionaryService dictionary;
} |
图 4 显示了当目标单词 book 不可用时,应用程序显示的警告消息窗格。 GeneralDictionary
类定义了 book 术语,但这个类不在应用程序的类路径中。
|
将 GeneralDictionary
类放入类路径的方法是将其添加到运行时环境的命令行类路径参数。使用以下命令行将词典添加到 Microsoft Windows 运行时类路径:< /p>
java -classpath DictionaryUser.jar;GeneralDictionary.jar
com.example.demo.DictionaryUser |
注意,这个命令行引用了两个 JAR 文件: DictionaryUser
和 GeneralDictionary
。作者对应用程序和 API 进行了划分,让 DictionaryUser.jar
文件包含 DictionaryService
类、 Dictionary
接口和 Dictionary User 应用程序本身。 GeneralDictionary.jar
文件包含提供程序实现。
使用最新可用的提供程序,Dictionary User 应用程序现在找到了单词。图 5 显示了查找结果。
|
将提供程序添加到类路径的方法是将提供程序的 JAR 文件附加到命令行类路径参数后面。这个例子中的新提供程序是 ExtendedDictionary
。以下命令行用于将它添加到应用程序中:
java -classpath DictionaryUser.jar;GeneralDictionary.jar;ExtendedDictionary.jar
com.example.demo.DictionaryUser |
现在,Dictionary User 应用程序中已经定义一些技术术语。图 6 显示了当用户添加了 ExtendedDictionary.jar
提供程序之后,搜索术语 REST 的结果:
|
ServiceLoader
API 的限制
ServiceLoader
API 很有用处,但是它有一些限制。例如,不能继承 ServiceLoader
,所以也无法修改其行为。您可以使用自定义的 ClassLoader
子类来改变找到类的方式,但是无法扩展 ServiceLoader
本身。此外,当运行时有新的提供程序可用时,当前的 ServiceLoader
类不会告诉应用程序。另外,您无法通过添加变化监听器给加载程序,来发现是否有新的提供程序被放到特定于应用程序的扩展目录中。
Java SE 6 中提供了公共 ServiceLoader
API。尽管加载程序服务存在的时间与 JDK 1.3 一样早,但其 API 是私有的,并且只适用于内部的 Java 运行时代码。
为应用程序提高可扩展服务的另一种方式是使用 NetBeans 平台。大多数开发人员都知道 NetBeans 集成开发环境( integrated development environment,IDE),但其中很多人都不清楚,这个 IDE 本身就是一个基于模块化通用平台构建而成的可扩展应用程序。
NetBeans 平台为创建模块化的可扩展应用程序提供了一个完整的应用程序框架。用于用户界面、打印、模块间通信和许多其他服务的模块已经存在于平台中。开发较大的应用程序时,使用这些经过严格测试的现有 API 可以节省大量时间。
尽管讨论整个平台已经超出了本文的范围,但它的确有一部分内容与注册、发现和使用服务提供程序有关。注册、发现和使用提供程序时所需的 API 在 org.openide.util.Lookup
类中都可以找到。这个类为应用程序提高了发现服务的能力,在简单的 ServiceLoader
类的基础上进行了重大改进。
您不必采用整个 NetBeans 平台就能获得增强后的查找功能。只要使用平台的一个模块就可以获得提供程序查找服务。如果您拥有 NetBeans IDE,同时也就拥有了 NetBeans 平台。对于大多数人来说,从 IDE 发布获得平台很有可能是获取平台最容易的方式。通过包含 <NETBEANS_HOME>\platform6\lib
中的 org-openide-util.jar
文件,您可以从 ServiceLoader
类的 Java SE 6 实现获得如下好处:
根据 NetBeans IDE 版本的不同,JAR 文件的确切位置可能也有所不同。在 NetBeans 5.5 中,文件所在位置是 <NETBEANS_HOME>\platform6\lib
,而在 NetBeans 6.0 或更新版本中则是 platform7\lib
。要使用 org-openide-util.jar
,您应该把它添加到编译和运行时类路径中。尽管这个 JAR 文件包含了很多工具,本文只会使用用于 Lookup
的工具以及相关 API。
Lookup
类
org.openide.util.Lookup
类具有 ServiceLoader
的所有功能,甚至更多。它还有一个接口,允许任何类变为 Lookup
类型,这意味着该类自身将提供一个 getLookup
方法。 Lookup
类提供一个默认的 Lookup
实例,用于搜索 classpath。本文中的例子使用的就是这个默认实例。然而,对于程序员来说,创建一个能够在应用程序运行时期间监控可变类路径的自定义 Lookup
子类,以支持真正动态的服务提供程序安装要相对容易一些。
系统范围内的 Lookup
实例默认值是从静态的 getDefault
方法中变为可用的:
Lookup myLookup = Lookup.getDefault(); |
在最基本的情况下,您可以使用 Lookup
来返回它在类路径上找到的首个提供程序实例。使用 Lookup
实例的 lookup
方法可以达到这个目的。提供目标类作为方法参数。以下代码将找到并返回它找到的首个 Dictionary
提供程序的一个实例:
Dictionary dictionary = myLookup.lookup(Dictionary.class); |
使用 NetBeans 平台的 5.5 版本时,必须使用一个模板类来找到并返回多个提供程序实例。创建一个 Lookup.Template
,并将模板提高给 lookup
方法。结果包含所有匹配的提供程序。以下代码显示了如何使用 Template
和 Result
类找到并返回 Dictionary
类的所有提供程序实例。
这个新的 DictionaryService2
类提供的功能与原来的 DictionaryService
类相同。区别在于,新的实现使用了 NetBeans Platform API,这些 API 可以工作在 JDK 的较早版本上,并提供以上描述的好处。
/*
* DictionaryService.java
*/
package com.example.dictionary;
import com.example.dictionary.spi.Dictionary;
import java.util.Collection;
import org.openide.util.Lookup;
import org.openide.util.Lookup.Result;
import org.openide.util.Lookup.Template;
public class DictionaryService2 {
private static DictionaryService2 service;
private Lookup dictionaryLookup;
private Collection<Dictionary> dictionaries;
private Template dictionaryTemplate;
private Result dictionaryResults;
/**
* Creates a new instance of DictionaryService
*/
private DictionaryService2() {
dictionaryLookup = Lookup.getDefault();
dictionaryTemplate = new Template(Dictionary.class);
dictionaryResults = dictionaryLookup.lookup(dictionaryTemplate);
dictionaries = dictionaryResults.allInstances();
}
public static synchronized DictionaryService2 getInstance() {
if (service == null) {
service = new DictionaryService2();
}
return service;
}
public String getDefinition(String word) {
String definition = null;
for(Dictionary d: dictionaries) {
definition = d.getDefinition(word);
if (d != null) break;
}
return definition;
}
} |
特别需要注意获得多个提供程序实例的方式。如以下私有 DictionaryService2
构造函数中的代码所示:
private DictionaryService2() {
dictionaryLookup = Lookup.getDefault();
dictionaryTemplate = new Template(Dictionary.class);
dictionaryResults = dictionaryLookup.lookup(dictionaryTemplate);
dictionaries = dictionaryResults.allInstances();
} |
模板 lookup
方法返回一个包含多个提供程序(如果它们存在的话)的 Result
实例。调用 Result
实例的 allInstances
方法便可获得提供程序的整个集合。这样,我们可以使用以下方法对 Dictionary
实例集合进行迭代:
for(Dictionary d: dictionaries) {
definition = d.getDefinition(word);
if (d != null) break;
} |
可扩展应用程序所提供的服务点可以通过服务提供程序进行扩展。创建可扩展应用程序最容易的方式是使用 Java SE 6 中可用的 ServiceLoader
类。使用这个类时,我们可以将提供程序实现添加到应用程序的类路径中,从而使新功能变为可用。
ServiceLoader
类只在 Java SE 6 中可用,因此对于较早的运行时环境可能需要考虑其他选项。此外, ServiceLoader
类是 final 类型的,所以我们不能修改它的功能。另一个类在 NetBeans 平台中,它可以使用其 Lookup
API访问可扩展服务。 Lookup
类可以提供 ServiceLoader
的所有功能,但是它还具有另外一个优点,就是可以继承。