JavaEnterprise JavaEclipse Tip: Use Optional Plug-in Dependencies to Support Diverse Runtime Environments

Eclipse Tip: Use Optional Plug-in Dependencies to Support Diverse Runtime Environments

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

The Eclipse runtime is a dynamic environment composed of numerous plug-ins, or bundles, running in an OSGi-compliant framework (Equinox). The OSGi Alliance aims to provide a sort of “universal middleware” for Java—a service-oriented, component-based environment that provides software vendors with standardized access to a variety of platforms and runtime environments. These properties allow Eclipse to deploy into a number of hardware platforms and operating systems.

In order to reach the broadest audience possible, Eclipse/RCP application and plug-in developers are advised to write software with as few dependencies and runtime environment requirements as possible. For instance, it is usually a good idea to avoid the latest and greatest features available in the most recent Java release, unless the problem at hand demands it. That way, customers who have not upgraded to this release yet (for whatever reason) still have access to your products.

However, if a later Java release indeed allows you to implement a superior solution, it would be a pitty not to take advantage of it—you would be effectively shortchanging customers who did upgrade. This tip explores an approach that allows you to provide your customers with the best possible solution available while still supporting those using older Java versions.

Bundle Constraints and Dependencies

In its manifest, a plug-in (or bundle, in OSGi terms) may specify several conditions, or constraints, that must be met for it to function properly. In a previous tip, you learned how to specify the supported Execution Enviornments for your plug-in. This setting instructs the framework to only resolve the plug-in if the current execution environment is compatible with at least one of those listed in its manifest. For instance, a bundle requiring J2SE-1.5 will work fine if the application is running in Java 5 or 6, but not in 1.4.2.

A bundle also can specify a set of bundles that must be available (in other words, resolved) for it to work. For instance, a bundle that requires access to the Eclipse Workbench (org.eclipse.ui) will not work unless that bundle can be resolved (which means it was able to resolve its own constraints, recursively). Doing this effectively allows you to access Java packages exported by the required bundle.

In certain situations, your code may work whether or not a specific bundle is available—it may be able to adjust dynamically to this condition (for example, use the exported services if they are there, or follow an alternative path if not). You can specify such an optional dependency by marking the required bundle as “optional.” You can do that by using the Plug-in Manifest Editor as shown in Figure 1. Listing 1 shows the contents of the manifest of an example bundle (main.plugin) specifying several required bundles, one of which (latest.service.impl.plugin) is marked as optional.

Figure 1: Plug-in Manifest Editor indicating an optional dependency on plug-in latest.service.impl.plugin.

Listing 1: Bundle Manifest for plug-in main.plugin.

Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Main Plug-in
Bundle-SymbolicName: main.plugin;singleton:=true
Bundle-Version: 1.0.0
Bundle-Activator: main.Activator
Require-Bundle: org.eclipse.ui,
   org.eclipse.core.runtime,
   service.plugin,
   latest.service.impl.plugin;resolution:=optional,
   legacy.service.impl.plugin
Eclipse-LazyStart: true
Bundle-RequiredExecutionEnvironment: J2SE-1.4

Providing Alternative Implementations

Suppose you have a service that you want to implement using the latest and greatest version of Java, but you also need to support a legacy JRE (for example, 1.4) using a less efficient approach. You can do develop each implementation in its own plug-in with the appropriate Execution Environment specified in its manifest. In a very simple example, you have the following plug-ins:

  • service.plugin: Plug-in with the service interface (service.MyService, no implementation)
  • main.plugin: The “client” plug-in that uses the service
  • latest.service.impl.plugin: Service implementation using some Java 5 features (for instance, requiring J2SE-1.5)
  • legacy.service.impl.plugin: Legacy service implementation using Java 1.4.2

All plug-ins (other than latest.service.impl.plugin) specify J2SE-1.4 as the required execution environment. Figure 2 shows the resulting dependency graph, with an optional dependency between main.plugin and latest.service.impl.plugin. Note that in order to avoid a circular dependency, the service interface has been broken out into a separate plugin (service.plugin).

Figure 2: Diagram showing a dependency graph for example plug-ins.

The resulting effect is that, at runtime, all plug-ins will resolve if running in Java 1.4.2 or later, but the latest.service.impl.plugin plug-in will resolve only if running in Java 5 or later. Because main.plugin has only an optional dependency on this plug-in, it won’t fail to resolve and will work as long as the code is able to deal with the missing dependency.

Locating the Appropriate Implementation

To put it all together, the client code that wants to use a service with alternative implementations must be able to locate the appropriate implementation. In Listing 2, the client plug-in first attempts to instantiate the latest implementation of MyService. Failing that, it falls back to the legacy implementation, which is always available. Note that the legacy.service.impl.plugin plug-in will be activated only if the implementation is in fact needed (for example, the class loader will only activate the plug-in if it actually needs to load one of its classes).

Listing 2: Method for obtaining the appropriate service implementation.

public MyService getMyService() {
   if (svc == null) {
      try {
         svc = new LatestServiceImpl();
         } catch (NoClassDefFoundError e) {
            svc = new LegacyServiceImpl();
      }
   }

   return svc;
}

When implementing this approach, care must be taken to only “touch” classes and interfaces that are optionally dependent (and thus may not be available at runtime) in places where this is expected. For instance, you might use a try/catch block designed to deal with a possible NoClassDefFoundError.

Test-Running Both Cases

The full source for this example is available for download (see the Resources section at the end of this article). The main plug-in’s activator implements the org.eclipse.ui.startup extension to run as soon as the Workbench starts up. When invoked, all it does is locate the appropriate service implementation and invoke its single method (hello()), which prints a message that includes the value of the java.version system property.

To appreciate the intended effect, you need Java 1.4.2 as well as 5.0 or later installed on your machine and registered in your Preferences (Java -> Installed JREs). First, launch the workspace as an Eclipse Application using Java 1.4.2. When the runtime Workbench comes up, you should see the following in your IDE’s Console view:

Main Plugin started.
Legacy Service Implementation Plugin started.
Hello from Legacy Service Implementation!
java.version=1.4.2_11
Main Plugin stopped.
Legacy Service Implementation Plugin stopped.

Exit the runtime Workbench and edit your Eclipse Application launch configuration to specify Java 5.0 (or later) as the JRE. Launch the workspace and observe the following console output:

Main Plugin started.
Latest Service Implementation Plugin started.
Hello from Latest Service Implementation!
java.version=1.5.0_09
Main Plugin stopped.
Latest Service Implementation Plugin stopped.

Note: In both cases, the service implementation most appropriate for the given runtime environment is used. Also, in the latter case, the legacy plug-in is never activated and thus consumes only minimal (if any) memory resources.

Resources

About the Author

Peter Nehrer is a software consultant specializing in Eclipse-based enterprise solutions and J2EE applications. He is the founder of Ecliptical Software Inc. and a contributor to several Eclipse-related Open Source projects. He holds an M.S. in Computer Science from the University of Massachusetts at Amherst, MA. Peter can be reached at pnehrer AT eclipticalsoftware DOT com.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories