Service Loader Requests

What Would a ServiceLoader That Could Take Requests Look Like?

In this ongoing series related to Java configuration, I’ve covered JNDI and Jakarta RESTful Web Services. Both involve loading Java Objects from potentially many providers with disambiguation algorithms built in, and both permit the application assembler to work around naming clashes. Of course, neither is a Java configuration system.

Let’s look at some of the super-concepts these two technologies have in common.

Objectspace

The first is what I’ll call, somewhat pretentiously, objectspace. This is the notional space of all possible Java objects (and primitive types) in all possible Java virtual machines in all possible worlds. Objectspace is big.

You can pick out an individual object from objectspace, let’s say, by supplying some loading system with an address. (This sounds like pointers! Indeed, that’s part of the analogy I’m going for.) Give the system an address, it hands you back the corresponding object. By definition, if you provide a precise enough address there will be exactly one object it picks out, never zero. Simple. What could possibly go wrong?

To get an object from objectspace according to the rules as I’ve laid them out so far, you have to be remarkably precise. You’d better be able to distinguish objects by type, certainly, and whatever the addressing scheme looks like, it better take type into account. But you can’t stop there.

Ambiguity

If we do stop there, we have to address ambiguity. If I ask for what I hope is the only String in all of objectspace by asking a hypothetical loading system for the one true object that has the type String.class, the loading system will laugh in my face. There are many Strings in objectspace.

OK, OK; I might instead want to ask this hypothetical loading system for a very particular String, namely the one in objectspace under the System property key java.home. But even this isn’t specific enough, since, remember, objectspace encompasses all Java objects in the universe. There are many Java homes. The one that identifies my Java installation is just one of them! And yet I get a single String back, so, Laird, your analogy sucks.

Addressing

Not so fast! When I ask for this hypothetical loading system to give me the java.home String, I’m actually supplying plenty of other implicit addressing information in my request, whether I know I’m doing this or not. Specifically, I’m actually asking for the only String in all of objectspace that has the type String.class, that is indexed under the System property key java.home, and on this JVM, running on this machine, on this architecture, in this universe, and so on and so forth. That may be enough to pick out the String I want (and normally is in non-hypothetical, non-abstract, non-architecture astronaut cases).

So types and JVM-wide names seem to be the bare minimum to pick out an object from all of objectspace. Sounds easy—but that’s not quite right either.

Let’s say instead that I would like this loading system to fetch me the one true String that can be found in some hazy way “under” the name preferredHost (I made this up so that it’s not some key defined by Java itself). Let’s further wave our hands about possible name collisions: let’s pretend that no other developer in the entire universe could possibly ever be writing a class like mine, and no other developer in the entire universe could possibly ever mean anything semantically different from what I mean when I say preferredHost. (In practice, of course, these assumptions are completely ridiculous. Bear with me; we’ll get there.)

But if my application is running in test, in some sort of hazy way, then there may very well be an ambiguity here. preferredHost and String.class no longer uniquely identify a String in objectspace. Maybe there is another string value indexed under preferredHost in objectspace (that identifies the production preferredHost for example). Oh, shoot, I guess really what I was asking for all along was the one true String indexed under the explicit name preferredHost and the implicit name test. (Note that in no way was I asking for the explicit name test.preferredHost, nor did I have some kind of fallback in mind.) As a component developer, I of course didn’t know what application my component was running in, so it didn’t occur to me to check for this case. Oops.

Objectspace is big. Like, you just won’t believe how vastly, hugely, mind-bogglingly big it is. Picking out a single Object in it is damn near impossible.

Instead, typically what we do, while blissfully not being aware of it, is: we supply some addressing information, and then rely on some common case where we were expecting one thing, and there was only one thing that happened to match our imprecise addressing information, and we asked our loading system for it, and it responded, and we got only one thing, not 27, and it never occurs to us that 27 of those things is actually a very real possibility. Then the bugs roll in.

Good loading system APIs recognize that no addressing system you can come up with will ever pick out an object from objectspace without some kind of further disambiguation.

ServiceLoader Addressing

Let’s look at java.util.ServiceLoader as an example. This class deliberately embraces ambiguity. The only kind of addressing information you can give it is type. Ask a ServiceLoader for the thing corresponding to SomeService.class, and it will hand you back an Iterable<SomeService>. Anyone can put a service provider file on the classpath (discoverable as a classpath resource at META-INF/services/com.foo.SomeService, let’s say), and its entries will be picked up. So great, how do you filter all the various SomeService implementors? That, dear reader, is up to you.

The upshot is: ServiceLoader, like many loading systems, accounts for the almost completely necessary ambiguity of objectspace in its API design. Ask for a thing of a type, get back many things of that type. Your problem to solve.

Jakarta RESTful Web Services Addressing

Let’s consider Jakarta RESTful Web Services. Let’s pretend that Jakarta RESTful Web Services is, among other things, a way to (again, partially) address into objectspace. After all, with a resource method you can say, effectively, “this method will return a SomeService if the addressing information matches the path a/b/c and the media type application/json.”. So if you ask this loading system for a SomeService.class object, providing no path information and no media type information, the ambiguity is reduced somewhat: this resource method I described will not “fire” and it, at least, will not be responsible for returning a SomeService. Some other resource method might. If more than one “fires”, then there is an algorithm that somewhat arbitrarily picks one.

JNDI Addressing

Let’s consider JNDI. Let’s pretend that JNDI is, among other things, a way to (again, partially) address into objectspace. After all, with a javax.naming.spi.ObjectFactory you can say, effectively, “this ObjectFactory will return a SomeService if the addressing information matches the compound name a/b/c and an expected environment.” So if you ask this loading system for a SomeService.class object, providing no compound name information and no environment information, the ambiguity is reduced somewhat: this ObjectFactory I described will not “fire” and it, at least, will not be responsible for returning a SomeService. Some other ObjectFactory might. If more than one “fires”, then there is an algorithm that somewhat arbitrarily picks one.

(You of course see the similarity.)

Each of these loading systems differs, of course, in what its “loaders” are permitted to do (resource methods and ObjectFactory instances have to play by the rules of their specifications). And each loading system permits more or less addressing information to “ride along” with the general request for a typed Java Object from objectspace, to help pare down the possible matching objects to a manageable number. But they’re very similar when we look at them as object loading systems that accept reasonably fine-grained addressing information that identifies many things, but hopefully not lots of things.

Finally, each of these systems is not a Java configuration system! But you know what is a configuration system, of a sort? java.util.ServiceLoader. It is a configuration system that just so happens to have punted the problem of resolving ambiguity to the end user. It is also designed to satisfy only one of several configuration-related use cases, which it does very well. What if we augmented ServiceLoader with the ability to receive some kind of addressing information that would allow it to avoid ambiguity in more cases than it does right now? What would we need to add?

Loaders, Paths and Qualifiers

First, let’s talk terminology. A configuration system doesn’t just load services, so we’ll drop that word. So ServiceLoader will become Loader for this discussion, and “service provider” will become “provider”, and so on.

Next, we need a good word for our addressing information. We can look to JNDI and Jakarta RESTful web services here. In JNDI, the addressing information is termed a name. In Jakarta RESTful Web Services, the addressing information is termed a path (since of course it is concerned with URIs). I like path better than name for addressing information because it implies a type (a filesystem path yields a file, for example; a Jakarta RESTful Web Services path yields an object of a particular media type) and can either imply a tree structure or not, depending on how it’s used. It’s also about finding your way to a destination. So we’ll go with path.

The kind of path we’ll talk about is similar to Jakarta RESTful Web Services’ path: it is sparse. That is, it’s not the case that every element in a path necessarily identifies an object at that location (in direct contrast with JNDI, where every atomic name in a compound name identifies a context). In this regard, a path is like a pointer or a probe: ultimately what matters is what it picks out at the end, not the intermediate objects that may exist along the way.

Our path will also need to have the ability to pass additional addressing information. JNDI has environment properties. Jakarta RESTful Web Services provides resource methods with the ability to inspect headers, query string parameters, path parameters and matrix parameters. Each of these facilities is a way to further qualify a request. We’ll consequently follow in the terminological footsteps of systems like CDI and call this additional addressing information qualifiers.

Putting it all together, a path is a typed and qualified ordered sequence of elements that identifies zero or more objects in objectspace.

A loader is a component that dereferences a path.

If you look at things this way, then a java.util.ServiceLoader is a loader that dereferences very particular kinds of paths, namely those that have a type, zero elements and no qualifiers.

What’s Next?

We’re on our way, but we’re not there yet. What lies at the end of any path is not necessarily an object, but may be nothing or many objects. Also, since in the real world the objectspace we’re dealing with at any given time in any given system is actually fairly small and sparsely populated, we might want a strategy for what to do when a path doesn’t actually identify anything but something else might be suitable for it.

Additionally, even if a path does identify something, it may come and go over time, being temporarily or permanently absent or present. What our loader loads may therefore need to be not the object itself, but some kind of dereferencer that is pinned to the path that yielded it.

There is also the notion of an application’s environment to be concerned with. An application’s environment is the portion of objectspace it inhabits (the machine it lives on, the locale it’s in, various other automatic and implicit coordinates, and the human-authored configuration that is suitable for it), which includes parts that a component of that application, asking for an object to be loaded, is unaware of, but that are necessary to include with the addressing information. Other environments within objectspace may or may not be suitable for a loader to consider when trying to dereference a path.

The journey continues; stay tuned.