I’ve been working on a little Java web framework [1] for an exploratory work project. I am building the framework and a sample app as a set of OSGi bundles to drastically reduce API surface area between components [2]. This also makes it easy to run my sample app directly within base Eclipse, using Eclipse’s built-in support for OSGi framework launches and a bundle-ized version of Jetty.
This configuration raises an interesting problem though, how do you inject the application code into the framework, since the framework obviously can’t statically depend on the application code, and OSGi will only let Java classes “see each other” [3] if one bundle makes a package (or packages) visible via an Export-Package
manifest declaration (e.g. Export-Package: my.api.package
) and another bundle declares an explicit dependency on that package via an Import-Package declaration (e.g. Import-Package: my.api.package
)? In other words, how will you avoid hitting java.lang.NoClassDefFoundError
s when trying to load the application code via reflection?
I sure didn’t know. Luckily I have a good buddy here at IBM in Research Triangle Park named Simon Archer who is an OSGi and Equinox expert [4], so I ran the problem by him. He told me about an OSGi manifest declaration I had never heard of called DynamicImport-Package
. My assumption that you can only get at code via explicit Import-Package
declarations was actually wrong.
Simon explained that the way DynamicImport-Package
works is that it basically allows a bundle to say “I want to be able to access any class that is part of an exported package in the runtime environment. So let’s say I have two bundles: bill.framework
and bill.sampleapp
. I want the code in bill.sampleapp
to run inside the web framework implemented in bill.framework
, but I obviously don’t want the bill.framework
code to have a static (class-level) dependency on the bill.sampleapp
code since the whole reason I’ve designed it as a framework is to allow build-time composition of arbitrary applications built on the framework [5]. So I put the following in bill.framework
‘s MANIFEST.MF
file:
DynamicImport-Package: *
Then in my Sample App bundle’s MANIFEST.MF
file, I put my application class in a package [6] that I export to the OSGi environment:
Export-Package: bill.sampleapp.app
Now the framework is able to dynamically load the sample app via reflection:
// MyFramework.java
String appClassName = System.getProperty("bill.app.classname");
IApplication app = (IApplication)Class.forName(appClassName).newInstance();
Voilà !
Footnotes:
[1] I know, because what the world needs now is another Java web framework. But as I observed in a journal entry, every framework is evil, except mine.
[2] Note that the framework itself doesn’t use or depend on OSGi. I build the bundles into a set of simple JARs that can run as part of a JEE web app or as standalone Java application again using an embedded Jetty web server.
[3] For a great primer on building modular Java applications with OSGi, see the recent book “OSGi and Equinox: Creating Highly Modular Java Systems” by McAffer, VanderLei, and Archer.
[4] E.g. Simon co-wrote the book mentioned in [3]. He is “Archer” 🙂
[5] Yes, I know. Most people call this pattern “dependency injection”. For the full treatise, see Fowler.
[6] The fact that you have to export the package for the code that you want to dynamically load wasn’t immediate obvious and Simon and I spent approximately twenty minutes staring at the screen wondering why we were getting java.lang.NoClassDefFoundError
even though we were using DynamicImport-Package: *
. After some unfruitful Googling, we decided to check out some bundle details using the OSGi console in Eclipse. As we were looking at the details for the sample app, I got the at the time unintuitive idea to try exporting the Sample App package. Sure enough this fixed it. Simon and I had a bit of a debate about whether or not it made sense to have to export application code since this effectively declares the application code to be API, which seems wrong – i.e. typically an application sits on top of a stack of code and depends on lots of stuff, but nothing depends on it.
But eventually we came to a reason that makes perfect sense for exporting the application code: If you didn’t have to explicitly export the code, theoretically any OSGi bundle’s code could get access to any other bundle’s code simply by declaring DynamicImport-Package: *
and loading random classes via reflection, defeating the whole purpose of the OSGi modularity system. So by requiring that the to-be-dynamically-loaded class be available to the environment via an explicit Export-Package
declaration you are still playing by the “normal rules” and just using reflection rather than static instantiation to poof up objects.
Of course this means that you should minimize your API surface area for the application class, so I put mine in its own package and its only public methods are those from the framework interface that it implements.
Good fences FTW!
I’m surprised that Simon didn’t recommend you use Services. There’s simply no need to muck about with reflection like this.
To extend the “good fences” analogy… why reach across your neighbour’s fence to take something (even with his implied permission), when you could just ask him for it?
At the very least load the classes with Bundle.loadClass(). Or TBH *anything* other than DynamicImport-Package, which is going to get you into all sorts of trouble.
Neil: Thanks very much for your comment.
Unless I’m misunderstanding something, neither OSGi Services nor Bundle.loadClass() would have worked for me because as I mentioned in footnote 2, my framework is not based on OSGi. It’s really just a simple POJO framework.
I am only using OSGi as development-time API tooling and for the nice little Equinox/Jetty launch configuration for my server-side code.
If I were creating an application that used OSGi at runtime, I suspect that Simon might have nudged me in the direction you suggest.
Hi Bill! I guess what I really don’t understand is why you are writing yet another dependency injection framework. Still, given that you are and that you want to remain compatible with non-OSGi scenarios, the key is to use ClassLoading the right way.
Don’t use Class.forName() without a ClassLoader argument, i.e. the single-arg version. When you do this, the JVM has to work out which loader to use to load the class, and you run into problems that force you to use DynamicImport-Package.
If you supply a ClassLoader — either by calling the three-arg version of Class.forName() or calling ClassLoader.loadClass() directly — then you can be compatible with both OSGi and non-OSGi scenarios. When in OSGi you can quite easily construct a ClassLoader instance that wraps the Bundle.loadClass() method.
To boil it down to a single principle: don’t try to load a class by name only; always specify which ClassLoader it should come from. Then you will at least have the flexibility to move to OSGi while steering clear of hacks like DynamicImport-Package.