I was recently faced with a problem where it was required to dispatch between different versions of the same library at runtime. We have this architecture where network management software (lets call it the "master") has to communicate with different other software components somewhere on (remote) hardware devices in the network. This communication goes over various protocols, some Java specific (for example RMI using springs http invoker), some not (for example telnet). We also have the requirement to support different versions of these remote devices from within this single "master".
This UML diagram (click to enlarge) shows an example possible situation where the "master" needs to communicate with two different versions of two devices. There are two instances of device A in the network, one with version X and one with version Y. There are also two instances of device B, one with version Z and one with version Q. A driver implementation exists for every version of the device, but ultimately each device type (A or B) has a single interface to be used by the "master". It is the drivers job to abstract away the differences between different versions of the same device type. If the protocol is telnet or something like that, we wouldn't really need different driver implementations. A naive implementation of the "master" could simply switch around the different possible versions with "if" statements (if this version send telnet command X, if that version send telnet command Y, etc). If the protocol is Java, this becomes a problem, because you then typically depend on a library which specifies the interfaces and types used for the communication. This is technically not required of course (you could manually write RMI to a raw byte output stream), but it is what you generally want. Event for the non Java protocols, you might want to abstract away the different commands required for different versions behind a single interface in different implementation libraries.
We discussed (and experimented with) different alternative solutions to this problem. I'll go through each of them, and explain the pros and cons.
idea 1: custom class loading magicThe first idea was to package the driver implementation libraries in jar files (together with their dependencies), and class load the appropriate version of such a driver at runtime. This also requires the usage of reflection to actually instantiate such a driver. The usage of reflection prevents compile time dependencies on the driver implementation libraries (this is impossible because we would have to depend on different implementations of the same driver interface at the same time). The compile time dependencies are from the "master" to the driver interfaces, and from the driver implementation libraries to the remote device interfaces.
To make this idea work, the class loaders of the "master" and the driver implementation libraries have to be separated. On the other hand, the driver interfaces (used to communicate between the two) have to be known by both. In Java, this is only possible if they both use the driver interface classes from the same class loader. This means that the driver class loader has to have a dependency on the "master" class loader, such that driver interfaces are loaded from there. Having the "master" class loader as parent from the driver library class loader solves this problem.
As a consequence, the driver will find all its libraries in the parent class loader first, unless they are not available there. For libraries on which the driver and the "master" both depend (in our situation, spring was such a library), this is a problem, because from within these classes, the classes loaded by the class loader from the driver itself are not visible. We found a solution to this problem by writing a custom class loader which inverses the regular pattern for finding classes (first in own class loader, then in parent, in stead of first in the parent). This solves the problem, but we have to make sure that the driver interfaces are loaded from the parent class loader (by not packaging them together with the driver implementation libraries).
In the end, this approach does work, but the implementation is scattered around the different components. The "class loading magic" happens in the "master", but it only works if the driver implementation libraries are correctly packaged. This correct packaging depends on whether certain libraries are used by the master and the driver together, or only by the driver interface, etc.
idea 2: deploy the drivers as WAR filesThe second idea was to package the driver implementation libraries as WAR files, and to give them a remote interface (RMI or something like that). This would imply that the container would take care of all the separation of class loaders, which is nice.
To understand the downside of this approach, you should know that the "master" doesn't simply uses these drivers once when it has to communicate with a remote device, and then forgets about them. The "master" does lots of interesting things with the drivers: it caches them (for example to reuse the same TCP connection to the device the next time), it manages concurrent access to the drivers (allowed on some, disallowed on others), etc... This means the master needs to have control over the lifetime of the drivers.
If we deploy the drivers as WAR files, we loose this functionality. We would effectively have to make the drivers stateless, or we would have to move a lot of the functionality from the "master" into the individual drivers.
idea 3: OSGi to the rescueOur next idea was to use OSGi. If we make every driver implementation an OSGi bundle, we could decide at runtime which service of which bundle to invoke, and OSGi would do all the class loading separation for us. I did some experiments which proved that this would be a feasible solution (although I didn't look into the possibilities for lifetime management of the drivers in detail), but apparently OSGi is a bit of an all or nothing approach. There are of course many more components in our architecture than what is shown in the above UML diagram, and at the time it was not feasible to introduce OSGi in all of them. It seems that we would have to deal with the same class loading problems as in the first idea on the boundaries between non-OSGi and OSGi components.
A colleague of mine found a library (transloader) which was created to solve this problem. The transloader library is not tight to OSGi though, it is a general solution to bridge the gap between different class loaders within Java. This led us to our fourth idea.
idea 4: class loading magic revisitedIf this transloader library can bridge the gap between different class loaders, why not use it to implement our first idea? We now have the same architecture as in the first idea, but without custom class loaders and without weird packaging constraints on the driver implementation libraries. All we have to do is package the drivers together with all their dependencies (no exceptions) and use the transloader library for the communication between the "master" and the drivers.
Check out the transloader tutorial to learn more, it was really easy to set up and use.