By John O'Conner, September 2007
An extensible application is one that you can extend easily without modifying its original code base. You can enhance its functionality with new plug-ins or modules. Developers, software vendors, and even customers can add new functionality or application programming interfaces (APIs) by simply adding a new Java Archive (JAR) file onto the application classpath or into an application-specific extension directory.
This article describes two ways to create applications with extensible services, which allow you or others to provide service implementations that require no modifications to the original application. By designing an extensible application, you provide an easy way to upgrade or enhance specific parts of a product without changing the core application.
One example of an extensible application is a word processor that allows the end user to add a new dictionary or spelling checker. In this example, the word processor provides a dictionary or spelling feature that other developers, or even customers, can extend by providing their own implementation of the feature. Another example is the NetBeans IDE, which in many cases allows users to add editors and other modules without restarting the application.
Definitions
A service is a set of programming interfaces and classes that provide access to some specific application functionality or feature. The service may simply define the interfaces for the functionality and a way to retrieve an implementation. In the word-processor example, a dictionary service can define a way to retrieve a dictionary and the definition of a word, but it does not implement the underlying feature set. Instead, it relies on a service provider to implement that functionality.
A service provider interface (SPI) is the set of public interfaces and abstract classes that a service defines. The SPI defines the classes and methods available to your application.
A service provider implements the SPI. An application with extensible services will allow you, vendors, and perhaps even customers to add service providers without modifying the original application.
Dictionary Service Example
Consider how you might design a dictionary service in a word processor or editor. One way would be to define a DictionaryService
class and a Dictionary
interface. TheDictionaryService
provides a singleton DictionaryService
object that can retrieve definitions of words from Dictionary
providers. Dictionary service clients -- your application code -- will retrieve an instance of this service, and the service will search, instantiate, and use Dictionary
service providers. The underlined attributes and methods in Figure 1 are static.
Figure 1. Class diagram for the Dictionary
service.
Click here for a larger image
All Dictionary
providers must register their presence with the service. Otherwise, the service will not know how to find them. Developers can register interfaces in a variety of ways, but one of the most common ways is to simply use your application's classpath. Services can examine the classpath to find interface implementations. In this case, the DictionaryService
can examine the application's classpath to find one or more Dictionary
interface providers.
Although the word-processor developer would most likely provide a basic, general dictionary with the original product, the customer might require a specialized dictionary, perhaps containing legal or technical terms. Ideally, the customer would be able to create or purchase new dictionaries and add them to the existing application.
The ServiceLoader
Class
The Java SE 6 platform provides a new API that helps you find, load, and use service providers. The java.util.ServiceLoader
class has been quietly performing its job in the Java platform since the 1.3 version, but it has become a public API in Java SE 6.
The ServiceLoader
class searches for service providers on your application's classpath or in your runtime environment's extensions directory. It loads them and allows your application to use the provider's APIs. If you add new providers to the classpath or runtime extension directory, the ServiceLoader
class will find them. If your application knows the provider interface, it can find and use different implementations of that interface. You can use the first loadable instance of the interface or even iterate through all the available interfaces.
The ServiceLoader
class is final, which means that you cannot subclass or override its loading algorithms. You cannot, for example, change its algorithm to search for services from a different location.
From the perspective of the ServiceLoader
class, all services have a single type, which is usually a single interface or abstract class. The provider itself contains one or more concrete classes that extend the service type with an implementation specific to its purpose. The ServiceLoader
class requires that the single exposed provider type has a default constructor, which requires no arguments. This allows the ServiceLoader
class to easily instantiate the service providers that it finds.
Define a service provider by implementing the service provider API. Usually, you will create a JAR file to hold your provider. To register your provider, you must create a provider configuration file in the JAR file's META-INF/services
directory. The configuration file name should be the fully qualified binary name of the service's type. The binary name is simply the fully qualified class name in which each component of the name is separated by a .
character, and nested classes are separated by a $
character.
For example, if you implement the com.example.dictionary.spi.Dictionary
service type, you should create a META-INF/services/com.example.dictionary.spi.Dictionary
file. On separate lines within the file, list the fully qualified binary names of your concrete implementations. The file must be UTF-8 encoded. Additionally, you can include comments in the file by beginning the comment line with the #
character.
A service loader will ignore duplicate provider class names in either the same configuration file or other configuration files. Although you will most likely put the configuration file within the same JAR file as the provider class itself, this is not strictly necessary. However, the provider must be accessible from the same class loader that was initially queried to locate the configuration file.
Providers are located and instantiated on demand. A service loader maintains a cache of the providers that have been loaded. Each invocation of the loader's iterator
method returns an iterator that first yields all of the elements of the cache, in instantiation order. It then locates and instantiates any new providers, adding each one to the cache in turn. You can clear the provider cache with the reload
method.
To create a loader for a specific class, provide the class itself to the load
or loadInstalled
method. You can use default class loaders or provide your own ClassLoader
subclass.
The loadInstalled
method searches the runtime environment's extension directory of installed runtime providers. The default extension location is your runtime environment'sjre/lib/ext
directory. You should use the extension location only for well-known, trusted providers because this location becomes part of the classpath for all applications. In this article, providers will not use the extension directory but will instead depend on an application-specific classpath.
Dictionary Provider Implementation
This section describes how to implement the DictionaryService
and Dictionary
provider classes described earlier in this article. Providers are not always implemented by the original application vendor. In fact, anyone can create a service provider if they have the SPI specification, which tells them what interface to implement. The example word-processor application provides a DictionaryService
and defines a Dictionary
SPI. The published SPI defines a single Dictionary
interface with one method. The entire interface is shown here:
package com.example.dictionary.spi;
public interface Dictionary {
String getDefinition(String word);
}
To provide this service, you must create a Dictionary
implementation. To keep things simple for now, start by creating a general dictionary that defines just a few words. You can implement the dictionary with a database, a set of property files, or any other technology. The easiest way to demonstrate the provider pattern is to include all the words and definitions within a single file.
The following code shows a possible implementation of this SPI. Notice that it provides a no-argument constructor and implements the getDefinition
method defined by the SPI.
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);
}
}
Before you compile and create this provider's JAR file, only one task remains. You must comply with the provider registration requirement to create a configuration file in your project and JAR file's META-INF/services
directory. Because this example implements the com.example.dictionary.spi.Dictionary
interface, you create a file of the same name within the directory. The contents should contain a single line listing the concrete class name of the implementation. In this case, the file contents look like this:
com.example.dictionary.GeneralDictionary
The final JAR contents will contain files as shown in Figure 2.
Figure 2. The GeneralDictionary
provider is packaged in the GeneralDictionary.jar
file.
The GeneralDictionary
provider for this example defines just two words: book and editor. Obviously, a more usable dictionary would provide a more substantial list of generally used vocabulary.
To use the GeneralDictionary
, you should place its deployment JAR file, GeneralDictionary.jar
, into the application's classpath.
To demonstrate how multiple providers can implement the same SPI, the following code shows yet another possible provider. This provider is an extended dictionary containing technical terms familiar to most software developers.
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);
}
}
This additional ExtendedDictionary
follows the same pattern as the GeneralDictionary
: You must create a configuration file for it and place the JAR file in your application's classpath. The configuration file should again be named using the SPI class name of com.example.dictionary.spi.Dictionary
. This time, however, the file contents will be different from the GeneralDictionary
implementation. For the ExtendedDictionary
provider, the file contains the following single line that declares the concrete class implementation of the SPI:
com.example.dictionary.ExtendedDictionary
The files and structure for this additional Dictionary
implementation are shown in Figure 3.
Figure 3. The ExtendedDictionary
provider is packaged in the ExtendedDictionary.jar
file.
It is easy to imagine customers using a complete set of Dictionary
providers for their own special needs. The service loader API allows them to add new dictionaries to their application as their needs or preferences change. Moreover, because the underlying word-processor application is extensible, no additional coding is required for customers to use the new providers.
Dictionary User Demo
Because developing a full word-processor application would be a significant undertaking, the author will provide a more simple application that defines and uses the DictionaryService
and Dictionary
SPI. The Dictionary User application allows a user to type in a word and retrieve its definition from any Dictionary
providers on the classpath.
The DictionaryService
class itself will sit in front of all Dictionary
implementations. The application will access the DictionaryService
to retrieve definitions. TheDictionaryService
instance will load and access available Dictionary
providers on behalf of the application. The DictionaryService
class source code is here:
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;
}
}
The DictionaryService
instance is the application's entry point to using any installed Dictionary
. Use the getInstance
method to retrieve the singleton service entry point. Then the application can call the getDefinition
method, which iterates through available Dictionary
providers until it finds the targeted word. The getDefinition
method returns null if no Dictionary
instance contains the specified definition of the word.
The dictionary service uses the ServiceLoader.load
method to find the target class. The SPI is defined by the interface com.example.dictionary.spi.Dictionary
, so the example uses this class as the load method's argument. The default load method searches the application classpath with the default class loader.
However, an overloaded version of this method allows you to specify custom class loaders if you wish. That would allow you to do more sophisticated class searches. A particularly enthusiastic programmer might, for example, create a ClassLoader
instance that could search in an application-specific subdirectory that contains provider JARs added during runtime. The result would be an application that would not require a restart to access new provider classes.
Once a loader for this class exists, you can use its iterator method to access and use each provider that it finds. The getDefinition
method uses a Dictionary
iterator to loop through the providers until it finds a definition for the specified word. The iterator method caches Dictionary
instances, so successive calls require little additional processing time. If new providers have been placed into service since the last invocation, the iterator method adds them to the list.
The DictionaryUser
class uses this service. To use the service, the application simply creates a DictionaryService
and calls the getDefinition
method when the user types a searchable word. If a definition is available, the application displays it. If a definition is not available, the application displays a message stating that no available dictionary carries the word.
The following code listing shows most of the DictionaryUser
implementation. Some of the user interface layout code has been removed to make the listing easier to read. The primary point of interest is the txtWordActionPerformed
method. This method runs when the user presses the Enter key within the application's text field. The method then requests a definition of the target word from the DictionaryService
object, which in turn passes the request to its known Dictionary
providers.
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;
}
Figure 4 shows the warning message pane that the application displays when the target word book is not available. The GeneralDictionary
class defines the book term, but this class is not in the application classpath.
Figure 4. Without a dictionary provider, the application cannot find definitions.
You can put the GeneralDictionary
class on the classpath by adding it to the command-line classpath argument of the runtime environment. The following command line adds the dictionary to a Microsoft Windows runtime classpath:
java -classpath DictionaryUser.jar;GeneralDictionary.jar
com.example.demo.DictionaryUser
Notice that this command line references two JAR files: DictionaryUser
, and GeneralDictionary
. The author divided the application and API so that the DictionaryUser.jar
file contains the DictionaryService
class, Dictionary
interface, and the Dictionary User application itself. The GeneralDictionary.jar
file contains the provider implementation.
Using the newly available provider, the Dictionary User application now finds the word. Figure 5 shows the result.
Figure 5. The application finds definitions in providers found on the classpath.
Add providers to the classpath by appending the provider's JAR file to the command line classpath argument. The new provider in this example is ExtendedDictionary
. The following command line would add it to the application:
java -classpath DictionaryUser.jar;GeneralDictionary.jar;ExtendedDictionary.jar
com.example.demo.DictionaryUser
Now some technical terms are defined in the Dictionary User application. Figure 6 shows the results of a search for the term REST after the user has added theExtendedDictionary.jar
provider:
Figure 6. New terms are available from additional dictionary providers.
Limitations of the ServiceLoader
API
The ServiceLoader
API is useful, but it has limitations. For example, it is impossible to subclass ServiceLoader
, so you cannot modify its behavior. You can use customClassLoader
subclasses to change how classes are found, but ServiceLoader
itself can't be extended. Also, the current ServiceLoader
class can't tell your application when new providers are available at runtime. Additionally, you cannot add change-listeners to the loader to find out whether a new provider has been placed into an application-specific extension directory.
The public ServiceLoader
API is available in Java SE 6. Although the loader service existed as early as JDK 1.3, the API was private and only available to internal Java runtime code.
NetBeans Platform Support
An alternate way to provide extensible services for an application is to use the NetBeans platform. Most developers know the NetBeans integrated development environment (IDE), but many are unaware that the IDE itself is an extensible application built upon modular, general platform.
The NetBeans platform provides a complete application framework for creating modular, extensible applications. Modules for user interface, printing, intermodule communication, and many other services already exist in the platform. Using these existing, well-tested APIs can save you time developing a larger application.
Although the entire platform is beyond this article's scope, it does have a subset of pertinent facilities for registering, discovering, and using service providers. Most of the APIs you need for registering, finding, and using providers are available from the org.openide.util.Lookup
class. This class provides applications with the ability to find services and is a significant improvement over the simple ServiceLoader
class.
You don't have to adopt the entire NetBeans platform to get enhanced lookup functionality. You can get provider lookup services by using just a single module of the platform. If you have the NetBeans IDE, you also have the NetBeans platform. Getting the platform from the IDE distribution is probably the easiest way for most people to acquire the platform. By including theorg-openide-util.jar
file from the <NETBEANS_HOME>\platform6\lib
subdirectory, you get some of the following benefits over the standard Java SE 6 implementation of theServiceLoader
class:
- The Lookup API is available even if you use earlier versions of the Java SE Development Kit (JDK).
- The Lookup API can be subclassed, allowing you to customize its functionality.
- The Lookup API allows you to listen and respond to changes in service providers.
The exact location of the JAR file may be different depending on your NetBeans IDE version. Instead of <NETBEANS_HOME>\platform6\lib
found in NetBeans 5.5, the file may be in aplatform7\lib
or different subdirectory if you use NetBeans 6.0 or later. To use org-openide-util.jar
, you should add it to your compile and runtime classpath. Although this JAR file contains many utilities, this article will use only the utilities for the Lookup
and related APIs.
The Lookup
Class
The org.openide.util.Lookup
class has all the functionality of ServiceLoader
and more. It also has an interface that allows any class to become a Lookup
type, which simply means that the class will provide a getLookup
method itself. The Lookup
class provides a default Lookup
instance that searches the classpath. The examples in this article use the default. However, it would be relatively easy for a programmer to create a customized Lookup
subclass that is able to monitor a changeable classpath during application runtime, allowing for truly dynamic service provider installations.
The systemwide Lookup
instance default is available from the static getDefault
method:
Lookup myLookup = Lookup.getDefault();
In the most basic case, you can use Lookup
to return the first provider instance it finds on the classpath. Use the Lookup
instance's lookup
method for that purpose. Provide the targeted class as the method argument. The following code will find and return an instance of the first Dictionary
provider it finds:
Dictionary dictionary = myLookup.lookup(Dictionary.class);
Using version 5.5 of the NetBeans platform, you must use a template class to find and return multiple provider instances. Create a Lookup.Template
and provide the template to thelookup
method. The result contains all the matching providers. The following code shows how to use Template
and Result
classes to find and return all provider instances of theDictionary
class.
This new DictionaryService2
class provides the same functionality as the original DictionaryService
class. The difference is that the new implementation uses the NetBeans Platform APIs, which work on earlier versions of the JDK and provide the benefits described earlier.
/*
* 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;
}
}
In particular, notice the way to get multiple provider instances. That code is shown in the private DictionaryService2
constructor:
private DictionaryService2() {
dictionaryLookup = Lookup.getDefault();
dictionaryTemplate = new Template(Dictionary.class);
dictionaryResults = dictionaryLookup.lookup(dictionaryTemplate);
dictionaries = dictionaryResults.allInstances();
}
The template lookup
method returns a Result
instance that contains multiple providers, if they exist. You can retrieve the entire collection of providers by calling the Result
instance'sallInstances
method. This allows you to iterate over the collection of Dictionary
instances like this:
for(Dictionary d: dictionaries) {
definition = d.getDefinition(word);
if (d != null) break;
}
Summary
Extensible applications provide service points that can be extended by service providers. The easiest way to create an extensible application is to use the ServiceLoader
class available in the Java SE 6 platform. Using this class, you can add provider implementations to the application classpath to make new functionality available.
The ServiceLoader
class is available only in Java SE 6, so you may need to consider other options for earlier runtime environments. Also, the ServiceLoader
class is final, so you cannot modify its abilities. One alternative class is in the NetBeans platform, which provides access to extensible services with its Lookup
API. The Lookup
class provides all the functionality of ServiceLoader
, but it has the added benefit of being subclassable.
More Information
- Read the new book about using the NetBeans platform: Rich Client Programming, Plugging Into the NetBeans Platform.
- Read the NetBeans platform documents and tutorials at the NetBeans platform home page.
- Read the Javadoc for the ServiceLoader API.
- Download the demo source code for this article.
http://java.sun.com/developer/technicalArticles/javase/extensible/
No comments:
Post a Comment