To describe that, I'll lightly cover what MP Config is. It's a CDI extension that allows you to annotate properties on a bean to indicate that they're intended to come from an available configuration source - often a .properties file in the project, but it's a pluggable system. Your bean will look like this:
The idea is that you'll then have a properties file or environment variable to fill in the value, allowing you to separate your configuration from the implementation in a consistent way. Here, I'm making use of the fact that a default provider looks up Java system properties, so I could just get it working before investigating adding providers.
Since I'd already added CDI and a CDI-based extension in the form of MVC, I figured this would be easy.
The problem I hit, though, was bizarre. CDI would identify the bean above, but would hit this problem:
The gist of this is that it noticed that the
javaVersion property is supposed to be an injected property, but it had no idea what the source should be. It did know about the MicroProfile provider, which handles
@ConfigProperty, but it couldn't put two and two together.
I banged my head against this for a while, and eventually determined that the class as loaded from the NSF is stripped of the
@ConfigProperty annotation outright. Other annotations, such as
@Inject and even custom annotations, would remain, but not
@ConfigProperty. I wrestled with OSGi dependency chains for a while, to no avail.
Eventually, I found the core, and it was an old nemesis of mine. It's this method in
This field is called by the ClassLoader used in an NSF to ensure that certain classes, by name prefix, cannot be loaded by code coming from an NSF. The last three lines there make a sort of sense: Domino is supposed to be an app container for XPages apps, and ideally it's not a simple process for an app to break out of its container to muck about in the parent environment. Fair enough. The NAPI line is presumably there because IBM wanted to protect developers from themselves, even though Notes devs had been making unauthenticated calls to C APIs for freaking ever.
It's the first two, and specifically the first, that are the source of my trouble. Those prohibitions are presumably meant to isolate XPages apps from the fact that they live in an OSGi world, with the assumption that anything beginning with
org.eclipse. refers to something like
org.eclipse.core.runtime, the OSGi system bundle.
And this is the issue. MicroProfile is not in any way related to OSGi, but it sure is an Eclipse project. Accordingly, the class name of
org.eclipse.microprofile.config.inject.ConfigProperty, and thus cannot be loaded from an NSF.
So I considered my options.
One was to fork MP Config and rename the packages. That would work, but it would defeat the portability goals of the XPages JEE project, and would also just be a hassle - I've already had to fork a few specs, and each new one adds to the maintenance burden. That remained an option, but it would be a last resort.
My next idea was to wrap around the
ModuleClassLoader class used by
NSFComponentModule for class loading purposes. This class is blessedly non-
final, and so in theory I could look at the instance in a module and swap it out with a replacement. I tinkered with this a bit, but the trouble became the way it's layered, with a
DynamicClassLoader private class within - something harder to subclass. In theory, I could reproduce the behavior of it wholesale, but that would be both fragile (if the implementation ever changes) but also verging on if not outright illegal (it's one thing to be API-compatible, and another to reproduce the internal functionality). After some wrangling, I decided to look elsewhere.
The True Workaround
I realized eventually that I don't really care about
ModuleClassLoader as such: it does its job fine, and it's only the response that it gets from
ClassLoaderUtil that is the problem. If I could change that, I would be set.
I've used the Javassist project here and there for a long time, ever since its inclusion in ODA for one reason or another. It's a handy toolkit, and notably includes the capability to alter a method implementation on the fly. There's my loophole.
The reason this kind of thing can work is related to how Java handles classes and calls between them. For all intents and purposes, you can consider a method call from one bit of Java to another to be a string-based lookup, saying "find a class named X and a method named Y, and then execute it". The "find" part there is much looser than you might think. It's easy to think of class references like C static linking, but they're really not. When code asks for a class, it asks the context
ClassLoader, and that object can do basically whatever the heck it wants to find it, as long as it eventually emits a
Class that the runtime can deal with.
Javassist's manipulation makes use of the fact that classes are generally eventually just a bunch of bytes, and you can do whatever you want with a bunch of bytes. Using Javassist, it's fairly simple to, once you have a handle on the class, alter the method. Truncated, that's:
And this works, as far as it goes: I now have a
Class version of
ClassLoaderUtil that skips the onerous check.
The trouble now was to get this to be actually used by other classes. Generally, once a
ClassLoader loads a class, it's difficult to feed it another version unless it's designed to do so: most
ClassLoader implementations, including those used here, are designed to read and emit classes by their own rules, not have new data fed into them.
I tried digging through the Eclipse OSGi
ModuleClassLoader (distinct from the NSF
ModuleClassLoader) for entrypoints and had some initially-promising work with Eclipse's internal
ClassLoaderHook type, but eventually determined that this would require more patching than I'd want, if it was possible at all.
I also considered using Java's instrumentation capabilities to intercept class loading, but that would require setting up a special Java agent in the launch parameters, which would be too onerous.
But then I remembered something I had heard about when looking into getting
ServiceLoader to work with OSGi: a concept in the OSGi spec called "weaving".
I had noted that this concept existed, but set it aside in large part due to how esoteric it sounds: the term "weaving" makes it sound like it's a way to interact with the threads of fate or something, which is evocative but not something that seems immediately useful.
What it really is, though, is an OSGi-friendly version of the above: when the OSGi runtime goes to load a class from a bundle, it reads the data but then gives any such listeners an opportunity to manipulate the code before it's actually reified into a class. This is how the
ServiceLoader mediator does its thing: it looks for
ServiceLoader calls during loading and re-"weaves" them to run through OSGi instead.
This was perfect: it provides exactly the hook I want and it does it in a clean, spec-based way, without having to do weird reflection to reassign object properties or anything.
So I went about writing such an implementation. All the pieces are there on Domino, and the mechanism for registering a
WeavingHook is something I'd done before in Open Liberty: it's a type of OSGi service that you can register and manage in an Activator class. It's also the sort of thing that would work well with Declarative Services, but Domino doesn't have a DS handler installed and I figured I didn't need to solve that quite yet.
So I wrote a
This builds on the above Javassist usage to now load the class from the byte array provided by OSGi, transform it, and then write the new version back. Since this happens while OSGi is reading the class to begin with, there's never a time when there's an older, less-permissive version of the class running around, as long as I get my service in early enough.
This service is registered in the Activator without too much fuss:
The final bit to get right was the "get the service in early enough" aspect. The main task was making sure that this bundle was activated before any XPages apps loaded, and that was a job for my old friend
IServiceFactory, which is the extension point that's intended to add handlers for incoming URLs but has the desirable attribute of being initialized right at the very start of HTTP loading.
With this in place, I now have a fix automatically applied to that fiendish class on load, and MicroProfile Config (and future MP specs) works like a charm.
This was an arduous one, and I think the FTL victory jingle actually physically played when I got it working. I've hated this restriction for a long time, and I'm glad to finally have a workaround.
It was also enlightening to properly learn about OSGi's weaving capability. As mentioned above, this is what the
ServiceLoader bridge does, and I'd tinkered with that at one point, but never got it working. I suspect now that it should be entirely doable to make it work, most likely also involving bringing in an implementation of the Declarative Services OSGi capability. That should be a fun project in its own right.
Moreover, the fact that I now have a system in place to do this weaving on the fly means that I may be able to un-fork some of the specs I had to fork to get working previously, which specifically required altering
ServiceLoader calls. Even if I don't get the official service bridge in, perhaps I can use this technique to just alter the parts I need to on the fly, and otherwise use stock implementations from Maven.
But, for now, the way is cleared for further progress, and a bizarre mystery is solved. I call that a good day.