The first thing to understand when talking about reloading Java code is the relation between classes and objects. All Java code is associated with methods contained in classes. Simplified, you can think of a class as a collection of methods, that receive "this" as the first argument. The class with all its methods is loaded into memory and receives a unique identity. In the Java API this identity is represented by an instance of
java.lang.Class
that you can access using theMyObject.class
expression.
Every object created gets a reference to this identity accessible through the
Object.getClass()
method. When a method is called on an object, the JVM consults the class reference and calls the method of that particular class. That is, when you call mo.method()
(where mo
is an instance ofMyObject
), then the JVM will call mo.getClass().getDeclaredMethod("method").invoke(mo)
(this is not what the JVM actually does, but the result is the same).
Every
Class
object is in turn associated with its classloader (MyObject.class.getClassLoader()
). The main role of the class loader is to define a class scope -- where the class is visible and where it isn't. This scoping allows for classes with the same name to exist as long as they are loaded in different classloaders. It also allows loading a newer version of the class in a different classloader.
The main problem with code reloading in Java is that although you can load a new version of a class, it will get a completely different identity and the existing objects will keep referring the previous version of the class. So when a method is called on those objects it will execute the old version of the method.
Let's assume that we load a new version of the
MyObject
class. Let's refer to the old version asMyObject_1
and to the new one as MyObject_2
. Let's also assume that MyObject.method()
returns "1" in MyObject_1
and "2" in MyObject_2
. Now if mo2
is an instance of MyObject_2
:mo.getClass() != mo2.getClass()
mo.getClass().getDeclaredMethod("method").invoke(mo)
!= mo2.getClass().getDeclaredMethod("method").invoke(mo2)mo.getClass().getDeclaredMethod("method").invoke(mo2)
throws aClassCastException
, because theClass
identities ofmo
andmo2
do no match.
This means that any useful solution must create a new instance of
mo2
that is an exact copy of mo
and replace all references to mo
with it. To understand how hard it is, remember the last time you had to change your phone number. It's easy enough to change the number itself, but then you have to make sure that everyone you know will use the new number, which is quite a hassle. It's just as difficult with objects (in fact, it's actually impossible, unless you control the object creation yourself), and we're talking about many objects that you must update at the same time.Down and Dirty
Let's see how this would look in code. Remember, what we're trying to do here is load a newer version of a class, in a different classloader. We'll use an
Example
class that looks like this:
We'll use a
main()
method that will loop infinitely and print out the information from the Example
class. We'll also need two instances of the Example
class: example1 that is created once in the beginning and example2 that is recreated on every roll of the loop:IExample
is an interface with all the methods from Example
. This is necessary because we'll be loading Example
in an isolated classloader, so Main
cannot use it directly (otherwise we'd get aClassCastException
).
From this example, you might be surprised to see how easy it is to create a dynamic class loader. If we remove the exception handling it boils down to this:
The method
getClassPath()
for the purposes of this example could return the hardcoded classpath. However, in the full source code (available in the Resources section below) you can see how we can use the ClassLoader.getResource()
API to automate that.
Now let's run
Main.main
and see the output after waiting for a few loop rolls:
As expected, while the counter in the first instance is updated, the second stays at "0". If we change the
Example.message()
method to return "Version 2". The output will change as follows:
As we can see, the first instance continues incrementing the counter, but uses the old version of the class to print out the version. The second instance class was updated, however all of the state is lost.
To remedy this, let's try to reconstruct the state for the second instance. To do that we can just copy it from the previous iteration.
First we add a new
copy()
method to Example
class (and corresponding interface method):
Next we update the line in the
Main.main()
method that creates the second instance:
Now waiting for a few iterations yields:
And changing
Example.message()
method to return "Version 2" yields:
As you can see even though it's possible for the end user to see that the second instance is updated and all its state is preserved, it involves managing that state by hand. Unfortunately, there is no way in the Java API to just update the class of an existing object or even reliably copy its state, so we will always have to resort to complicated workarounds.
~jevgeni
~jevgeni
No comments:
Post a Comment