The Myriad Idioms For Finding Implementations In Java
Tue Oct 18 10:25:06 EDT 2022
- Java Services (Not the RESTful Kind)
- Java ClassLoaders
- Managed Beans to CDI
- The Myriad Idioms For Finding Implementations In Java
A few years ago, I wrote a post about Java service location, which covered things like META-INF/services
and OSGI extensions. Today, I'd like to discuss a similar concept: code in a top-level API that finds a specific implementation. For reasons that will become clear shortly, I'll call this the "FactoryFinder pattern".
Background
Not all Java code uses this kind of thing and, while service loading is related, the overlap isn't complete. Where this does come up a lot is in a framework like Jakarta EE, which is very intentionally split between vendor-neutral specification classes/interfaces (the ones starting with jakarta.*
) and specific implementations.
For example, the Jakarta REST (n?e JAX-RS) specification only defines various classes and interfaces within the jakarta.ws.rs
package space, but doesn't include any actual implementation. That's left to various vendors. The number of implementations varies by spec, and JAX-RS is particularly prolific on this front. In the XPages Jakarta EE project, we use RESTEasy, whose classes are all in the org.jboss.resteasy
package space.
There's a (usually) hard wall between these layers: the spec declares an API that programmers can use, and then the implementation has to allow itself to be called by those class names and obey the specification's rules. When writing JAX-RS resources in an NSF, the fact that it's using RESTEasy does not enter into your experience. That raises the question, though, of how this works. How does the vendor-neutral specification locate the implementation classes to hand off the work? Well, that question has a number of different answers.
Entrypoint Classes and Locating Implementations
In general, each spec accomplishes this using one or more entrypoint classes. For example, JAX-RS uses RuntimeDelegate
and its static getInstance()
method to locate server implementations and ClientBuilder
and its newBuilder()
method to load client implementations. Outwardly, these methods just promise that they'll find and provide an implementation, but the actual way that specs do this varies.
One of the most common ways to coordinate this loading is to have a class named FactoryFinder
. This idiom and specific name proved very popular over at Sun as they built up the JEE specs:
Despite their identical names, each of these classes is a different implementation, and they have different characteristics. There are routines in common, and each spec uses a subset of these. I'll go over the common ones here, in no particular order other than that I'll start with the ones found in the JAX-RS API first.
ServiceLoader
This one is used in basically every spec up until the latest era. This uses the java.util.ServiceLoader
class to find implementations by way of text files in META-INF/services
named after the spec class and containing implementation class names. For example, RESTEasy contains a file named META-INF/services/jakarta.ws.rs.ext.RuntimeDelegate
that references the class org.jboss.resteasy.core.providerfactory.ResteasyProviderFactoryImpl
. That looks like this:
1 2 3 4 5 | Iterator<T> iterator = ServiceLoader.load(service, FactoryFinder.getContextClassLoader()).iterator(); if(iterator.hasNext()) { return iterator.next(); } |
FactoryFinder.getContextClassLoader()
there is a utility method that just uses an AccessController
block to work with Java policy limitations like we see on Domino all the time.
This is simple enough in the normal case, but can get a little tricky when you add in something like OSGi. By default, ServiceLoader
will look in the thread-context class loader, which will usually be where your application code lives. Inside an app container, like an NSF, the implementation class may not actually be visible, though. Accordingly, many of these finders fall back to looking using the class loader of the spec class, which has a higher chance of seeing the implementation. That looks similar:
1 2 3 4 5 | Iterator<T> iterator = ServiceLoader.load(service, FactoryFinder.class.getClassLoader()).iterator(); if(iterator.hasNext()) { return iterator.next(); } |
In the XPages Jakarta EE project, neither of these calls will tend to work by default, since neither the app nor the API bundle won't see the implementation bundle by default. In some cases, I deal with this via the methods below, but in others I will do so by re-packaging the implementation as an OSGi fragment bundle. Fragment bundles attach themselves onto their host's classloader fully, and this allows ServiceLoader
to find the implementation.
Configuration Properties
A handful of these specs, JAX-RS included, will also look for the name of an implementation class using an external properties file. The placement of this in the priority order - as a fallback after ServiceLoader - and the classes used in the implementation make me figure that these are quite often relics of earlier habits.
JAX-RS, for its part, will look within the java.home
system property, which points to the JVM's installation directory. In there, it looks for a properties file named lib/jaxrs.properties
:
1 2 3 4 5 6 7 8 9 10 | String javah = System.getProperty("java.home"); configFile = javah + File.separator + "lib" + File.separator + "jaxrs.properties"; File f = new File(configFile); if (f.exists()) { Properties props = new Properties(); inputStream = new FileInputStream(f); props.load(inputStream); String factoryClassName = props.getProperty(factoryId); return newInstance(factoryClassName, classLoader); } |
This tries to use the thread-context class loader only, so it wouldn't work for a complex app server situation. Likely, it's meant for either an older type of application or a standalone special-purpose JAR.
System Properties
Similar to reading a designated properties file, these specs will often then fall back to looking for a Java system property of a given name. These properties may be dynamically set at runtime or may be set during the JVM launch. Often, this property will be the name of the interface/abstract class being looked up, like so:
1 2 3 4 | String systemProp = System.getProperty(factoryId); if (systemProp != null) { return newInstance(systemProp, classLoader); } |
This one can actually come in handy sometimes - though not ideal, I've used similar cases where I set the name of an implementation or delegation class in a property before initializing the spec. It's best to avoid that when possible, but I'm often glad it's there.
OSGi Escape
Next up is one that JAX-RS doesn't use, but shows up periodically. Though Jakarta EE isn't based around OSGi, a good number of the implementations historically have used (and still use) it, and OSGi always sits in a "not standard, but too popular to consistently ignore" limbo.
To account for this, there's a similarly semi-standard library called the OSGi resource locator. This library provides a class named org.glassfish.hk2.osgiresourcelocator.ServiceLoader
that does its own search and loading for ServiceLoader
-compatible META-INF/services
files within OSGi bundles in the current platform. The idea is that, if you have an OSGi-based platform that you want to work with this type of loading, you will provide the Resource Locator class and let any loaders written to use it fall back to it.
Because this class is not normally present even when actually in OSGi, APIs that make use of it have to be careful and indirect about trying to load it at all. We'll use JAX-B as our example here. They'll generally try to load the bridge class reflectively, which avoids having OSGi-wrapping tools like bnd create a potentially-undesired dependency on the presence of the bridge. Then, they'll reflectively ask it to load service implementations. That tends to look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Use reflection to avoid having any dependency on ServiceLoader class Class serviceClass = Class.forName(factoryId); Class target = Class.forName(OSGI_SERVICE_LOADER_CLASS_NAME); Method m = target.getMethod(OSGI_SERVICE_LOADER_METHOD_NAME, Class.class); Iterator iter = ((Iterable) m.invoke(null, serviceClass)).iterator(); if (iter.hasNext()) { Object next = iter.next(); logger.fine("Found implementation using OSGi facility; returning object [" + next.getClass().getName() + "]."); return next; } else { return null; } |
That's also generally wrapped in a big try/catch block to avoid gumming up the works if any pieces are missing.
The XPages Jakarta EE project actually contains a reimplementation of this that avoids some hurdle or other that I found with the stock version. I avoided doing something like that for a while, but it ended up being the most practical way to get some of these specs working.
Default Implementation
Back outside the realm of OSGi, a handful of these specifications will also include a hard-coded default provider class name. These are generally the classes from what used to be dubbed reference implementations and which are largely components of GlassFish by virtue of that being Sun's version.
For example, the JSON-P API has a final fallback of trying to look for org.glassfish.json.JsonProviderImpl
by name:
1 2 | Class<?> clazz = Class.forName(DEFAULT_PROVIDER); return (JsonProvider) clazz.getConstructor().newInstance(); |
Though these implementations generally also declare themselves via ServiceLoader files, this is presumably useful in historical or edge cases where there's still a decent chance that the RI will be available. This does have an unfortunate effect on error messages, though, where the case of "I can't find any implementation at all" ends up being reported as e.g. "Provider org.glassfish.json.JsonProviderImpl not found". That's not really a problem with the approach as such, though, but rather just the way it shakes out in practice.
Manually-Set Implementation or Locator
The final mechanism I'm going to discuss is sort of a final escape hatch. Sometimes, the provider class will have a method that lets you set an arbitrary implementation yourself, without having the API do any of these lookups at all. Some, like MicroProfile Config and CDI even go one step further and provide a method that configures not just a specific implementation but rather an implementation locator. These APIs are my friends and I love them.
This mechanism works well for my needs in the XPages Jakarta EE project, where either it's easier to just set one implementation for the whole server or, like with CDI, there's complex logic that requires inspecting the active Servlet request to see what NSF I'm in.
APIs of this style will usually have a method named like setInstance
or setProvider
on either their core entrypoint class or on the provider locator. For example, MicroProfile Config provides the former on its ConfigProviderResolver
class:
1 2 3 | public static void setInstance(ConfigProviderResolver resolver) { instance = resolver; } |
instance
here is a static property. Once it's set - either by this method or by a dynamic lookup - the main instance()
method will use it:
1 2 3 4 5 6 7 8 9 10 | if (instance == null) { synchronized (ConfigProviderResolver.class) { if (instance != null) { return instance; } instance = loadSpi(ConfigProviderResolver.class.getClassLoader()); } } return instance; |
The XPages JEE project makes use of this method at HTTP start, setting a provider resolver that includes some stock config sources as well as some classes that know how to read properties from the Notes environment and from the xsp.properties file.
Though this mechanism seems like the crudest out of the bunch, I'm extremely happy whenever it's there.
Conclusion
That was a lot! And there's not really a lesson to be learned here, but rather more that it's often useful to know about all these different mechanisms. When working in the XPages JEE project, I've had to use almost all of them at one time or another, and I've had to familiarize myself with which APIs use which and adapt them individually. For some, I've altered the implementation to be a fragment bundle; for others, I've created my own fragment to provide services and implementations; and so forth. It's a bit of a shame that there's no grand unified system for this, but at least it can be interesting to see the messy path that these specs have taken as Java technologies and the ecosystem evolved.