您的位置:首页 > 移动开发

Creating Extensible Applications With the Java Platform

2013-04-24 06:48 549 查看

Lookup API Outside the NetBeans Platform

Creating Extensible Applications With the Java Platform

——————————————————————————————————————————————————————————————————

Creating Extensible Applications With the Java Platform

By John O'Conner, September 2007

Articles Index

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. The
DictionaryService
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.

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's
jre/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. The
DictionaryService
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;
}
}


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;
}
}


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 the
ExtendedDictionary.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 custom
ClassLoader
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 the
org-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 the
ServiceLoader
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 a
platform7\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 the
lookup
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 the
Dictionary
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's
allInstances
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.


                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐