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:
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).
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.
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.
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.
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.
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.
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.
The same classloader configuration can be repeated with (this time) child-first classloading.
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.
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.
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).
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.