The Intricate Work of OSGi Dependencies on Domino
Dec 2, 2020, 3:03 PM
- Converting Tycho Projects to maven-bundle-plugin, Initial Phase
- Winter Project #2: Maven P2 Repository Resolver
- OpenNTF Fork of p2-maven-plugin
- The Intricate Work of OSGi Dependencies on Domino
One of the main goals of OSGi is proper runtime dependency management, not only allowing a bundle to declare what its dependencies are to ensure that they're there, but even to select one of multiple available versions. For example, you might have one bundle that expects Guava 15 but not higher and another that expects 18 or above, and both can be loaded successfully if you have multiple Guava versions installed. The goal is that you're supposed to be able to just throw a whole bunch of bundles into a pot and the system will figure it out.
Before I get into why this system is hobbled on Domino, I'll add a little more background.
For our purposes here, bundles have two main ways to declare what they are (there are more than this, but they're not relevant right now):
- The bundle's symbolic name combined with its bundle version. For example, the core bundle for ODA is named "org.openntf.domino" and has a version like "18.104.22.168006091416".
- The bundle's exported packages, which are Java class packages and might also individually have versions. Using ODA again as an example, it exports a slew of packages, such as
org.openntf.domino.nsfdata, though it doesn't specify versions for any of these.
The versions of the bundle and the exported packages don't need to be the same, nor do all the exported packages need to have the same version. This shows up a lot in "spec bundles", where a vendor will wrap a standard spec API in their own bundle for various reasons. For example, Apache Geronimo has a bundle called "org.apache.geronimo.specs.geronimo-jaxrs_2.1_spec", which provides the JAX-RS 2.1 spec. The bundle itself has a version of "1.1.0", while the exported packages are all version "2.1".
To require a bundle by name, you use the
Require-Bundle header, like
Require-Bundle: org.openntf.domino;bundle-version="11.0.0", which requires specifically the ODA bundle, version 11 or above. To require a package, you use
Import-Package: javax.ws.rs;version="2.1.0". The two methods have some implications when it comes to how the ClassLoaders work, which I touched on a bit earlier this year.
The package-based mechanism is generally preferred nowadays over the bundle-based one, largely for the kind of flexibility and division-of-responsibilities it provides. For example, if I want version 2.1.0 of
javax.ws.rs, my code shouldn't care at all whether it comes from Geronimo's bundle, JBoss's, or anywhere else - it's all the same thing (in theory). This is extremely common for projects created with
bnd and tools based on it, which can generated imported and exported packages automatically based on your code and some small configuration. For example, the bundles that make up Open Liberty use
bnd config files that have some loose configuration, which then is processed out to full listings of packages with version information. This is also often paired with OSGi's "Capability" system, but that's one of the "not important for now" things.
Domino - and I believe this is inherited from Eclipse, which does the same thing - is largely based on bundle requirements. For example, the
org.eclipse.jdt.ui bundle (part of the Java development tools in Eclipse and Designer) requires a bevy of bundles by name version and doesn't tag any of its exported packages with versions. This is similarly reinforced in the tools. Eclipse, unsurprisingly, uses Tycho to bring the "Eclipse PDE" style to the table, where the MANIFEST.MF file is more hand-crafted, and the tools don't do as much for you automatically. You can go full
Import-Package and attach versions to your exported packages with Eclipse, but the tooling doesn't encourage it.
Conflicts in Practice
That brings us back to how this all contributes to making working with OSGi a little extra annoying on Domino. This is something that comes up constantly in my XPages Jakarta EE Support project: I want to bring in an implementation component for a JEE spec, but it will either already have or will have generated for it OSGi rules that lean towards the "package-style" of doing things, versions and all. Because there are common packages (like, say,
javax.activation) that are supplied by bundles present in the Domino runtime, I want to use those. However, since the packages from Domino don't have versions specified, I need to re-wrap the bundles to import the packages without versions. Thus begins this ongoing nightmare.
Another approach I could take would be to bring my own, nicely-OSGi-ified versions of the afflicted packages, and options abound. However, that leads to a sneaky other trouble: because some of the Domino bundles are multi-spec monsters - with
com.ibm.designer.lib.javamail being the absolute worst culprit - some other bundle on the system might casually import
javax.mail. If I have this other spec implementation floating around, then it could get matched to my bundle for the former and IBM's for the latter... and crash right into "exposed to a package via two dependency chains" problem. That wouldn't necessarily be an issue if everything used versions on packages, but leaving them off means it's kind of up to the container to match bundles to each other, and it's entirely happy creating impossible conflicts.
Ways Around It
When working through this sort of trouble, I've found a few ways around the things I've run into. The first is what I mentioned above: using
p2-maven-plugin to re-wrap bundles with instructions that make them more Domino-friendly. This involves a few tricks:
- Excluding specific dependencies from the final distribution, when they would otherwise bring in specs that are already present by default or in my implementation
- Adding explicit
Require-Bundlelines in order to "hard bind" the bundles where I need them
- Using "visibility:=reexport" in the above, which means that bundles that depend on the current one will also depend on the re-exported dependency by name.
bundle-symbolic-namerules to package imports, which is like a weird corruption of package imports that says "only accept this package from a bundle named X"
Aside from reworking existing bundles, I have a few times ended up creating fragment bundles that glom onto one bundle to tie it to another one after the fact, usually for the components that bridge different JEE specs.
The Wildcard: The System Bundle
In general, with OSGi, you can expect the packages you need to be provided in bundles, but it also allows for the core JDK to be implicitly available - that is, things like
java.lang don't need to be imported, and are always present. That's fine, since it's generally well-enough-defined what makes up the JDK and what you'd need to instead bring in.
However, the Domino core classpath doesn't contain just the JDK, but also anything in
jvm/lib/ext and (as a curveball)
ndext from the Domino directory. Above, I specifically pointed out the
javax.activation package, and that's because it suffers the most from both the
javamail bundle as well as this. If you run
tell http osgi packages javax.activation on Domino's console, you should see that bundles get this from two places: some use
com.ibm.designer.lib.javamail, while others use the system bundle,
org.eclipse.osgi. That "system bundle" concept is not only the thing in charge, but it also passively provides access to classes found in the lower-level classpath. In a fully OSGi environment, that system classpath will be pretty clean, but Domino's isn't that.
The good news here is that it isn't usually a big problem. If you import packages by a non-zero version, they'll never match from the system bundle this way. Still, it's always there, lurking, and the problems increase if you start adding more JAR files to
jvm/lib/ext to avoid policy or
amgr-memory issues. We've seen this periodically with ODA, where we used to support the use case of putting the core files there and using just a shim at the XSP layer, before it became too much of a hassle to manage. I ran into it again more recently when Guava showed up there: because I hadn't specified a version, it was able to match from the system bundle, and ended up with classes available at compile time but missing at runtime.
The upshot here is that there's no simple advice for dealing with this. Cultural and implementation factors make bringing third-party code into Domino unusually difficult, but it can generally be dealt with via various patching mechanisms. The XPages JEE project's dependency module ended up turning into a trove of such workarounds, so perhaps it can be useful if you ever run into this sort of thing yourself.
Karsten Lehmann - Dec 8, 2020, 6:26 AM
My latest approach for the javax.mail / javax.activation issue is to load them with my own classloader in an OSGi plugin and block the parent classloader call for com.sun.mail.* , javax.mail.* , javax.activation.* and com.sun.activation.* (to prevent the bootstrap classloader from returning them). So far it seems to work. Will probably be in the Domino JNA plugin for XPages development.