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

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
Rate and Review
Tell us what you think of the content of this page.
Excellent   Good   Fair   Poor  
Comments:
Your email address (no reply is possible without an address):
Sun Privacy Policy

Note: We are not able to respond to all submitted comments.