Java Configuration: JAX-RS as a Configuration System

Background and Rationale

I’ve written previously about looking at JNDI through a Java-centric configuration system design lens. Here I’ll do something similar with the JAX-RS specification (now known as {deep breath} Jakarta RESTful Web Services, or, hopefully soon, simply Jakarta REST, which is how I’ll refer to it in this article).

Jakarta REST as a Configuration System?!

Hear me out.

First, I’m not actually proposing that if you want a configuration framework in your Java program you should grab a Jakarta REST implementation and go to town.

But I am looking at it as primarily a Java framework, and not as something that is web-oriented. After all, one of its founding goals was to make it easy to use plain old Java objects (POJOs) to model representational state transfers. Who really cares if there is a network involved or not?

The Foundation

Jakarta REST is built atop resource classes. While you should of course consult the specification for the official definition of resource classes (and anything else I’m going to wave my hands about in this article), the general gist is: a resource class is a POJO class with some specifically shaped methods in it, and annotated in a particular way.

For retrieval purposes (GET), which is all I’m interested in, methods that do retrievals obviously need to have a return type. The return type can be Jakarta-REST-specific (Response), or can be a POJO designed by the resource class designer.

The annotations on resource classes and their methods help them declare what paths they respond to, and what types of objects those methods supply in response.

When you put this all together, you have a subsystem that receives a typed path, finds a relevant supplier (a resource method), caches that fact, and then uses that supplier to serve up an Object of some kind that corresponds to that path (and other qualifiers).

Configuration Concerns

When you look at Jakarta REST this way, it starts to look an awful lot like some of the JNDI concepts I’ve written about previously.

In both cases there is a lookup operation, with name-like structures identifying the thing to retrieve. In both cases (although in JNDI it’s a pain in the neck) you can qualify your lookup. In both cases an application assembler can declare explicitly how bundles of components should be combined into an application in such a way that naming conflicts do not occur. In both cases, how a resulting Object is put together, or found, or synthesized, is completely transparent to the caller and is deliberately unspecified.

Jakarta REST also features a wealth of additional qualifiers that ride along with every request. You have the path, of course, but you also have headers (key/value pairs), MIME types, and request-level content negotiation strategies. If you look at these coarsely enough, they’re just qualifiers further picking out the Object that is being requested.

One of the nice things about the lookup request format that Jakarta REST uses is that in a path/with/many/components there is no presumption that each component in the path designates a retrievable resource. (JNDI, by contrast, basically requires that a Context exist at each juncture.) This allows for sparse graphs of resources, dynamic subresources, and all sorts of other interesting bits that end up being directly relevant to configuration systems.

Another nice thing about the lookup request format is that the incoming name-like structure (the request) consists not just of the name-like thing (the path) plus its qualifiers (the headers and matrix parameters and everything else) but also the type of the object being requested (expressed as a MIME type). In my earlier JNDI article, I noted that configuration systems involve a typed path at the heart of configuration lookup. JNDI sort of lets you get there, but it is awful and clunky to do. Jakarta REST makes it reasonably easy: by the time a resource method gets invoked, you know that MIME type matching has already occurred according to a well-specified algorithm, so you know that the resource method in question is equipped to service the request.

Disambiguation

Thankfully, the designers of Jakarta REST (well, JAX-RS, in this case) also realized the namespace issues that always show up when you talk about someone assembling components together into an application, and provided for their solution.

In JNDI, namespace issues are somewhat moot, because names are always relative to a Context: there’s no such thing as an absolute name. The same is not true in Jakarta REST, but the application assembler can explicitly designate an Application implementation that says for certain which Java classes are to be considered resource classes, and which are not. This allows two resource methods, for example, from two different sources, annotated with the same @Path annotation, to coexist: the application assembler can choose just one, can wrap the other, or any of a variety of other strategies at assembly time to resolve the ambiguity.

(It’s worth noting that no Java-centric configuration system that I’m aware of lets you do this fundamental disambiguation operation at assembly time. That’s really odd.)

Suitability

Probably the most interesting feature of Jakarta REST when looked at through a Java-centric configuration lens is its built-in notion of suitability.

A resource method is more or less suitable for a given request as specified by an exceedingly well-defined algorithm. For my purposes, the exact steps of the algorithm are unimportant. The fact that it exists and is defined in terms of application-level concerns, rather than component-level concerns is what is important. If this algorithm completes and there are somehow still two or more candidate resource methods for a given request, then an error is thrown. This means that resource method selection is deterministic: if you supply the same inputs, you get the same outputs every time.

Recognizing the difference between application-level concerns and component-level concerns is critical for this kind of determinism, because components are often developed in isolation from one another, so sharing things like namespaces and numberspaces and pathspaces and all the rest can be difficult. So, for example, defining the matching algorithm in terms of a global set of MIME types means that there can be no name clashes between types: application/octet-stream means what it means, regardless of which component uses it.

Contrast this with another popular but extraordinarily misguided strategy of labeling some component somewhere with a numeric priority and believing erroneously that you have somehow solved the ambiguity problem. Instead, you’ve just punted it: If component A and component B are developed in isolation, and both have independently decided to declare that they are of priority 10, that is still a problem the application assembler has to solve, but unless there is yet another mechanism for her to disambiguiate this ambiguity, it can lead to a non-deterministic state of affairs. We see this, as I’ve noted earlier, in MicroProfile Config. (Interestingly, Jakarta REST did walk into this trap in terms of providers and other accessory entities, but at least they give the application assembler a welcome “out” since she can always write an Application class to make things more explicit.)

Conclusion

Jakarta REST is a specification for web services, yes, but it is also a specification for acquiring Java Objects given path-like requests, where the potential suppliers of such Objects can be more or less suitable for any given request. This lines up pretty well with the requirements of a Java-centric configuration system. There are lessons to be learned here that can be applied to the design of a “real” Java-centric configuration system.