Instead of intro
This topic assumes that reader has above average Java skills, and, at least, creating own class loader does not sound to him as terrible hack or Masonic mystery.
Where all problems start...
As it sometimes happens, after some upgrade of third-party library my code stops working. Who knows when I will discover this issue with class loading otherwise, but if it soooo itches -- it will be scratched, immediately ;)
You know, any of us never writes bugs ;) So the reason should be definitely with third-party library. Ok, I get ClassNotFoundException (CNFE below) and was almost sure that they did something dumb, because my application works perfectly with previous version. I was sure that they used wrong class loader - anyway, what else could happen???
After several hours of debugging I noticed subtle difference in library code. Instead of ClassLoader.loadClass(clsName) they start to use Class.forName(clsName, init, classLoader). In both cases class loader was correct - it was my own (bugless, remember ;) custom class loader. However, forName throws CNFE. I trace execution sequence - my class loader is invoked, it loads class successfully, but CNFE was still thrown! What the heck?
Root of all classes, root of all evil...
After googling for "forName AND loadClass" in Sun BugDatabase I still get nothing. Except 2 observations:
- Try the search yourself, how many references are you getting? It is amazing that functionality that lies at very core of Java causes such amount of bug reports.
- Any stack traces I see has roots inside of ClassLoader methods (somewhere below ClassLoader.loadClass), in my case root of exception was native method Class.forName0.
So I write a standalone test program, that emulates my class loading behavior and interactions between my code and library code. Same error! Replacing Class.forName with ClassLoader.loadClass yields correct result!
So the problem is class loader itself. If you study accompanied sample code, you may notice that very specific class loading scheme is used. Unlike classic Parent-Child relationship my class loader uses something like Self-Siblings-Parent scheme. Former one is standard: first class loader queries parent class loader to load class, then if not found it tries to load class itself. My scheme is more complex: first try to map supplied name of class ("synonym") to actual class name, and, if found, uses parent class loader to load bytecode, but defines class itself (class loader, that is used for defineClass will be returned by clsInstance.getClassLoader). If synonym is unknown at this step, then my class loader asks siblings to load class by synonym. If they also fail, then class loader decides that name is not synonym but rather regular class name and delegates call to parent. Tricky? Yes. Forbidden / Incorrect ? No. Otherwise please point me to JLS / API docs where this is stated explicitly.
So, if you are invoking Class.forName(name, init, classLoaderA) it is possible that class is loaded by some classLoaderB, that is neither classLoaderA nor any parent on chain of classLoaderA.
Don't ask me why I've created such class loading scheme. However, similar could be found with JBoss "Unified Class Loader". Also I'm not sure, but SAP WebAS has to resolve references (remember? library, interface, service references...) in similar way. So this is not exceptional case.
But why?..
As I mentioned above, all that jazz happens in native Class.forName0 method. I have no sources for this method, but from results I got I may conclude that for Sun JVM (vendor is important!, see below) it looks like the following in Java:
private Class forName0(String clsName, boolean init, ClassLoader originalCL)
throws ClassNotFoundException
{
/* loadClassInternal delegates to loadClass */
final Class cls = originalCL.loadClassInternal(clsName, init);
final ClassLoader actualCL = cls.getClassLoader();
for (ClassLoader x = originalCL; x != null; x = x.getParent())
{
/* Identity equality or equals, who knows ;) */
if ( x == actualCL) return cls;
}
/*
* Class is loaded by class loader that is
outside class loaders hierarchy of originalCL
*/
throw new ClassNotFoundException(clsName);
}
At least, this is observable behavior (handling null as bootstrap class loader is omitted).
Now the question are: 1) where this behavior of Class.forName is exactly described; 2) where there is at least a notice that such check could ever happen; 3). does Sun understands, that this way they implicitly forbid any class loading scheme besides classic Parent-Child?
I've submitted this issue as bug to Sun BugDatabase, but it takes 3 weeks for review. Let us see what will follow.
Just pray that it works expected way...
Menwhile, I've already get answers to questions 1 and 2 myself: the post-loading checks in Class.forName are totally unspecified, and may yield different results when using JVMs by different vendors.
Below are results of my JVMs shootout. You may add your own results, just download sample program and submit output of 3 command lines:
java -version
java -jar cl_test.jar
java -jar cl_test.jar error
Second command uses version with ClassLoader.loadClass, and it works identically with all JVMs. Third line force usage of Class.forName and results are varying.
Sun JVM 1.4.2 Windows
Java 5 produces the same results, so I've omitted it
java -version
java version "1.4.2_06"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.2_06-b03)
Java HotSpot(TM) Client VM (build 1.4.2_06-b03, mixed mode)
java -jar cl_test.jar
MODEL CL: #27355241
This: #12115735
Class: j5cl.main.impl.Part_01 #20876681
ClassLoader: #31866429
This: #27837671
Class: j5cl.main.impl.Part_02 #18296328
ClassLoader: #31866429
This: #30223967
Class: j5cl.main.impl.Part_01 #27235645
ClassLoader: #24287316
This: #32429958
Class: j5cl.main.impl.Part_02 #25669322
ClassLoader: #24287316
This: #14978587
Class: j5cl.main.impl.Part_02 #25669322
ClassLoader: #24287316
java -jar cl_test.jar error
MODEL CL: #27355241
java.lang.IllegalArgumentException: Unnable to load implementation class: app.PartA
at j5cl.main.impl.ThingClass_ForName.createPart(ThingClass_ForName.java:30)
at j5cl.main.Test.main(Test.java:42)
Caused by: java.lang.ClassNotFoundException: app/PartA
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:219)
at j5cl.main.impl.ThingClass_ForName.createPart(ThingClass_ForName.java:13)
... 1 more
Exception in thread "main"
IBM JVM 1.4.2 Windows
I like this one: the check is either even more strict or just na?ve. Anyway, error message deserves applauds - it is totally self-explanatory
java -version
java version "1.4.2"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.2)
Classic VM (build 1.4.2, J2RE 1.4.2 IBM Windows 32 build cn1420-20040626 (JIT enabled: jitc))
java -jar cl_test.jar
MODEL CL: #1934690227
This: #2072987570
Class: j5cl.main.impl.Part_01 #1776109491
ClassLoader: #1934985139
This: #2104821682
Class: j5cl.main.impl.Part_02 #1777911731
ClassLoader: #1934985139
This: #18188210
Class: j5cl.main.impl.Part_01 #1777551283
ClassLoader: #1935017907
This: #50939826
Class: j5cl.main.impl.Part_02 #1777190835
ClassLoader: #1935017907
This: #52758450
Class: j5cl.main.impl.Part_02 #1777190835
ClassLoader: #1935017907
java -jar cl_test.jar error
MODEL CL: #1934854089
Exception in thread "main" java.lang.NoClassDefFoundError: Bad class name
(expect: app/PartA, get: j5cl/main/impl/Part_01)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:260)
at j5cl.main.impl.ThingClass_ForName.createPart(ThingClass_ForName.java:13)
at j5cl.main.Test.main(Test.java:42)
Sun(?) JVM 1.3.1 Mac
Top of stupidity - duplicate classes may happens with single class loader, not with separate ones
java -version
java version "1.3.1"
Java(TM) 2 Runtime Environment, Standard Edition
(build 1.3.1-root_1.3.1_020714-12:46)
Java HotSpot(TM) Client VM (build 1.3.1_03-69, mixed mode)
[iMac-400-Users-Computer:/shara]
java -jar cl_test.jar
MODEL CL: #6489619
This: #1795395
Class: j5cl.main.impl.Part_01 #1387762
ClassLoader: #3496821
This: #6160135
Class: j5cl.main.impl.Part_02 #1205273
ClassLoader: #3496821
This: #3817081
Class: j5cl.main.impl.Part_01 #6000338
ClassLoader: #6196891
This: #3238994
Class: j5cl.main.impl.Part_02 #4106754
ClassLoader: #6196891
This: #727841
Class: j5cl.main.impl.Part_02 #4106754
ClassLoader: #6196891
java -jar cl_test.jar error
MODEL CL: #6489619
This: #6966304
Class: j5cl.main.impl.Part_01 #2873465
ClassLoader: #4138247
This: #2039515
Class: j5cl.main.impl.Part_02 #2291301
ClassLoader: #4138247
Exception in thread "main" java.lang.LinkageError: duplicate class
definition: j5cl/main/impl/Part_01
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:190)
at
j5cl.main.impl.ThingClass_ForName.createPart(ThingClass_ForName.java:13)
at j5cl.main.Test.main(Test.java:44)
BEA JRockit 7.0 / 1.4.0
Yes! The winner! As an option, your code may even run correctly!
java -version
java version "1.4.0"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.0)
BEA Weblogic JRockit(R) Virtual Machine
(build 7.0-1.4.0-win32-GARAK-20020830-1714, Native Threads, Generational Concurrent Garbage Collector)
java -jar cl_test.jar
MODEL CL: #4719
This: #15
Class: j5cl.main.impl.Part_01 #17
ClassLoader: #4721
This: #19
Class: j5cl.main.impl.Part_02 #21
ClassLoader: #4721
This: #23
Class: j5cl.main.impl.Part_01 #25
ClassLoader: #4723
This: #27
Class: j5cl.main.impl.Part_02 #29
ClassLoader: #4723
This: #31
Class: j5cl.main.impl.Part_02 #29
ClassLoader: #4723
java -jar cl_test.jar error
MODEL CL: #4719
This: #15
Class: j5cl.main.impl.Part_01 #17
ClassLoader: #4721
This: #19
Class: j5cl.main.impl.Part_02 #21
ClassLoader: #4721
This: #23
Class: j5cl.main.impl.Part_01 #17
ClassLoader: #4721
This: #25
Class: j5cl.main.impl.Part_02 #21
ClassLoader: #4721
This: #27
Class: j5cl.main.impl.Part_02 #21
ClassLoader: #4721
Nice diversity of options: checked exception (ClassNotFoundException), runtime error (NoClassDefFoundError), dumb runtime error (special subclass of runtime errors ;) and correct execution! Do you think this should be left "as is"?
You may download sample program here (use "Save as...):
Executable JAR
Sources (rename to *.zip)