I have been fairly quiet recently as we've been heads down working on an alpha release of Glassbox 2.0, with a focus on further ease of use in troubleshooting Java applications. Most of the Glassbox functionality will be deployed as a portable Web application that is easy to install and run (except for the parts that have to integrate at a system level to monitor applications).
However, making a portable Web application is challenging because of nondelegating loaders and, you guessed it, logging. Of course, Jakarta Commons Logging is both widespread in its use and as Ceki Gülcü illustrated, there can be a number of problems with how it functions when you have a copy of it deployed to a shared location on a server AND inside a Web application with nondelegating classloaders. We have to support it because libraries we use like Spring use it. However, it turns out that there is a portable work-around: force your Web app's parent loader to load the classes in JCL before you use logging:
private void eagerlyLoadCommonsLogging() {
String classes[] = {
"org.apache.commons.logging.Log",
"org.apache.commons.logging.LogFactory",
…
"org.apache.commons.logging.impl.NoOpLog"
};
ClassLoader myLoader = getClass().getClassLoader();
if (myLoader != null) {
ClassLoader parent = myLoader.getParent();
for (int i=0; i<classes.length; i++) {
try {
Class.forName(classes[i], false, parent);
} catch (ClassNotFoundException cne) {
// ok - not present in parent
}
}
}
}
Of course this exercise isn't limited to logging: any library that a portable Web application uses can have problems like this. But logging is crosscutting, widespread and often used. Some of the forces that make this exercise particularly challenging for us are:
- We don't want to require log4j, commons logging, or slf4j to be installed on the system class loader.
- We don't want to ship many different versions of the app, one per environment
- We don't want to modify the Web app during installation (e.g., to remove overlapping versions of the libraries)
- We don't want to require configuring our app manually (e.g., plugging in a different logging adaptor as slf4j does)
- We need to work with older versions of libraries (e.g., that eager loading code could find JCL 1.0!)
- We want to configure logging for shared classes that are typically on the system classpath (or in the equivalent of Tomcat's common loader). This last one requires us to set the context ClassLoader when initializing a log inside our library (good thing we manage logging with aspects!)
Of course, if you'd like to configure how logging works (e.g., redirecting to files, not leaving on overly verbose INFO logging for used components etc.) you still have to do that programmatically or ask users to configure their global log settings. Indeed, setting them programmatically has its own problems for shared libraries (the configuration would be global).
This is an area where reuse hurts. If you use an emerging logging implementation today you will probably not have any conflicts with existing code. In three years, you may find a lot of them. This is also the reason that tools like jarjar exist. Of course, this is typical for successful component models: they have to become popular before people take isolation and versioning seriously... remember when Java developers were glad that we didn't suffer from DLL hell?