JDK's Service Provider Mechanism & OSGi IntroductionA number of Java SE and EE APIs like JAXP, JAXB, JPA rely on Java Platform Service Provider mechanism to support pluggability of alternative implementations at runtime. Typically such APIs provide an abstract factory class having a static method which users can invoke to obtain a concrete factory instance. The static method uses a spec defined ordered lookup procedure to determine which concrete instance to return. The procedure involves look up of META-INF/services file as well. If no class loader is supplied by user, Thread's context class loader is used to search service configuration files. Let's illustrate this by an example. StAX has a factory class called XMLInputFactory which has a static factory method called newFactory() defined like this: public abstract class XMLInputFactory { ... /** * Create a new instance of the factory. * This static method creates a new factory instance. * This method uses the following ordered lookup procedure to determine * the XMLInputFactory implementation class to load: * Use the javax.xml.stream.XMLInputFactory system property. * Use the properties file "lib/stax.properties" in the JRE directory. * This configuration file is in standard java.util.Properties format and contains * the fully qualified name of the implementation class with the key being the system property defined above. * Use the Services API (as detailed in the JAR specification), if available, to determine the classname. * The Services API will look for a classname in the file META-INF/services/javax.xml.stream.XMLInputFactory * in jars available to the runtime. * Platform default XMLInputFactory instance. * Once an application has obtained a reference to a XMLInputFactory * it can use the factory to configure and obtain stream instances. * * @throws FactoryConfigurationError if an instance of this factory cannot be loaded */ public static XMLInputFactory newFactory() throws FactoryConfigurationError { ... } } As you can see from the above javadocs, at runtime, newFactory() looks for META-INF/services/javax.xml.stream.XMLInputFactory files in jars available in runtime to see if there is an alternate implementation available. Let's see a simple client using the API: import javax.xml.stream.XMLInputFactory; class Client { public static void main(String[] args) { XMLInputFactory xif = XMLInputFactory.newFactory(); System.out.println(xif); } } When users runs this just in a Java SE 6 environment, XMLInputFactory that's part of JRE is returned. But, when user adds a StAX provider (say) Woodstox to classpath and runs as shown below, XMLInputFactory from their provider (Woodstox) is returned. java -classpath woodstox.jar Client Issues in OSGi EnvironmentAs we have seen so far, this simple technique works quite well in Java SE and EE environment. The key to its success is Thread's context class loader which is used to demarcate an application boundary. It assumes two things about Thread's context class loader:
Unfortunately, neither of them holds good in an OSGi environment. Let's turn our example into an OSGi bundle to illustrate this point: Bundle 1: staxosgitest.jar
Bundle-Activator: staxosgitest.Activator Import-Package: javax.xml.stream, org.osgi.framework Manifest-Version: 1 Bundle-ManifestVersion: 2 package staxosgitest; import javax.xml.stream.XMLInputFactory; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; public class Activator implements BundleActivator { public void start(BundleContext ctx) { XMLInputFactory xif = XMLInputFactory.newFactory(); System.out.println(xif); } public void stop(BundleContext ctx) {} } This is the Woodstox StAX implementation packaged as an OSGi bundle. It also has a META-INF/services/javax.xml.stream.XMLInputFactory. If we install these two bundles and activate staxosgitest bundle, then you can see that it would not be able to see woodstox implementation. The staxosgitest bundle only sees StAX implementation from JRE. Why does META-INF/service lookup didn't see woodstox services file? That's because Thread's context class loader is really not defined when activator of staxosgitest bundle is executed. Even if it did, it could not have loaded the provider class from woodstox bundle. To get an idea of real life bugs, refer to https://github.com/javaee/glassfish/issues/16970 Proposal(Package and bundle names are subject to change) We propose to provide a simple API which can be used to look up service providers in OSGi environment instead of relying on Thread's context class loader. The API would look something like this: package org.glassfish.hk2.osgiresourcelocator; /** * This is a gateway to OSGi bundles as far as META-INF/services files are concerned. * Since META-INF/services files are not exportable, clients relying on Java SPI mechanism * can't discover all providers. This utility helps in such a situation. It provides a utility method * {@link #lookupProviderInstances} which can find META-INF/services being part of OSGi bundles. * * @see {@link #lookupProviderInstances} * @see {@link #lookupProviderClasses} * * @author Sanjeeb.Sahoo@Sun.COM */ public abstract class ServiceLoader { /** * Singleton */ private static volatile ServiceLoader delegate; /*package*/ ServiceLoader() {} // TODO: Create a special Permission for this method public static synchronized void initialize(ServiceLoader delegate) { if (delegate == null) throw new NullPointerException("Did you intend to call reset()?"); if (this.delegate != null) throw new IllegalStateException("Already initialzed with [" + delegate + "]"); this.delegate = delegate; } // TODO: Create a special Permission for this method public static synchronized void reset() { if (delegate == null) { throw new IllegalStateException("Not yet initialized"); } delegate = null; } /** * @param serviceType Type of service requested * @param <T> * @return providers matching this service type. Returns null if not initialized. */ public static <T> Iterable<? extends T> lookupProviderInstances(Class<T> serviceType) { if (delegate == null) { return null; } else { return delegate.lookupProviderinstances1(serviceType); } } /** * @param serviceType Type of service requested * @param <T> * @return provider classes matching this service type. Returns null if not initialized. */ public static <T> Iterable<Class<? extends T>> lookupProviderClasses(Class<T> serviceType) { if (delegate == null) { return null; } else { return delegate.lookupProviderClasses1(serviceType); } } // Implementation provided by an OSGi aware subclass. /*package*/ abstract <T> Iterable<? extends T> lookupProviderInstances1(Class<T> serviceType); /*package*/ abstract <T> Iterable<Class<? extends T>> lookupProviderClasses1(Class<T> serviceType); } This class is packaged in an independent OSGi bundle called osgi-resource-locator.jar with its own activator. The activator is responsible for initializing this class with proper context. The activator is given special permission to call the secured methods. public class Activator implements BundleActivator { public void start(BundleContext context) throws Exception { ServiceLoaderImpl serviceLoader = new ServiceLoaderImpl(); AccessController.doPrivileged(new PrivilegedAction() { public Object run() { ServiceLoader.initialize(serviceLoader); return null; } }); } public void stop(BundleContext context) throws Exception { AccessController.doPrivileged(new PrivilegedAction() { public Object run() { ServiceLoader.reset(); return null; } }); } } The bundle manifest for osgi-util.jar looks like this: osgi-resource-locator.jar
Bundle-Activator org.glassfish.hk2.osgiresourcelocator.Activator Bundle-ManifestVersion 2 Bundle-Name OSGi resource locator bundle Bundle-SymbolicName org.glassfish.hk2.osgi-resource-locator Bundle-Vendor GlassFish Community Export-Package org.glassfish.hk2.osgiresourcelocator;uses:="org.osgi.framework";version="1.0.1" Import-Package org.osgi.framework;version="1.5" Bundle-ActivationPolicy lazy Point worth noting here is that this bundle uses LAZY activation policy. Existing code that rely on Thread's context class loader have two options while using this API: a) Use reflection to avoid compile time dependency: This is described below. package javax.xml.stream; class FactoryFinder { private static Object findJarServiceProvider(String factoryId) { if (isOsgi()) { return lookupUsingOSGiServiceLoader(factoryId); } else { // existing code } } private static boolean isOsgi() { try { Class.forName("org.glassfish.hk2.osgiresourcelocator.ServiceLoader"); return true; } catch(ClassNotFoundException cnfe) { } return false; } private static Object lookupUsingOSGiServiceLoader(String factoryId) { try { // Use reflection to avoid having any dependendcy on ServiceLoader class Class serviceClass = Class.forName(factoryId); Class[] args = new Class[]{serviceClass}; Class target = Class.forName("org.glassfish.hk2.osgiresourcelocator.ServiceLoader"); java.lang.reflect.Method m = target.getMethod("lookupProviderInstances"); java.util.Iterator iter = ((Iterable)m.invoke(null, args)).iterator(); return iter.hasNext()? iter.next() : null; } catch(Exception e) { // log and continue return null; } } } woodstox-osgi.jar will be changed to have an additional Import-Package like this: DynamicImport-Package: org.glassfish.hk2.osgiresourcelocator Since woodstox-osgi.jar uses DynamicImport, as soon as osgi-util.jar is activated, FactoryFinder will be able to use it. b) Package the API class and optionally import it as well In this method, woodstox-osgi.jar contains a private copy of org.glassfish.hk2.osgiresourcelocator.ServiceLoader.class so that it can use the API directly in the absence of osgi-util.jar bundle. It also optionally imports org.glassfish.hk2.osgiresourcelocator package. When osgi-util.jar is installed in the system before woodstox-osgi.jar, then woodstox.jar will be wired to osgi-util.jar for org.glassfish.hk2.osgiresourcelocator package even though it has a private copy. If the osgi-util.jar has been started before StAX API is used, then the custom service finder logic would kick in as well. The downside of this approach is that every client bundle now has a copy of ServiceLoader.class. Although the class is particularly simple, it is not an elegant solution, but them some may find it better than reflection. Next StepA stable version of osgi-resource-locator.jar is made available in maven repo along with sources . Download and give a try. |