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 an API provider and then access the provider through the public interface. But while this allows us to reuse code at compile time -- 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, co-ordinating and testing code branches for different application variants. In this article, we will explore some common patterns for allowing our system components to be dynamically chosen and hooked together at run time.
Dynamic configuration, in this context, does not include how JavaBeans are assembled at "compile" time in a bean box, or even how an EJB tool compiles container adapters for enterprise java beans. Rather, pluggability is more often seen in APIs as banal as JDBC or as wild as Jini. In fact, as we shall 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 the case of JTAPI, just to add confusion, Peers are Factories for Providers. This article outlines the requirements for building pluggable components, reviews some of the possible strategies and offers some guidelines and caveats.
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 can 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.Memory m = new GrandLakeMemory();
Java provides a couple of key features that make this easier: dynamic class loading and interfaces (see below for descriptions of these). 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.
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 Object). This allows the usage of a class which 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. As well, these reflected variables and methods could be acted upon. As of Java 2, given the correct security rights, even private variables and methods can be accessed and invoked.
Please note two things: The purpose of this article is not to enumerate all of uses of pluggable patterns in the core or extension Java APIs. As well, 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. It relies on known interfaces for remote services. Services can then be created and registered with the service. A client connects to the service, possibly through some well-known socket port, and then looks up the required service.Usually this is designed for the 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.
Interaction Diagram
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 well, 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
More commonly used in distributed applications. This is 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 no 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. As well, there is 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. As well, a mapping table may map names to class instances or class names. See the discussion of character encoding.
Examples
CORBA Naming and Trader servicesCORBA defines remote registries that allow remote services to be resolved at run-time. The Naming service uses a flat name space, whereas the Trader service allows for services to be "brokered" based on their capabilities.RMI RegistryLike 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.JNDIJNDI defines an interface for talking to Naming and Directory services. The resolution of a particular service provider for a look-up context uses the Factory pluggable pattern (see below). It is included here since many of the naming and directory services it bridges to (RMI, EJB, LDAP) fit under this category.JavaSpacesJavaSpaces 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()". 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.Interaction Diagram
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 behaviour, 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
AWTMost 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.JTAPIThe AWT defines an abstract glue class "java.awt.Toolkit" which is responsible for getting peers for the base AWT components. A java.awt.Frame, for instance, before becoming visible, will call:
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.Toolkit.getDefaultToolkit().createFrame(this);Note as well, 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 using class file name instantiation using both system properties as well as names read from a property file.
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 behaviour, 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, which leads into...Broker
The Broker Pattern 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 (see JNDI discussion).Structure
Factories are well-known basic patterns, and exist in at least two of the basic patterns in Gamma et al.'s bible on patterns. 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 sub-patterns 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.
Interaction Diagram
Pattern usage
Generally the Broker pattern allows the actual implementation of an object to be determined at run-time 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
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
JDBCJDBC 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:JNDI Service ProviderAt 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 successively asked to handle the url until a driver on the list responds to "acceptsURL(String url)" with a true response.static { DriverManager.registerDriver(new MyDriver()); }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 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. As well, pluggable support is also given for determining a default service provider for access names that do not have a URL syntax.Byte/Character encoding/decodingJNDI searches for named entities using a context tree routed in an initial or root context. Each initial context is created by a factory that sets the default context to use to look-up does not have a URL syntax. The factory can be set through a passed in Hashtable of properties:
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.Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.fscontext.RefFSContextFactory"); Context ctx = new InitialContext(env);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.
So, 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 of these names:
We can see, then, that adding a new JNDI service provider at runtime is as simple as altering putting a new context factory and context in the classpath and then changing an environment variable.com.baz.foo.fooURLContextFactory com.grandlake.foo.fooURLContextFactoryThere 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 spi guide at ftp://ftp.javasoft.com/docs/j2se1.3/jndispi12.pdf.
Many of the java.io package Readers and Writers provide constructors that take an encoding name to specify how to translate between eight-bit bytes and Java's Unicode characters. Sun's implementation uses ByteToCharacterConvertor and CharacterToByteConvertor abstract classes to look up the actual convertors based on a encoding name. In essence, these convertors act as mapping registries between encoding names and encoders. Note however, that these are hard-coded into the implementation and so do not really fit within our dynamically pluggable domain. It would be possible, however, to use a Hashtable and allow new encoders and decoders to register themselves against various encoding canonical names.SwingIf 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:Swing RenderersIf Swing allows the look-and-feel of components to be separated from their logical behaviour 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 MVC layer built over a view.
- Solving the lowest-common-denominator problems. In AWT, the only widgets provided were those that were available on all Java platforms.
- Removing platform ideosyncracies. A composite widget that looks good on Solaris Motif may layout very differently on Windows or the 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 OS. This leads to a consistent look and behaviour independent or the platform.
At runtime, a call to UIManager.installLookAndFeel(String name, String className) allows us to add a new look-and-feel to the application set. Note that 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 use 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.
A 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 Model-View-Controller 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 run time (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 dare without the need to open up the code.Now some may be thinking that I am stretching the definition of pluggability at this point, and they 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, however, how we can add new levels of pluggability to our code by building of the dynamic and configurable capabilities of the base Java platform.
Introspection
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, however, of plugging in new components to our system. Consider a JavaBean BeanBox written in Java. At runtime, we can select an load a bean into a bean palette using its file name. There is no JavaBean class or interface, however, that JavaBeans inherit. In fact, 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. This allows us to plug-in classes of no previously known 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.
Interaction Diagram
Pattern usage
This is most often used for the manipulation of objects whose signatures cannot be known at system build time. For instance, a Java Bean 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
Java Bean buildersJavaBeans relies 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.
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. So, 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 re-usable than they have to be. Re-use is best achieved by subclassing and iterative development. After all, how can you re-use 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 yourself a re-usable framework, this doesn't really apply. But how many more frameworks do we really need?
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 sub-systems; 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 which can be easily configured for client's 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 re-coding.
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 costs.
The end