Module Compatible Classloading Guide

Introduction

 

Modular classloading has a much greater focus on isolation and separation than traditional heirarchical models, so it is far less forgiving about using the "wrong" classloader. Unfortunately, this means that any dynamic classloading code you have written in the past may need to be updated to play nicely in a modular environment. However, the good news is that code that is module compatible is more correct, and therefore less error prone in traditional environments. This article provides some background infromation along with some pointers on how to make this transition.

 

Common Terms

This article assumes that the following terms are well understood.


Defining Classloader - The classloader which loaded the calling class. This is what is used when you use Class.forName() without specifying a classloader. It's also what loads classes that you reference statically (e.g. via an import). It can be obtained by calling getClass().getClassLoader() on the respective class.

 

Thread Context Classloader (TCCL) - The classloader which has been assigned to the calling thread. It's meaning is specific to the environment it was assigned in. In Java EE code, the TCCL is always the classloader of the relevant deployment. The TCCL is the most commonly misused classloader.

 

Dynamic Classloading - Loading a class by name using a ClassLoader. This is typically used for pluggable architectures that discover some kind of extension. An example is an API framework that supports multiple implementations/providers (e.g. JAXP). It dynamically loads the provider by name using some kind of service discovery mechanism (a file in META-INF/services, a system property, etc). Another example is application server code which needs to operate against a deployment's classes. Since they are logically isolated, and may even be hot-deployed, static linking (imports) can not be used.

 

JDK Service Provider Loading - The common pattern used by Java SE and Java EE frameworks to load different providers / implementations of an API. This is done by checking a system property, a special properties file, and more commonly looking for files in META-INF/services. See the ServiceLoader javadoc for more info on how the process works.

 

Module - A single consistent class loading namespace. It encompases classes/resources directly available in the module (contents of one or more jar(s))  local to the module, as well as all module dependencies defined on the module. Every module has it's own ClassLoader which it uses to load all classes that can be loaded from the module.

 

Deployment - A deployment is a user provided package of components. In AS7, every deployment has a Module. In some cases a deployment may contain nested deployments or other artifacts. Those are often represented by a module of their own.

 

System Classloader - Also commonly referred to as the "application classloader". It represents everything that is defined on java.class.path, and commonly includes java extensions and some jdk classes.

 

Picking the "Right" Classloader

The key to loading classes in a modular environment is to use the correct classloader. In the past many frameworks, including many Java standard APIs, expect that the users classes (or the deployment classes) and the framework classes  are accessible via the same classloader. The problem with this assumption is that it prevents proper isolation of the framework implementation classes and a user's classes. In many cases though it would work in a heirarchical model (as long as there was no name conflicts), since class sharing is done by inheritance rather than explicit links as is the case in a modular environment. This single bad assumption makes it impossible to achieve capabilities like allowing a user to use their own XML parser.

 

The correct way to pick a ClassLoader is to always use the one that is directly associated with the class you are trying to load. So if a framework wants to load a class from one of it's dependencies then it should use it's defining classloader. Likewise, If the framework wants to load a class from a deployment, then it should use the deployment's classloader. In some cases, like in the case of service provider discovery, a framework might want to look for a particular resource in both it's classloader (say a default provider), and the user Classloader. Under this scenario the framework should check both ClassLoaders following the order that best suits it's needs.

 

FRAMEWORKS, NEVER LOAD YOUR CLASSES FROM THE TCCL!!

If you expect portability between different classloading environments, it is always wrong for a framework to load it's classes from the TCCL. Instead use the right classloader, which is most often the defining classloader. If the defining classloader is not the right classloader, then look for a way to get the right loader and use it to load classes. Even if you have an inheritance model with a heirarchy that sees the classes you want, it is always more correct to pick the classloader that "owns" the classes.

 

The TCCL should be used with extreme care. EE code is required to see the defining deployments classloader as the TCCL. Beware though that other frameworks may have different rules, and may set it to another value when executing in their context. If you have access to the classloader you want it's always better to use it over the TCCL.

 

Note that AS7 subsystems that are executing within any kind of EE context can safely asssume that the TCCL will point to the deployment classloader. They should, however, never attempt to load AS7 code from the deployment classloader.

 

What To Export/Expose

The only thing that a module should ever expose is classes that make up it's API contracts. In most cases this means that an AS7 module should not export it's dependencies. This allows for the module and it's calling module to use completely different versions of the same dependency. Deployments can, for example, use commons-bah 1.2 and AS7 code can happily use commons-blah 1.0.

 

How to deal with "Bad" dependencies

If your module/deployment/framework depends on something that isn't compatible with a modular environment. Then you have a couple of options that may workaround the issue. However in some cases the only workable solution is to patch the bad framework.

 

Common workarounds include:

 

  • Swapping TCCL - If a call to a framework method incorrectly uses the TCCL to load it's classes/resources, and the same call does not need to see deployment classes, then temporarily set the TCCL to the framework's defining classloader. (e.g. Thread.currentThread().setContextClassLoader(frameworkClass.getClass().getClassLoader()
  • Prefer calls which have a CL argument  - If you can pass a classloader, prefer that to calls which try to guess (usually picking the TCCL) if possible.
  • Heirarchical Emulation - This is a worst case workaround that should only be used as a last resort. In the unfortunate case where a framework expects both deployment classes and it's own classes to be on the same classloader,  a custom classloader can be created which delegates first to the deployment classsloader and second to frameworks defining classloader. This classloader can the be swapped to the TCCL as described above, or somehow passed to the framework via a mechnism it exposes. This has the undesirable effect of a possible conflict between a user's implementation choices and the frameworks. However, it at least does not pollute the deployment module.

 

If you have a bad dependency then file a bug to get it fixed/replaced. In particularly if you have to use heirarchical emulation.

 

How to use JAXP and other service provider APIs from AS/Framework code

 

EE applications can safely use the no-arg newInstance() methods provided by JAXP. However framework code must either not use them, or use the swapping TCCL workaround described in the "bad" dependencies section above. This ensures that either the container's xml parser is used or the parser explictly imported by the frameworks module (requires a service import). Not doing this will result in the deployments xml parser being used (since the deployment CL is often the TCCL), which may be incompatible.

 

The following is an example of setting the TCCL to the framework's defining classloader. It's recommended to create a simple utility method for every occurence of this in your framework.

 

public class Util {
    public static DocumentBuilderFactory createDocumentBuilderFactory() {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        DocumentBuilderFactory factory;
        try {
            Thread.currentThread().setContextClassLoader(Util.class.getClassLoader());
            factory = DocumentBuilderFactory.newInstance();
        } finally {
            Thread.currentThread().setContextClassLoader(cl);
        }
        return factory;
    }
}

 

 

Serialization

See the aritcle on Modular Serialization.