Enterprise JavaPluggability PatternsThis extra flexibility comes at a cost
- By Richard Deadman
- April 12, 2000
WE HEAR A LOT of talk about Java's APIs and the power they bring to the language by decoupling the interfaces of a service from its implementation. In this regard they are more flexible than a static set of foundation classes. One of the easiest ways to use an API is to hard-code the instantiation of an API provider and then access the provider through the public interface. But while this allows us to reuse code at compile time—e.g., I pull out my animal diagnosis source I wrote for the veterinarian and pass it the fish registry I just bought from AquariumSoftware.com—wouldn't it be better if I could have plugged in the new animal registry API provider without having to open up my source code? No more tracking, coordinating, and testing code branches for different application variants. In this article, we explore some common patterns for allowing our system components to be dynamically chosen and hooked together at runtime.
Dynamic configuration, in this context, does not include how JavaBeans are assembled at compile time in a bean box, or even how an enterprise JavaBeans (EJB) tool compiles container adapters for EJBs. Rather, pluggability is more often seen in APIs as banal as JDBC or as wild as Jini. As we will see, the whole Java framework is ripe with pluggable patterns binding the generic framework to platform-, data-, or vendor-specific implementations. Sometimes the plugged-in components are called Peers; at other times they are referred to as Providers. In JTAPI, just to add confusion, Peers are Factories for Providers. I will outline the requirements for building pluggable components, review some of the possible strategies, and offer some guidelines and caveats.
Background
Traditionally, software builders use interfaces to allow sets of objects to be treated identically during application assembly. This is akin to a hardware board manufacturer knowing that all memory modules of a certain specification are identical from a pin layout and signaling point of view. It is then possible to design a board that talks to the memory module through the "interface." When the board is manufactured, a particular vendor's memory is chosen and attached to the board, somewhat akin to:
Memory m = new GrandLakeMemory();
But what if you don't want to hard-code the memory manufacturer at assembly time? What if we want to plug a SIMM into a socket? What if we want the application to discover the appropriate "provider" of a service at runtime? This would allow, for instance, the JDBC driver to be determined at the last minute so that the same application could be installed against multiple databases by only changing a configuration parameter. Late binding taken to the macroscopic level, if you will. No longer would versions of your application have to be developed and shipped for each particular combination of installation circumstances.
Java provides a couple of key features that make this easier: dynamic class loading and interfaces. Together these two features allow an unknown class to be specified, instantiated, and talked to regardless of its coding—even if it didn't yet exist when it's user class was developed.
Requirements
Providing pluggable support to your application allows for the tailoring of the application at runtime without access to source code or special tools. We then expect that the behavior of the application can be changed by some configuration technique, whether it is a configuration file, command-line parameter, or user input. To accomplish this, we need to architect our application to provide:
- Low coupling among components. The components should only communicate through known interfaces and "callback" listeners. This is usually accomplished by using a well-defined set of interfaces ( i.e., many of the Java standard and extension APIs).
- The ability to determine the component implementation at runtime.
- The ability to add a new component implementation to a prebuilt application.
- Optionally, the ability to change implementations during program execution.
Support Technologies
Before we go too much farther, let's review some of the basic Java capabilities that we will use over and over again:
Java properties: These are system properties held in a Properties object by java.lang.System. They can be set by both command-line flags (-Dname=value) and applet tags. They can also be read from a file of the common Unix ".XXX" or Windows "XXX.ini" format, allowing the simple setting of an application environment.
Class loading by name: Java can search for a class on the classpath (including a remote server) and load it given its fully qualified class name: Class c = Class.forName("com.aquariumsoftware.image.QuickRenderer");
Interfaces: Interfaces provide a common message signature, allowing two classes that implement the same interface to be of the same type, even if they share no common implementation (other than Java.lang.Object). This allows the usage of a class that supports the interface even if that class did not exist when its user was written and compiled.
Reflection: As of Java 1.1, classes could be inspected for variables and methods, including signatures. These reflected variables and methods could be acted on as well. As of Java 2, given the correct security rights, even private variables and methods can be accessed and invoked.
Patterns
Now, let's outline some of the common strategies for building pluggable components. I list here four general categories of patterns with examples lifted straight from the Java APIs. Since one of the strengths of Java is the ability to use multiple vendors' implementations for certain services, the Java Framework is heavily dependent on pluggable patterns. I chose these examples because they should be familiar to many readers. Understanding how common Java APIs support "pluggability" enables developers to leverage and use this flexibility. Please note: 1) The purpose of this article is not to enumerate all of the uses of pluggable patterns in the core or extension Java APIs. 2) Since pluggable patterns are more about discovery than structure, I use interaction diagrams instead of class diagrams to illustrate the basic patterns.
Server Lookup Pattern
Structure. This is a common pattern used in distributed computing (see Figure 1). It relies on known interfaces for remote services. Services can then be created and registered with a registry. A client connects to the registry, possibly through some well-known socket port, and then looks up the required service.
Figure 1. Server Lookup pattern.
Usually this is designed for plugging remote components together. Note that RMI, however, allows for the development of a remote Factory that returns serialized instances. RMI deserialization provides for the automatic class loading from the http server, if required.
Pattern Usage. This is most typically applied in the creation of distributed systems. A server may create a Security service and make it available to peers and clients by providing a reference to it on its well-known Naming service. The goal is not usually to provide for the dynamic hooking together of new modules as much as it is to provide for the hooking together of distributed computing components.
Certain instances of this pattern, such as JavaSpaces (used in Jini) and CORBA's seldom seen Trader service, are designed specifically to allow for the dynamic introduction of new components to each other—a dating service, if you will.
As of Java 2, RMI has taken on the CORBA activation feature. Basically this means that the registry can also act as a factory and create the registered service when it is requested.
Consequences. The Server Lookup pattern is more commonly used in distributed applications, because determining which service to instantiate and register itself requires some sort of hard-coded or dynamic configuration. Using a registry is, in essence, a way of dynamically plugging in a configured service. If the system is not distributed, the selection of the service is usually registered with the code directly instead of through some registry.
Using distributed services and frameworks is more difficult to build and test and requires handling of network errors. There is also a performance cost to distributed processing, both in terms of application response and network traffic.
That said, a resource pool for, say, JDBC connections, can also be thought of as a registry. Also, a mapping table may map names to class instances or class names. (See the character encoding sidebar.)
Examples.
CORBA Naming and Trader services. CORBA defines remote registries that allow remote services to be resolved at runtime. The Naming service uses a flat name space, whereas the Trader service allows for services to be "brokered" based on their capabilities.
RMI registry. Like the CORBA Naming service, it provides service registry at a well-known port (typically 1099) on a server machine. For security reasons, only the server machine is trusted for service registry. Clients may look up remote references to RMI services and automatically load the appropriate client-side stub classes.
JNDI. JNDI defines an interface for talking to Naming and Directory services. The resolution of a particular service provider for a lookup context uses the Broker pluggable pattern (see Broker Pattern section). It is included here since many of the naming and directory services it bridges to (RMI, EJB, LDAP) fit under this category.
JavaSpaces. JavaSpaces and IBM's TSpaces are both tuple-based remote lookup services based on a project called Linda. They provide a template-based registry for the putting, viewing, and taking of Java objects from a remote space. Jini, for instance, uses JavaSpaces for the registration of remote device existence and capabilities. A client can then search the JavaSpace for all devices with some profile, wild cards being supported. The returned objects are then basically calling cards for connecting to the real device.
Class Name Pattern
Structure. In this pattern, an object is instantiated using Class.forName() (see Figure 2). Usually the instantiated class is then cast to some expected interface. This allows the implementation of a particular class to be defined though an environment variable, Java property, Properties file, Resource Bundle, or some other configuration technique.
Figure 2. Class Name pattern.
Pattern Usage. This is most useful in the simple specification of local component implementations. It allows the deployer to include a new component implementation in the application class set and then hook it in by simply changing a system property or PropertyResourceBundle file.
Consequences. This is useful only if the configured classes are specified solely by their behavior or class definition. Of course, class instances can also be configured with plugged-in data during construction, but this is usually thought of as data population rather than as component "plugging."
Examples.
AWT. Most developers realize that the AWT, and by extension Swing, are built on a peer framework that couples the platform-independent widget classes to delegates, or peers, that perform the platform-specific rendering. What most developers don't realize, however, is that this peer mapping is not hard-coded by the VM vendor.
The AWT defines an abstract glue class java.awt.Toolkit responsible for getting peers for the base AWT components. A java.awt.Frame, for instance, before becoming visible, will call:
Toolkit.getDefaultToolkit().createFrame(this);
Toolkit itself is abstract. The actual mapping toolkit is found by looking for a fully qualified class name in the system property
awt.toolkit, or by loading a default on (
sun.awt.motif.MToolkit) if the property is not set. It is entirely possible, then, to write your own AWT peer implementation and hook it into any Java application by simply setting the
awt.toolkit java.lang.System property.
Note that as of Java 2, the first call to getDefaultToolkit() will also read a list of assistive technologies class files from the accessibility.properties file and instantiate instances of each of these classes. In one fell swoop, then, the AWT plugs in components that use class file name instantiation, using both system properties as well as names read from a property file.
JTAPI. While a service provider may provide a DefaultJtapiPeer class, the preferred way of plugging a JTAPI service into a JTAPI application is to pass the class name into the static JtapiPeerFactory.getJtapiPeer(String classname) method. Other than some default behavior, this factory does almost nothing other than to load and instantiate the referenced class. A JTAPI Peer, it turns out, is nothing more than a broker for JTAPI providers.
Broker Pattern
The Broker pattern (see Figure 3) provides indirect service lookup based on some service requirements. Unlike a Server Lookup, it is generally local and applies some algorithm to the service determination. It may also be linked to the Class Name pattern if services are dynamically searched for based on a naming convention.
Figure 3. Broker pattern.
Structure. Factories are well-known basic patterns and exist in at least two of the basic patterns in the Gang of Four patterns bible.1 In the context of pluggable systems, Brokers may act like factories by providing an appropriate instance based on input information. Usually, however, they do not create a new instance as much as find one. Of course they may find an appropriate factory and then ask the factory to create an instance for the caller.
There are variations on this. Sometimes a service factory is plugged in itself—although this would more properly fall under the Class Name pattern. Other subpatterns include service providers registering interest in certain types at load time, or service providers being searched for from a combination of registered package prefixes with generated class names.
Pattern Usage. Generally the Broker pattern allows the actual implementation of an object to be determined at runtime by some input criteria other than the class name. For a pluggable pattern, there is some capability to either register new implementations with the broker or to have the broker search for a previously unknown implementation (or implementation factory) based on some naming convention.
In either case, the user of the factory requests a service, passing in some input criteria. This is often a URL. The factory strips information from the URL and applies a lookup algorithm to find the appropriate object or provider.
Consequences. Broker usually requires access to environment properties on the JVM. In most cases it also requires the management or addition of new classes on the VM's classpath.
Examples.
JDBC. JDBC provides an interesting Broker pattern more complex than most in this article. JDBC providers are either listed in a colon-separated list of classes in the system property jdbc.drivers or are manually loaded in an application though the use of Class.forName(fullyQualifiedClassName). In either case, the driver is loaded and it is expected that the class's static init() method will call the DriverManager.registerDriver() static method, passing in an instance of itself:
static {
DriverManager.registerDriver(new
MyDriver());
}
At this point, the
DriverManager simply has a handle on the driver as yet another possible handler for JDBC connection requests. When an application calls the
DriverManager with the call
DriverManager.getConnection(String url), the list of
registered Drivers are successively asked to handle the URL until a driver on the list responds to
acceptsURL(String url) with a true response.
This pattern essentially uses that DriverManager as a broker that asks its stable of drivers for someone to belly up and accept a job based on the job's description.
JNDI service provider. JNDI also uses strings and URLs to lookup the service provider. In the case of JNDI, however, service providers do not register themselves but instead are searched for using a naming pattern. Pluggable support is also given for determining a default service provider for access names that do not have a URL syntax.
JNDI searches for named entities using a context tree routed in an initial or root context. JNDI allows the plugging in of an initial context factory that brokers initial contexts for JNDI references. This factory will return a default context for JNDI references without a URL syntax. The factory can be set through a passed-in Hashtable of properties:
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.fscontext.RefFSContextFactory");
Context ctx = new InitialContext(env);
Alternatively, this initial factory can also be set through the
java.lang.System environment variable
java.naming.factory.initial, which may be passed in to the JVM using the
-D command-line parameters. Changing the plugged-in initial JNDI context is therefore as easy as changing a start-up script.
We see, then, how JNDI allows us to plug in a default starting point for our search. If a URL is sent to an initial context, however, the message is not sent off to the default context. Instead, the InitialContext tries to load a context with a particular fully qualified name. There is another environment variable used for this: java.naming.factory.url. This contains a colon-delimited list of class-name prefixes. When an unregistered URL is received, the InitialContext tries to load a context factory by appending a URL-derived string to each prefix.
For instance, if we do a lookup on foo://bar and our colon-separated list contains com.baz:com.grandlake, the InitialContext will look for a foo factory with either the name com.baz.foo.fooURLContextFactory or com.grandlake.foo.fooURLContextFactory.
Adding a new JNDI service provider at runtime is as simple as putting a new context factory and context in the classpath and then changing an environment variable.
There is, of course, a lot more to the JNDI service provider framework; it is a complex issue worthy of an article all to itself. The topic is covered in the javax.naming and javax.naming.spi javadocs as well as in the service provider interface (SPI) guide at http://java.sun.com/j2se/1.3/pdf/jndispi.pdf. (See sidebar on p. 56.)
Swing. If Swing is built on top of AWT and its pluggable peers, why am I listing it again? Well, most obviously, because Swing provides a pluggable look-and-feel. Whereas the AWT peer defines how a logical component will be visualized by native system calls, the Swing look-and-feel allows for the window decorations to be decoupled from the native widget set. Basically, Swing bypasses native widgets and renders all Swing widgets as drawings on a basic borderless window. At a cost of losing native widget performance, this allows for three things:
- Solving the lowest-common-denominator problems. In AWT, the only widgets provided were those that were available on all Java platforms.
- Removing platform idiosyncrasies. A composite widget that looks good on Solaris Motif may layout very differently on Windows or a Mac. If text fields are clipped, an application that is supposed to be "write once, run anywhere" can end up losing this feature.
- Allowing a platform look-and-feel independent of the operating system (OS). This leads to a consistent look and behavior independent of the platform.
If Swing allows the look-and-feel of components to be separated from their logical behavior as well as the OS, how is this accomplished? Well, it turns out that Swing supports a
UIManager class that keeps track of the available look-and-feels and the currently used look-and-feel. When a component needs to be rendered, a call is made to the
UIManager to get the appropriate
UIComponent (presentation) of the logical component. Essentially, we have a Model-View-Controller (MVC) layer built over a view.
At runtime, a call to UIManager.installLookAndFeel(String name, String className) allows us to add a new look-and-feel to the application set. These could be easily read from a properties file. This does not change the look-and-feel, but simply makes it available. A call to one of the UIManager.setLookAndFeel() methods will actually change the look-and-feel for the application, even updating current widgets. Often a menu item will present installed look-and-feels and allow the user to choose.
Note: While it is easy to see how we could plug in and set a new look-and-feel, creating a new look-and-feel may not be trivial. Creating one from scratch is a huge undertaking; simply extending an existing look-and-feel is more manageable.
Not only can Swing provide for a pluggable look-and-feel, it also offers the very powerful concept of renderers and editors. Using the MVC paradigm, Swing decouples the presentation from the objects being viewed. Through the use of adapters, or renderers, however, Swing allows for the presentation of an object to be plugged in instead of hard-coded into traditional subclasses. We can, for instance, assign a default renderer for a class at runtime (assuming that the table column does not have a renderer assigned to it). By reading class names and their corresponding renderer classes from a property file, we could add the capability to change the set of loaded default renderers for a table. Then new renderers could be written and added to the application at a later date without the need to open up the code.
You may be thinking that I am stretching the definition of pluggability at this point, and you are probably correct. Renderers and Editors, after all, are usually hard-coded into an application to allow it to deal with application-specific model representation. Considering how we could use the Swing Renderer capability to provide a pluggable rendering capability allows us to ponder how we can add new levels of pluggability to our code by building the dynamic and configurable capabilities of the base Java platform.
Introspection Pattern
Structure. Up until now we have dealt with patterns that generally use well-known interfaces for hooking up plugged-in services. There is another way, though, of plugging in new components to our system. Consider a JavaBean BeanBox written in Java. At runtime, we can select and load a bean into a bean palette using its file name. There is no JavaBean class or interface that JavaBeans inherit. Since beans can have any method names, how could there be?
The answer lies in Java's ability to introspect over objects and both discover their method signatures as well as call these methods (see Figure 4). This allows us to plug in classes of previously unknown signature and manipulate them at runtime. As of Java 2, we are even able to manipulate private instance variables. This is particularly useful in the development of automatic object-persistence frameworks.
Figure 4. Introspection pattern.
Pattern Usage. This is most often used for the manipulation of objects whose signatures cannot be known at system build time. For instance, a JavaBean BeanBox box can present and manipulate beans of an unknown type. Many other tools, such as object-to-relational database mapping tools and object-persistence frameworks also use this feature.
Consequences. Introspection is slow and requires the coder to manage type-checking. It is really only useful when we cannot know the type (interface or class) of an object beforehand, or when we must deal generically with a large set of classes with no common superclass other than Object.
Examples.
JavaBean builders. JavaBeans rely on introspection and a naming pattern. Any class that has a public empty constructor and a set of setXXX(Object y) and getXXX() methods is a valid bean. When the class is loaded, the tool uses the default constructor to instantiate the bean. It then can use introspection to infer the existence of attributes from the getter and setter methods. In essence, it knows how to manipulate the class at runtime without knowing its type.
Guidelines
As we all learned in adolescence, just because you can do something, doesn't mean you should. There are costs to pluggable systems. Generally you have to add an extra layer of complexity to your application. This may involve the definition of reusable interfaces, or the development of factories or service repositories. For Swing, it requires the decoupling of the look-and-feel from the widget behavior, complicating the design, breaking some encapsulation, and effecting performance.
So, here's guideline #1: Don't! Apologies to the author of the optimization rule with the same content.
For those who don't like guideline #1, we can also apply the second rule of optimization: Don't yet.
Building pluggable patterns not only adds to the volume of code and development time, it also increases complexity and the chances for errors. Down the road, it will lead to extra headaches at support time. Wait until you are sure that your application is going to require the benefits of pluggability before you impose one of these techniques. Some may argue that these decisions need to be made during the architecture and design phase. That is true, but aren't you using iterative design? There will always be time to go back and refactor your program to make a component pluggable later. It may be as simple as building an adapter class that delegates from the interface methods to your original classes. If you avoid building yourself just one pluggable pattern, you will have saved the project a great deal.
In essence, this is an extension of the argument that you should not make your objects any more reusable than they have to be. Reuse is best achieved by subclassing and iterative development. After all, how can you reuse something that you haven't first used? Trying to create the all-time generic invoice object for all financial applications will either send you into a navel-gazing architectural morass or it will simply fail. The more flexible something is, the more complex and slow it will become. Once again, look at Swing.
Don't get me wrong, I'm not implying that you hard-code the JDBC driver class name into your source code. If the pluggable capability is already developed, by all means use it. But don't develop it until you know, really know, you need it. Of course, if you are building a reusable framework, this doesn't really apply. But how many more frameworks do we really need?
More Guidelines
All right, so you really do need a pluggable pattern to allow the system to select among different password server bridges. Here are some guidelines to follow:
- Don't reinvent the wheel. Look for an existing API that does what you want, or most of what you want. For instance, say you want to set up a distributed user properties framework. Before defining your own, see if you could use JavaSpaces to store java.util.Properties objects by user, role, or group.
- Define the simplest API for your needs. Just because you can add a listener event that notifies the client of changes to the Factory's registered implementations, doesn't mean you should. Cool often means complex, which leads to extra work and more potential for bugs. Let your development iterations take care of unforeseen requirements.
- Model your system with the lowest coupling possible between components. The greater the number of object handles between two system components, the harder it is to build replacement implementations. High coupling tends to dictate the component's composition. One technique to reduce the number of public interfaces to a component is to apply the Façade pattern.1
- Use the simplest pluggable strategy available. No sense setting up a remote service registry if all you need to do is read the implementation class name from a property file, instantiate it and plug it in.
Parting Shot
Interestingly, the common JVM can be thought of as using a pluggability pattern during initialization. After all, it reads a Java class name as a command-line parameter, loads the class, and calls its main method. As such, it uses dynamic class loading and introspection to plug a new control object into the VM.
As I said at the beginning of the article, the whole Java framework is ripe with examples of pluggable layers that allow applications to be decoupled from implementation choices. Some APIs, like JTAPI, are quite explicit in their use of pluggable subsystems; others, like AWT, hide it almost completely. A Java API without the ability to plug in service providers independent of the VM is, however, severely crippled.
Pluggable patterns offer a Java system architect extra flexibility to design systems that can be easily configured for clients' needs. For enterprise applications, in particular, this is very powerful in that an application can be attached to different databases, LDAP servers, CORBA vendors, and telephony systems at deployment time without any recoding. As with all functionality, this extra flexibility comes at a cost. Pluggable interfaces are more expensive to design, implement, and maintain and so should be used only when their value outweighs their cost.
Reference
- Gamma, E., et al., Design Patterns: Elements of Reusable Object-Oriented Software, AddisonWesley, 1995.
Byte/Character Encoding/Decoding |
Many of the java.io package Readers and Writers provide constructors that take an encoding name to specify how to translate between 8-bit bytes and Java's Unicode characters. Sun's implementation uses ByteToCharacterConvertor and CharacterToByteConvertor abstract classes to look up the actual converters based on an encoding name. In essence, these converters act as mapping registries between encoding names and encoders. However, these are hard-coded into the implementation and so do not really fit within our dynamically pluggable domain. It would be possible to use a Hashtable and allow new encoders and decoders to register themselves against various canonical encoding names. |