Java Services (Not the RESTful Kind)
Thu Jun 04 16:42:28 EDT 2020
- Java Services (Not the RESTful Kind)
- Java ClassLoaders
- Managed Beans to CDI
- The Myriad Idioms For Finding Implementations In Java
The concept of "services" in Java is fairly critical, but, especially with the XPages stack we've grown used to, the term covers quite a few different technologies.
Definition
Before I continue on, I want to make clear what I mean by "service" in this context. It's unrelated to REST services or even remote access of any kind; instead, it's about how an app can find implementations of some kind of class or interface within its runtime.
A very-common type of this sort of thing is a data adapter or converter. Say you have your own object FizzBuzz
that you use within your app, one that represents data storable in multiple ways. One way to handle converting from various types to FizzBuzz
would be a giant if
tree, like:
1 2 3 4 5 6 7 8 9 10 11 | public FizzBuzz convert(Object input) { if(input instanceof String) { // ... } else if(input instanceof JsonObject) { // ... } else if(input instanceof org.w3c.dom.Document) { // ... } else { throw new IllegalArgumentException("Cannot convert to FizzBuzz: " + input); } } |
That'd work well enough, especially for a small app. You can imagine, though, how this might get out of hand in an even moderately-complicated case, with the if
tree turning into a tangled mess. Moreover, this doesn't allow for any extensibility without directly modifying the convert
method - any new type will have to go into this, making management of a large team more cumbersome and completely cutting off the possibility of third-party additions.
So, to keep things scalable, it'd make sense to create an interface that would specify a generic way to convert some type of object to a FizzBuzz
:
1 2 3 4 5 6 | package com.sprockets.data; public interface FizzBuzzConverter { boolean canConvert(Object o); FizzBuzz convert(Object o); } |
Then the code that actually needs to convert would look more like this:
1 2 3 4 5 6 7 8 | public FizzBuzz convert(Object input) { Stream<FizzBuzzConverter> converters = moreOnHowToFetchLater(); return converters .filter(converter -> converter.canConvert(input)) .findFirst() .map(converter -> converter.convert(input)) .orElseThrow(() -> new IllegalArgumentException("Cannot convert to FizzBuzz: " + input)); } |
In a small case like this, that's not necessarily going to be a big deal, but it doesn't take too long for it to become desirable to break it apart. Take the case of JAX-RS providers, which do exactly this kind of entity conversion when processing HTTP requests. Everything over HTTP comes in as plain text (more or less), but programmers want to be able to accept an int
input parameter, or to automatically convert their custom business-logic object to JSON. Without a separation like this, the code to handle all known types would be impossible to manage all in one place, and there'd be no way to handle custom types that didn't exist when the code was written.
Types
There are quite a few distinct types of services that I've run across, and I'll list them here in roughly the likelihood that a programmer coming from an XPages background will encounter them.
ServiceLoader Services
This is the most-common kind of service you're likely to encounter in a Java application, and you can generally identify it by its use of the META-INF/services
directory inside a JAR. java.util.ServiceLoader
itself was added to Java in 1.6 but was designed to codify habits that become common beforehand.
The way this works is designed to be simple: you create a plain-text file within META-INF/services
named after the service class you're implementing, and then put the names of your implementing classes within it, one on each line. So, in our above example, you'd create a file named META-INF/services/com.sprockets.data.FizzBuzzConverter
and fill it with something like:
com.sprockets.data.impl.StringFizzBuzzConverter
com.sprockets.data.impl.JsonObjectFizzBuzzConverter
com.sprockets.data.impl.DomDocumentFizzBuzzConverter
Code that calls ServiceLoader.load(FizzBuzzConverter.class)
will find all of those files within the current ClassLoader space (more fun with that down the line) and instantiate the named classes, returning an Iterator
to loop through them.
IBM Commons Services
Within the XPages stack, the bulk of service interactions are managed by the IBM Commons ExtensionManager
class, which is a generic way to ask for a service type by String name.
In the normal case, this acts as a slightly-old-timey variant of the now-standard ServiceLoader
mechanism, likely by dint of preceding the standard's introduction. Like ServiceLoader
, it looks for files with the name you pass it in the META-INF/services
directory in your app and adds instances of all the names it finds within.
What makes it important (and what gives it longevity in non-XPages OSGi apps on Domino) is that it also bridges into the Equinox OSGi service infrastructure when available and looks for services registered there by the com.ibm.commons.Extension
name. The reason this is important is that, in an OSGi context, one bundle can't by default see the files in another bundle in its ClassLoader, which means that services registered via META-INF/services
in one won't be picked up by a ServiceLoader
call in another.
Since XPages's life spanned a pre-OSGi era and the 8.5.2 "Extensibility API" era, it bears the signifiers of both, smoothly papered over by IBM Commons:
The Equinox loader looks in both places, in fact, which is why you can declare XPages services within an application using META-INF/services as well as within an OSGi bundle's plugin.xml file.
Equinox plugin.xml Extensions
I mentioned above that IBM Commons bridges the difference between ServiceLoader
and Equinox, but now I'd better go into a little more detail about the latter.
"Equinox" refers to the particular OSGi implementation that underlies both Eclipse-the-IDE (and thus Notes) and Domino's web stack. While Equinox is the fully-fledged reference implementation of OSGi, plugin.xml is specific to it and I believe pre-dates Eclipse's migration to OSGi (which we still see reflected in 9.0.1FP10+'s plugin trouble).
plugin.xml used to house a lot of information that was moved over to META-INF/MANIFEST.MF
, but its primary remaining function is to declare services for the Equinox environment. Eclipse itself uses this extensively, and it remains the primary way to extend the IDE's capabilities.
One important thing to note here is that plugin.xml's extensions aren't limited to just providing a service class implementation. While many do that, it's also used heavily to provide configuration information without executable classes at all.
Multi-type "FactoryFinder" style
This type of service locator is similar to the IBM Commons ExtensionManager
, but is usually confined to an individual domain, like a specific Jakarta EE spec. The way this idiom works is that there's a central coordinating class, usually named FactoryFinder
, whose job it is to locate implementations of services from one or more sources, and often using a known fallback implementation.
I encountered one of these when diving deep into the XPages stack. javax.faces.FactoryFinder
is responsible for finding implementations of very-low-level entities, like the services that spit out JSF applications at the start of initialization, or those that create FacesContext
objects.
These will often have specialized behavior. For example, the standard SOAP API looks through a system property, then an external "jaxm.properties" file, then ServiceLoader
, then an older META-INF/services
name, then OSGi, and finally falls back to a default class name.
Java 9 Modules
I have to admit that I haven't actually used this, but it's too important to skip. Java 9 and above include a module system that is sort of like an OSGi bundle in that it lets you declare what your module exports and other characteristics about its interactions with the outside world.
Along with this support came a new way to declare services. Since this is also baked in to Java itself, it gets the advantage of also working with ServiceLoader
. In this case, instead of writing a text file in META-INF/services
, you declare the type of service you're providing and the class implementing it in the module definition. This not only unifies the service with other module information, but it also makes it more type-safe and programmatically clear. It's neat-looking.
OSGi
I already mentioned that Equinox can use the "plugin.xml" file to do cross-bundle services in OSGi, but I also mentioned that it's specific to that one implementation and not actually part of the OSGi spec.
Instead, OSGi has a couple (for some reason) standard mechanisms for providing and consuming services. I encountered these mechanisms in practice when I created a UserRegistry
implementation for Open Liberty.
In my first version, I declared my services programmatically in the bundle's activator (which is a class that you can write to run when your bundle is loaded/unloaded). In that way, you can dynamically tell the runtime that your bundle provides any number of services.
In my second revision, I changed to using what's dubbed Declarative Services. These do basically the same thing, but are defined for the runtime in a combination of the META-INF/MANIFEST.MF
file and some service-definition files in the bundle - essentially, like a re-thought version of plugin.xml.
Summary
Okay! So, what's the upshot? Well, in my work, I use the first two all the time: inside a non-Domino app, META-INF/services
is king; when working with Domino, IBM Commons ExtensionManager
handles everything I need.
As far as implementing your own services, it's definitely a critical concept to keep in your pocket (I can only assume it's somewhere in Design Patterns). You could certainly go crazy with it and make a real mess of incomprehensible indirection, but it's probably useful more often than you'd think at first. Give it a shot next time you find yourself writing a big if
tree with complicated branches.