It seems that the "same" classes from different classloaders shouldn't appear in a same execution context, OSGI / app server ensure boundaries are not violated.
Imaging we escaped a jail and are comparing objects, which are "equal" (data & package & class name) but loaded from different jars: methods' byte code could be different, one could be compiled for Java 8, another for Java 17.
I expect obj1 instanceof Clz2
& obj1.getClass() == obj2.getClass()
return false
.
Do classloaders break equality and inheritance?
Did you try it? You would indeed end up with false. You can write code like so:
import com.foo.Foo;
void test(Object o) {
System.out.println(o.getClass());
System.out.println(o instanceof Foo);
}
and call it so that it first prints com.foo.Foo
and then nevertheless prints false
, even leading to the fun error message "An instance of type com.foo.Foo cannot be cast to com.foo.Foo".
The thing is, you would have 2 actual instances of java.lang.Class
here, both with name com.foo.Foo
, but with different values for 'classLoader', and as far as java is concerned, these 2 classes are not, in any way, related or compatible.
When writing:
foo instanceof Foo
There are 2 types involved:
The Foo
that was written directly into the source file, translated to byte code, by the JVM that bytecode (the class file) was read in and 'parsed', and this results in one take on 'what class is this actually', and it'll have the same classloader as whatever class this code is in.
The foo
variable is a reference variable; it is pointing at some object. That object knows what its type is - that object began life by some new X()
call, and that 'type' is the exact same principle at work as in the previous bullet, but this time for X
, and crucially, this time in the context of the class that the new X()
source code expression is in.
If ClassThatExecutedNewFoo
and ClassThatIsExecutingInstanceofFoo
have different loaders and they didn't both end up delegating the job of loading Foo
to a common parent, then you end up with 2 different classes, and therefore, foo instanceof Foo
resulting in false
, even if foo.getClass().getName().equals(Foo.class.getName())
is true
. Yes, the JVM can load the same identical class more than once if you want to, so long as multiple class loaders are involved.
A crucial aspect here is classloader hierarchy. ClassLoaders always have a parent, and classloader really really tries very hard to make you write new ones such that they first ask their 'parent loader' to load a class, and only if the parent can't get the job done, then and only then does the ClassLoader itself try.
The reason, once you think about it, should be obvious: If it did not work like that, and each classloader aims to load whatever you ask on its own, then that means a class loaded by loader A and a class loaded by loader B don't even agree on something as utterly fundamental as java.lang.String
. Or worse, java.lang.Object
. Which makes any attempt to communicate between code from these 2 classes pretty much impossible. There has to be some form of common ground for them to talk to each other.
Hence, if you simply follow the obvious path and write class MyLoader extends ClassLoader
and you implement the findClass
method, you are likely to run into this shared parentage situation. You could make 2 loaders and nevertheless conclude: Odd, I asked loader A to load com.foo.Foo
, I then asked loader B to load it, and I was expecting to end up with 2 Foo
types that are completely incompatible (instanceof
is false, that sort of thing). And yet they aren't.
If that happens, it's because both of your loaders first asked parent to laod it, and it succeeded, meaning the loader of Foo
is actually that parent and not A, nor B. Same class loaded by the same loader? That really is the same class and therefore things are compatible with each other.
You can stop this from happening; either ensure parent-loader can't find it (in the common situation where 'parent loader' is the main app loader that java creates for you, simply ensure com.foo.Foo
is not on the class path!) - or, override loadClass
, but when you do that, be real careful. You should not defineClass()
the class up in a loader unless you want that class to be incompatible with any other loader's attempt to do so (other than children of your own loader of course). e.g. trying to load java.lang.String
will work but means Strings in classes you load are now incompatible with strings anywhere else, and that's rather unlikely to be something you want.