使用Java平台创建可扩展应用程序

来源:岁月联盟 编辑:zhuzhu 时间:2008-12-23

  本文将介绍使用可扩展服务创建应用程序的两种方法,任何人都可以在无需修改原始应用程序的情况下提供服务实现。通过设计一个可扩展的应用程序,我们可以轻松地升级或增强产品的特定部分,同时无需修改核心应用程序。

  可扩展应用程序的一个例子是文字处理程序,它允许最终用户添加新的词典或拼写检查程序。在本例中,文字处理程序将提供一个目录或拼写功能,其他开发人员或者甚至是客户,都可以通过提供自己的功能实现对其进行扩展。另一个例子是 NetBeans IDE,在很多情况下,它都允许用户添加编辑器和其他模块,同时无需重新启动应用程序。

  词典服务示例

  考虑如何在文字处理程序或编辑器中设计一个词典服务。一种方法是分别定义一个 DictionaryService 类和一个 Dictionary 接口。 DictionaryService 将提供一个 singleton 模式的 DictionaryService 对象,用于从 Dictionary 提供程序检索单词的定义。词典服务客户端(您的应用程序代码)将获得这个服务的一个实例,而服务将会搜索、实例化和使用 Dictionary 服务提供程序。图 1 中用下划线标出的属性和方法是静态的。

使用 Java 平台创建可扩展应用程序

  图 1. 词典 服务的类图 

  所有 Dictionary 提供程序都必须在服务中注册自己。否则,服务就不知道如何才能找到它们。开发人员可以通过各种方法注册接口,但其中最常用的一种方法就是使用应用程序的类路径。服务可以通过检查类路径来找到接口实现。在这种情况下, DictionaryService 可以通过检查应用程序的类路径来找到一个或多个 Dictionary 接口提供程序。

  尽管文字处理程序的开发人员极有可能在原始产品中提供了一个基本的通用词典,客户还是可能需要专业化的词典,其中或许包含法律或技术术语。理想情况是,客户能够创建或购买新的词典,并把它们添加到现有应用程序中。

  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 所示。

使用 Java 平台创建可扩展应用程序

  图 2. GeneralDictionary 提供程序打包在 GeneralDictionary.jar 文件中。

  这个例子的 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 所示。

使用 Java 平台创建可扩展应用程序

  Figure 3. ExtendedDictionary 提供程序打包在 ExtendedDictionary.jar 文件中。

  很容易想像,客户使用一组完整的 Dictionary 提供程序来满足他们自己的特殊需求。服务加载程序 API 允许他们在需求或选择变化之后,将新的词典添加到应用程序中。此外,因为底层的文字处理程序应用程序是可扩展的,客户无需进行额外的编码就可以使用新的提供程序。

  Dictionary User 演示程序

  因为开发一个完整的文字处理程序应用程序是一项艰巨的任务,作者将会提供一个更加简单的应用程序,该应用程序将定义和使用 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 术语,但这个类不在应用程序的类路径中。

使用 Java 平台创建可扩展应用程序

  图 4. 没有词典提供程序,应用程序无法找到定义。

  将 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 显示了查找结果。

使用 Java 平台创建可扩展应用程序

  图 5. 应用程序在类路径上的提供程序中找到了定义。

  将提供程序添加到类路径的方法是将提供程序的 JAR 文件附加到命令行类路径参数后面。这个例子中的新提供程序是 ExtendedDictionary。以下命令行用于将它添加到应用程序中:

java -classpath DictionaryUser.jar;GeneralDictionary.jar;ExtendedDictionary.jar
com.example.demo.DictionaryUser

  现在,Dictionary User 应用程序中已经定义一些技术术语。图 6 显示了当用户添加了 ExtendedDictionary.jar 提供程序之后,搜索术语 REST 的结果:

使用 Java 平台创建可扩展应用程序

  图 6. 在另外的词典提供程序中可用的新术语。

  ServiceLoader API 的限制

  ServiceLoader API 很有用处,但是它有一些限制。例如,不能继承 ServiceLoader,所以也无法修改其行为。您可以使用自定义的 ClassLoader 子类来改变找到类的方式,但是无法扩展 ServiceLoader 本身。此外,当运行时有新的提供程序可用时,当前的 ServiceLoader 类不会告诉应用程序。另外,您无法通过添加变化监听器给加载程序,来发现是否有新的提供程序被放到特定于应用程序的扩展目录中。

  Java SE 6 中提供了公共 ServiceLoader API。尽管加载程序服务存在的时间与 JDK 1.3 一样早,但其 API 是私有的,并且只适用于内部的 Java 运行时代码。

  NetBeans 平台支持

  为应用程序提高可扩展服务的另一种方式是使用 NetBeans 平台。大多数开发人员都知道 NetBeans 集成开发环境( integrated development environment,IDE),但其中很多人都不清楚,这个 IDE 本身就是一个基于模块化通用平台构建而成的可扩展应用程序。

  NetBeans 平台为创建模块化的可扩展应用程序提供了一个完整的应用程序框架。用于用户界面、打印、模块间通信和许多其他服务的模块已经存在于平台中。开发较大的应用程序时,使用这些经过严格测试的现有 API 可以节省大量时间。

  尽管讨论整个平台已经超出了本文的范围,但它的确有一部分内容与注册、发现和使用服务提供程序有关。注册、发现和使用提供程序时所需的 API 在 org.openide.util.Lookup 类中都可以找到。这个类为应用程序提高了发现服务的能力,在简单的 ServiceLoader 类的基础上进行了重大改进。

  您不必采用整个 NetBeans 平台就能获得增强后的查找功能。只要使用平台的一个模块就可以获得提供程序查找服务。如果您拥有 NetBeans IDE,同时也就拥有了 NetBeans 平台。对于大多数人来说,从 IDE 发布获得平台很有可能是获取平台最容易的方式。通过包含 <NETBEANS_HOME>platform6lib 中的 org-openide-util.jar 文件,您可以从 ServiceLoader 类的 Java SE 6 实现获得如下好处:

  Lookup API 是可用的,即便您使用的是 Java SE Development Kit (JDK)的较早版本。

  Lookup API 可以继承,允许您自定义其功能。

  Lookup API 允许您监听服务提供程序中的变化,并对其做出响应。

  根据 NetBeans IDE 版本的不同,JAR 文件的确切位置可能也有所不同。在 NetBeans 5.5 中,文件所在位置是 <NETBEANS_HOME>platform6lib,而在 NetBeans 6.0 或更新版本中则是 platform7lib。要使用 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 的所有功能,但是它还具有另外一个优点,就是可以继承。