Protecting Java in a Modular World – Isolating the Bad Guy

This post is about isolating intrusive frameworks by classloaders so that they stop getting in the way of what you are actually supposed to do: Build a maintainable, consistent, well-defined solution to a business problem.

This post is a logical continuation of the previous post Protecting Java in a Modular World – Context Classloaders.

So you have been building your applications carefully watching out to choose a consistent set of tools and 3rd party libs that allow impressive features with little implementation effort and now you are going to integrate Eclipse BIRT (the reporting tool) and boom!

  • There is a seemingly inifinite number of dependencies brought in that may or may not match the versions you already have (and that may or may not match the versions the other framework you integrated requires) – so your code stops working when satisfying BIRTs demands.
  • Even, if you would get that sorted out – you will need to sort it out once more after the next upgrade.
  • All the problems in Protecting Java in a Modular World – Context Classloaders apply once more.

You are right in the middle of Java’s DLL Hell.

And Eclipse BIRT is just one example. Apache Hadoop is another and there is countless others out there.

Naively adding all the required libs into one class loading scope typically creates a huge mess that a mere human can hardly get under control at one point in time – much less so over time.

How to fix that?

Isolation!

Make sure that BIRT lives in its own, dead end class loading scope. In other words, make sure that BIRT operates as if it was the end of the food chain – or more specifically that its classloader is not delegated to by other modules of the solution – or only for very dedicated purposes.

In plain Java the way to achieve this is to create a child classloader of main application class loading scope. The isolated code can be accessed via reflection or by “accessor” style implementation classes that implement some parent loader provided interface and that are added to the BIRT scope.

Here is some almost not-so-pseudo code that shows the initialization:

public class BIRTService {
  private BIRTService impl;

  private void init(File birtJarsFolder) {
    try {
      // collect jars
      List<URL> urls = new LinkedList<URL>();
      for (File j : birtJarsFolder.listFiles(
        new FileFilter() {
          public boolean accept(File pathname) {
            return pathname.getName().endsWith("*.jar");}
          }
        )
      ) {
        urls.add(j.toURI().toURL());
      }

      // construct class loader
      URLClassLoader cl = new URLClassLoader(
        urls.toArray(new URL[urls.size()]),
        this.getClass().getClassLoader()
      );

      // load impl
      this.impl = (BIRTService) Class.forName(
        "sample.BIRTServiceImpl",
        false,
        cl)
      .newInstance();

    } catch (Exception e) {
      throw new IllegalStateException("BIRT Service init failed",e);
    }
  }

  /*
   * delegate
   */
  public void methodA() {
    this.impl.methodA();
  }
}

Of course, in addition make sure that context classloaders  are set (as described in in the other blog) to the BIRT scope – on every invocation in this case – in the implementation of that service. Otherwise BIRT will still fish in the applications other scopes – which is asking for trouble.

So, in terms of class loaders we would get the following scheme:

Now apart from the complexity of packaging stuff the right way, what if you use stuff that BIRT cannot work with – or may not work with in the future? Following the (otherwise robust) class loading delegation pattern, types that you provide with your application will be provided to BIRT with preference over its own types. In other words, if you use a commonly used library in a version you prefer but BIRT cannot live with, you will still need to follow BIRT’s regime to make things work.

In other words: To do it right, you need more, better, stronger modularization!

Specifically, you need more class loading separating modularization

Think about it: If the modules of your application would hide and manage their visibilities as we just did to isolate BIRT, you could actually keep clean scopes.

In Z2 you would set up modules to achieve the following class loading scheme (slightly abstracted):

Instead of loading an implementation class from a class loader you instantiated, you would look up a component and leave the module mechanics to Z2.

(In OSGi you could use the BIRT bundles – and spread out the mess – or hide BIRT into one bundle achieve a similar scheme as above).

Now, when done, what do we get:

  • BIRT is confined to a leaf on the class loading hierarchy – minimizing the impact BIRT has on other modules.
  • BIRT only sees what is needed – minimizing the impact other modules have on BIRT.

Other frameworks pose similar or harder challenges and what we did not even get into is how to provide extensions to something like BIRT but that’s another topic.

 

Advertisements