Demonstrates Jakarta Commons Logging (JCL) concepts.

Introduction

This document contains analysis of various JCL use cases. It works both as an educational document and as a specification (of sorts). It's intended audience are (potential) JCL developers and (potential) expert users. The code that accompanies this document demonstrates the cases analysed in the text.

Familiarity with (advanced) class loading concepts and terminology is assumed. Please digest the JCL Technology Guide before starting.

This approach was inspired by a JCL critique written by Ceki Gülcü.

These demonstrations focus on discovery in general and discovery of Log4J in particular. Not only is Log4J the most important target but these are some of the most difficult and contentious cases. Lessons learnt from these cases should easily and safely extrapolate to other cases.

It is important that this document is as accurate as possible both in facts and analysis. Readers are encouraged to contribute corrections and improvements. Please either:

Overview

The basic set up for these demonstrations is intentionally simple. There are no tricks that might obscure a clear view of the concepts. The source code used to run these demonstrations is simple and should be easy to understand.

Each demonstration is initiated by a runner. The runner loads the other code components into appropriate ClassLoaders and then calls the caller.

The caller is used to simulate a class that needs to log something. It is isolated from the code that runs the demonstration in order to allow a greater variety of ClassLoader situations to be simulated. The calling code is split into one class that presents the formatted results and another that actually performs the calls. Calls are made to JCL and to a static logger.

The static logger simply contains a call directly to Log4J. This is useful for comparison.

Now would be a good idea to study the javadocs.

Results are printed (with a little formatting) to System.out. The run target in the ant build scripts runs all cases. It may be difficult to run these demonstrations from an IDE (a clean system ClassLoader is required).

Conventional And Unconventional Context ClassLoaders

This analysis will start by making a division between conventional context classloaders and unconventional ones. Conventionally, the context classloader for a thread executing code in a particular class should either be the classloader used to load the class, an ancestor of that classloader or a descendent of it. The conventional context classloader cases will focus on context classloaders which obey these rules.

Conventional Classloader Cases

The aim of the set up will be isolate the essentials. Only three classloaders will be considered:

This situation is commonly encountered in containers (where the child classloader is the application classloader). In practical situations, the hierarchy is likely to be contain more classloaders but it should be possible either to extrapolate from these simple cases or reduce the more complex ones to these.

The analysis will proceed by considering two cases separately:

In the parent first cases, when the child classloader initiates loading, it starts by delegating to the parent classloader. Only if this classloader does not define the class will the child classloader attempt to define the class. In the child first cases, the child classloader will begin by attempting to define the class itself. Only when it cannot define a class will it delegate to it's parents. Further discussion of each of these cases will be contained in the appropriate section.

The other variable is the setting of the context classloader. In these cases, it will be set to either the system classloader (the default) or the child classloader (the application classloader). Perhaps the cases for setting to the parent classloader should be added (but this would be unusual: conventionally the context classloader should be either unset or set to the application classloader). Contributions of these extra cases would be welcomed.

The cases will be presented in a matrix. This contains details of the classloader set ups including which which classes are definable by which loaders and how the context classloader is set. It also contains the results of analysis about the way that both the static logging and JCL (ideally) should behave.

For example

Context ClassLoader System
Parent JCL+Static
Child JCL+Static
Log4J
Caller
Expected Result JCL->java.util
static FAIL

This describes a case where the context classloader is unset (and so defaults to the system), the static.jar and commons-logging.jar are definable by the parent classloader, the static.jar, commons-logging.jar and log4j are definable by the child. The caller.jar is also definable by the child classloader. The expected result in this case is that JCL will discover java.util.logging and that logging statically to Log4j will fail.

For purposes of reference, an indication is given in those cases that correspond to examples used by Ceki in his critique.

Analytical notes explaining how these results were obtained are included below each table. As the cases proceed, the notes will grow more terse.

Parent First ClassLoader Cases

Log4J Defined By Child, JCL By Parent
Case 1 2 3 4
Ceki Example - 1 2A 3
Context ClassLoader System System Child Child
Parent JCL+Static JCL+Static Caller JCL+Static JCL+Static Caller
Child JCL+Static
Log4J
Caller
JCL+Static
Log4J
JCL+Static
Log4J
Caller
JCL+Static
Log4J
Expected Result JCL->java.util
static FAIL
JCL->java.util
static FAIL
JCL->java.util
static FAIL
JCL->java.util
static FAIL

One important point to bear in mind when analysing parent-first classloaders is that the presence or absence in the child classloader of classes definable by the parent classloader produces no difference in behaviour: the parent classloader will always load the class in question.

In the cases above, Log4J is defined (only) by the child and all JCL classes by the parent classloader. The symbolic references from Log4JLogger to Log4J classes therefore cannot be resolved as Log4j is not definable by the parent classloader. For the same reason, the static call should also always fail.

The context classloader can be of no use (whether set or not) since Log4JLogger will always be defined by the parent classloader whether loading is initiated by the child or by the parent loader.

Therefore, in these cases, the appropriate behaviour is for JCL to discover that java.util.logging is the only available logging system.

Log4J Defined By Parent, JCL By Parent
Case 5 6 7 8
Ceki Example - - - -
Context ClassLoader System System Child Child
Parent Log4J
JCL+Static
Log4J
JCL+Static
Caller
Log4J
JCL+Static
Log4J
JCL+Static
Caller
Child JCL+Static
Log4J
Caller
JCL+Static
Log4J
JCL+Static
Log4J
Caller
JCL+Static
Log4J
Expected Result JCL->log4j
static OK
JCL->log4j
static OK
JCL->log4j
static OK
JCL->log4j
static OK

This demonstrates the usual way to fix the problems encountered when Log4J is not definable by the loader that defines JCL: just add the Log4j.jar into the classloader that defines JCL.

This is a very simple set of cases with JCL and static being able to resolve the appropriate symbolic references to Log4j directly. Whether the context classloader is set or not in these cases should make no difference.

Log4J Defined By Child, JCL API By Parent, JCL By Child
Case 9 10 11 12
Ceki Example - - - -
Context ClassLoader System System Child Child
Parent API+Static API+Static
Caller
API+Static API+Static
Caller
Child JCL+Static
Log4J
Caller
JCL+Static
Log4J
JCL+Static
Log4J
Caller
JCL+Static
Log4J
Expected Result JCL->java.util
static FAIL
JCL->java.util
static FAIL
JCL->log4j
static FAIL
JCL->log4j
static FAIL

These demonstrate variations on the first series of cases where Log4J is defined by the child. The placement of the static jar does not vary (from the previous set) and again is expected to consistently fail.

This time the API jar is placed in the parent classloader and the full JCL jar in the child. This means that the symbolic reference from the Log4JLogger class (contained in the full jar but not the API) to Log4J classes can be resolved.

In these cases, whether JCL can succeed depends on the context classloader. The delegation model employed by Java ClassLoaders allows child classloaders to know about parents but not vice versa. Therefore, the context classloader is the only means available to attempt to load Log4JLogger. When the context classloader is set to the child, this classloader defines Log4JLogger and Log4J. Therefore, JCL should be able to discover Log4J (only) when the context classloader is set to the child.

Log4J Defined By Parent, JCL API By Parent, JCL By Child
Case 13 14 15 16
Ceki Example - - - -
Context ClassLoader System System Child Child
Parent Log4J
API+Static
Log4J
API+Static
Caller
Log4J
API+Static
Log4J
API+Static
Caller
Child JCL+Static
Log4J
Caller
JCL+Static
Log4J
JCL+Static
Log4J
Caller
JCL+Static
Log4J
Expected Result JCL->java.util
static OK
JCL->java.util
static OK
JCL->log4j
static OK
JCL->log4j
static OK

Trivial variations on the second series. Now API and JCL are definable only by parent and child respectively (as in the last series). When the context classloader is set to system, Log4JLogger cannot be loaded and so java.util.logging is the only viable logger.

Child First ClassLoader Cases

The same classloader configuration can be repeated with (this time) child-first classloading.

Log4J Defined By Child, JCL By Parent
Case 17 18 19 20
Ceki Example - - 5 6
Context ClassLoader System System Child Child
Parent JCL+Static JCL+Static
Caller
JCL+Static JCL+Static
Caller
Child JCL+Static
Log4J
Caller
JCL+Static
Log4J
JCL+Static
Log4J
Caller
JCL+Static
Log4J
Expected Result JCL->Log4j
static OK
JCL->java.util
static FAIL
JCL->Log4j
static OK
JCL->java.util
static FAIL

In child-first cases, the classloader which defines the caller plays a more important role. When the caller is defined by the child classloader, both static and JCL should succeed whether the context classloader is set or not.

When the caller is defined by the parent classloader, this means that the parent classloader will define the JCL and static classes (rather than the child). Log4J is not defined by the parent loader and so the static call must fail in both cases.

With a friendly context classloader, JCL can load an instance of Log4JLogger whose symbolic references to Log4J can be resolved. However, the child classloader which defines this class will resolve the symbolic reference to Log to the class defined by the child classloader rather than the Log class defined by the parent classloader. They are not mutually accessible and so JCL must discover java.util.logging.

Log4J Defined By Parent, JCL By Parent
Case 21 22 23 24
Ceki Example - - - -
Context ClassLoader System System Child Child
Parent Log4J
JCL+Static
Log4J
JCL+Static
Caller
Log4J
JCL+Static
Log4J
JCL+Static
Caller
Child JCL+Static
Log4J
Caller
JCL+Static
Log4J
JCL+Static
Log4J
Caller
JCL+Static
Log4J
Expected Result JCL->log4j
static OK
JCL->log4j
static OK
JCL->log4j
static OK
JCL->log4j
static OK

Trivial case where jar's are definable by both loaders.

Log4J Defined By Child, JCL API By Parent, JCL By Child
Case 25 26 27 28
Ceki Example - - - -
Context ClassLoader System System Child Child
Parent API+Static API+Static
Caller
API+Static API+Static
Caller
Child JCL+Static
Log4J
Caller
JCL+Static
Log4J
JCL+Static
Log4J
Caller
JCL+Static
Log4J
Expected Result JCL->log4j
static OK
JCL->java.util
static FAIL
JCL->log4j
static OK
JCL->java.util
static FAIL

(As above) to succeed, the static call requires that Log4J is definable by the loader which defines the callers. JCL should discover Log4J in the cases where the static call succeeds.

Even with a friendly context classloader, the Log class referenced by the Log4JLogger defined by the child classloader will be inaccessible to the caller (loader by the parent classloader).

Log4J Defined By Parent, JCL API By Parent, JCL By Child
Case 29 30 31 32
Ceki Example - - - -
Context ClassLoader System System Child Child
Parent Log4J
API+Static
Log4J
API+Static
Caller
Log4J
API+Static
Log4J
API+Static
Caller
Child JCL+Static
Log4J
Caller
JCL+Static
Log4J
JCL+Static
Log4J
Caller
JCL+Static
Log4J
Expected Result JCL->log4j
static OK
JCL->java.util
static OK
JCL->log4j
static OK
JCL->java.util
static OK

These results at first glance may seem a little confusing. The issue for JCL is that classes needed to log to Log4J are only definable by the child classloader.

Even with a friendly context classloader, JCL runs into the same difficulties with accessibility that occurred above.