Build- and Runtime Dependencies

Managing dependencies is a topic that keeps coming up in the Java community. In most projects, the need arises as soon as one starts using external, third party libraries and components and nowadays, with the rise of open source, there are many such dependencies. Depending on the development tooling that is used, there are several different ways of dealing with dependencies.

If you use Ant, you can create your own mechanism using property files that list all libraries and versions. Depending on your preferences, you can either keep those libraries around in your favorite revision control system, or refer to some external location. If you don’t want to manage such dependencies yourself, Ivy is a good extension to Ant that is very flexible and interoperates with most existing repositories out there.

If you use Maven, you use its built-in dependency management solution. Libraries will be downloaded and used automatically from a set of configurable repositories.

Both are examples of systems that allow you to manage build-time dependencies which, in the end, are used to provide a class path to the Java compiler so you can build your code.

The second type of dependencies you need to manage are runtime dependencies. In traditional Java applications, your end up having a classpath that is similar to your build-time classpath, and there’s not much more to it than that. When you use OSGi, your runtime dependencies are defined differently. You no longer have a global, linear classpath, but instead use import and export package statements in the manifest of your bundle to declare what that bundle needs. Alternatively, you can also use require bundle manifest headers, but that is definitely not the preferred way. There are two fundamental problems with managing dependencies at the JAR level:

  • First of all is that you define them by hardcoding links to implementations. By hardcoding an implementation, it is no longer substitutable by another one, even if it implements the same interface. Take an application that needs a web server to run a Servlet. Two well known implementations in Java are Apache Tomcat and Jetty. If you hardcode your servlet to depend on Tomcat, you can no longer switch to Jetty and vice versa, even though both allow you to use servlets.
  • Furthermore, dependencies on JAR files are rather coarse grained. JARs or bundles are the unit of deployment, and might include various public and private packages. A JAR might include a big library, and even though you only use a single class, you get the whole thing. Even worse, other parts that you might not even use can depend on other JARs and before you know it, you’ve downloaded the internet and created tight coupling with many JARs that you weren’t even using.

A comment I often make is that Eclipse is the best known and worst example of using OSGi. Whilst that’s at least a bit exaggerated, there are certainly things they could have done better. In its defense, a lot of these problems were already in the codebase from the days before they were using OSGi, and staying backward compatible with a huge eco-system of third-party plugins often requires them to make compromises. However, this also means that if you do not have such a legacy, you should not use Eclipse as an example.

Eclipse uses the require bundle a lot and therefore has both problems mentioned above. A lot of its plugins have dependencies on specific versions of implementations, which causes all kinds of problems determining all dependencies necessary to deploy that plugin in your version of Eclipse. That problem has become so big that Eclipse has resorted to “big bang” distribution releases. This is wrong, they’re fighting the symptoms. The real cause of these problems is that they’re doing dependency management all wrong. This negates some of the benefits of a modular architecture. The right way to implement dependencies is doing them at the package level using imports and exports, and to properly apply semantic versioning.

Although it sounds like I’m bashing Eclipse, I am using it as an example here because it’s so well known. There are other examples out there that exhibit the same problems and the point I’m trying to make is that if you do dependency management well in OSGi, you end up with a very flexible, loosely coupled system that is easy to partially update. By combining package imports and exports with proper semantic versioning, you are ready to build large scale applications that are easy to evolve and maintain.

Posted in Article