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 |
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
DictionaryServiceclass and a
Dictionaryinterface. The
DictionaryServiceprovides a singleton
DictionaryServiceobject that can retrieve definitions of words from
Dictionaryproviders. Dictionary service clients -- your application code -- will retrieve an instance of this service, and the service will search, instantiate, and use
Dictionaryservice providers. The underlined attributes and methods in Figure 1 are static.
Figure 1. Class diagram for the Dictionaryservice. |
Dictionaryproviders 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
DictionaryServicecan examine the application's classpath to find one or more
Dictionaryinterface 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
ServiceLoaderClass
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
ServiceLoaderclass 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
ServiceLoaderclass 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
ServiceLoaderclass 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/servicesdirectory. 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.Dictionaryservice 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
iteratormethod
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
reloadmethod.
To create a loader for a specific class, provide the class itself to the
loador
loadInstalledmethod. You can use default class loaders or provide your own
ClassLoadersubclass.
The
loadInstalledmethod searches the
runtime environment's extension directory of installed runtime
providers. The default extension location is your runtime environment's
jre/lib/extdirectory. 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
DictionaryServiceand
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
DictionaryServiceand defines a
DictionarySPI. The published SPI defines a single
Dictionaryinterface with one method. The entire interface is shown here:
package com.example.dictionary.spi; public interface Dictionary { String getDefinition(String word); } |
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
getDefinitionmethod 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/servicesdirectory. Because this example implements the
com.example.dictionary.spi.Dictionaryinterface, 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 |
Figure 2. The GeneralDictionaryprovider is packaged in the GeneralDictionary.jarfile. |
GeneralDictionaryprovider 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
ExtendedDictionaryfollows 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
GeneralDictionaryimplementation. For the
ExtendedDictionaryprovider, the file contains the following single line that declares the concrete class implementation of the SPI:
com.example.dictionary.ExtendedDictionary |
Dictionaryimplementation are shown in Figure 3.
Figure 3. The ExtendedDictionaryprovider is packaged in the ExtendedDictionary.jarfile. |
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
significant undertaking, the author will provide a more simple
application that defines and uses the
DictionaryServiceand
DictionarySPI. The Dictionary User application allows a user to type in a word and retrieve its definition from any
Dictionaryproviders on the classpath.
The
DictionaryServiceclass itself will sit in front of all
Dictionaryimplementations. The application will access the
DictionaryServiceto retrieve definitions. The
DictionaryServiceinstance will load and access available
Dictionaryproviders on behalf of the application. The
DictionaryServiceclass 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
DictionaryServiceinstance is the application's entry point to using any installed
Dictionary. Use the
getInstancemethod to retrieve the singleton service entry point. Then the application can call the
getDefinitionmethod, which iterates through available
Dictionaryproviders until it finds the targeted word. The
getDefinitionmethod returns null if no
Dictionaryinstance contains the specified definition of the word.
The dictionary service uses the
ServiceLoader.loadmethod 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
ClassLoaderinstance 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
getDefinitionmethod uses a
Dictionaryiterator to loop through the providers until it finds a definition for the specified word. The iterator method caches
Dictionaryinstances, 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
DictionaryUserclass uses this service. To use the service, the application simply creates a
DictionaryServiceand calls the
getDefinitionmethod 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
DictionaryUserimplementation. Some of the user interface layout code has been removed to make the listing easier to read. The primary point of interest is the
txtWordActionPerformedmethod. 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
DictionaryServiceobject, which in turn passes the request to its known
Dictionaryproviders.
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
GeneralDictionaryclass defines the book term, but this class is not in the application classpath.
Figure 4. Without a dictionary provider, the application cannot find definitions. |
GeneralDictionaryclass 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 |
DictionaryUser, and
GeneralDictionary. The author divided the application and API so that the
DictionaryUser.jarfile contains the
DictionaryServiceclass,
Dictionaryinterface, and the Dictionary User application itself. The
GeneralDictionary.jarfile 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. |
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 |
application. Figure 6 shows the results of a search for the term
REST after the user has added the
ExtendedDictionary.jarprovider:
Figure 6. New terms are available from additional dictionary providers. |
ServiceLoaderAPI
ServiceLoaderAPI is useful, but it has limitations. For example, it is impossible to subclass
ServiceLoader, so you cannot modify its behavior. You can use custom
ClassLoadersubclasses to change how classes are found, but
ServiceLoaderitself 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
ServiceLoaderAPI 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
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
ServiceLoaderclass.
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.jarfile from the
<NETBEANS_HOME>\platform6\libsubdirectory, you get some of the following benefits over the standard Java SE 6 implementation of the
ServiceLoaderclass:
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\libfound in NetBeans 5.5, the file may be in a
platform7\libor 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
Lookupand related APIs.
The
LookupClass
org.openide.util.Lookupclass has all the functionality of
ServiceLoaderand more. It also has an interface that allows any class to become a
Lookuptype, which simply means that the class will provide a
getLookupmethod itself. The
Lookupclass 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
Lookupsubclass that is
able to monitor a changeable classpath during application runtime,
allowing for truly dynamic service provider installations.
The systemwide
Lookupinstance default is available from the static
getDefaultmethod:
Lookup myLookup = Lookup.getDefault(); |
Lookupto return the first provider instance it finds on the classpath. Use the
Lookupinstance'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
Dictionaryprovider it finds:
Dictionary dictionary = myLookup.lookup(Dictionary.class); |
class to find and return multiple provider instances. Create a
Lookup.Templateand provide the template to the
lookupmethod. The result contains all the matching providers. The following code shows how to use
Templateand
Resultclasses to find and return all provider instances of the
Dictionaryclass.
This new
DictionaryService2class 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
DictionaryService2constructor:
private DictionaryService2() { dictionaryLookup = Lookup.getDefault(); dictionaryTemplate = new Template(Dictionary.class); dictionaryResults = dictionaryLookup.lookup(dictionaryTemplate); dictionaries = dictionaryResults.allInstances(); }
The template
lookupmethod returns a
Resultinstance that contains multiple providers, if they exist. You can retrieve the entire collection of providers by calling the
Resultinstance's
allInstancesmethod. This allows you to iterate over the collection of
Dictionaryinstances like this:
for(Dictionary d: dictionaries) { definition = d.getDefinition(word); if (d != null) break; }
Summary
ServiceLoaderclass 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
ServiceLoaderclass is available only in Java SE 6, so you may need to consider other options for earlier runtime environments. Also, the
ServiceLoaderclass is final, so you cannot modify its abilities. One alternative class is in the NetBeans platform, which provides access to extensible services with its
LookupAPI. The
Lookupclass provides all the functionality of
ServiceLoader, but it has the added benefit of being subclassable.
More Information
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.
相关文章推荐
- 2.Creating WebSocket Applications in the Java EE Platform
- Programming wireless devices with the Java 2 Platform
- More Responsive Single-Page Applications With AngularJS & Socket.IO: Creating the Library
- 诚征译者:《Programming Flex 3: The Comprehensive Guide to Creating Rich Internet Applications with Adobe Flex》
- Creating Mobile Games: Using Java ME Platform to Put the Fun into Your Mobile Device and Cell Phone
- Programming Flex 2: The comprehensive guide to creating rich media applications with Adobe Flex
- Struts in Action: Building Web Applications with the Leading Java Framework
- Building Applications with Force.com and VisualForce (DEV401) (二) : Application Essentials:Designing Application on the Force.com Platform
- Spark - A tiny Sinatra inspired framework for creating web applications in Java 8 with minimal effor
- Designing Enterprise Applications with the J2EE Platform
- Java出现No enclosing instance of type E is accessible. Must qualify the allocation with an enclosing
- The ins and outs of using Java with Domino
- Achieving Groovy-like Fluency in Java with Google Collections | The Kaptain on ... stuff
- Java出现No enclosing instance of type E is accessible. Must qualify the allocation with an enclosing
- 保存文件报错:Compilation unit name must end with .java, or one of the registered Java-like extensions
- Getting Started with Java EE 6 Applications
- Can not find a java.io.InputStream with the name [inputStream] in the invocation stack. Check the <
- struts2文件下载出现Can not find a java.io.InputStream with the name的错误
- How can I fix “Compilation unit name must end with .java, or one of the registered Java-like extensions”?
- You are attempting to build with the incorrect version of java.