Updating code at runtime (spring-loaded demystified)
When the development cycle from compile over deployment up to testing takes too long, one wishes to be able to replace the running code just in time without the need for restarting an application server and waiting until deployment has been finished. Commercial solutions like JRebel or open source frameworks like Grails help in such kind of situations.
Replacing code at runtime is not supported out of the box by the JVM in a kind like you can dynamically load classes with for example Class.forName()
. Basically you have the following options:
- HotSwap: A technolog introduced with Java 1.4 that allows you to redefine classes within a debugger session. This approach is very limited as it only allows you to change the body of a method but not the addition of new methods or classes.
- OSGi: This technology allows you to define bundles. At runtime a bundle can be replaced by a newer version of this bundle.
- Throwaway classloaders: By wrapping a separate Classloader over all classes of your module, you can throw away the Classloader and replace it, once a new version of your module is availalbe.
- Instrumenting classes with a Java Agent: A Java Agent can instrument classes before they are defined. This way it can inject code into loaded classes that connects this class with one version of the class file. Once a new version is available, the new code gets executed.
The technology behing Grails is called spring-loaded and uses the “Java Agent” approach to instrument classes that are loaded from the file system and not from a jar file. But how does this work under the hood?
To understand spring-loaded, we setup a small sample project that allows us to examine the technology in more detail. This project only consists of two classes: the Main
class calls the print()
method of the ToBeChanged
class and sleeps for a while:
public static void main(String[] args) throws InterruptedException { while (true) { ToBeChanged toBeChanged = new ToBeChanged(); toBeChanged.print(); Thread.sleep(500); } }
The print()
method just prints out a version, such that we can see that it has changed. Additionally we also print out the stack trace in order to see how this changes over time:
public void print() { System.out.println("V1"); StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); for (StackTraceElement element : stackTrace) { System.out.println("\t" + element.getClassName() + "." + element.getMethodName() + ":" + element.getLineNumber()); } }
When starting the application we have to provide the jar file that contains the Java Agent using the option javaagent
. As spring-loaded modifies the bytecode in a way that the verifier does not like, we have to disable verification of the bytecode by passing the option noverify
to the JVM. Finally we pass the folder that contains our class files with cp
and tell the JVM the class that contains the main()
method:
java -javaagent:springloaded-1.2.4.BUILD-SNAPSHOT.jar -noverify -cp target/classes com.martinsdeveloperworld.springloaded.Main
After having updated the version in class ToBeChanged
from V1
to V2
and rebuilding the project with mvn package
, we see the following output:
... V1 java.lang.Thread.getStackTrace:-1 com.martinsdeveloperworld.springloaded.ToBeChanged.print:7 com.martinsdeveloperworld.springloaded.Main.main:8 V2 java.lang.Thread.getStackTrace:-1 com.martinsdeveloperworld.springloaded.ToBeChanged$$EPBF0gVl.print:7 com.martinsdeveloperworld.springloaded.ToBeChanged$$DPBF0gVl.print:-1 com.martinsdeveloperworld.springloaded.ToBeChanged.print:-1 com.martinsdeveloperworld.springloaded.Main.main:8 ...
The stacktrace of version V1
looks like we have expected. From Main.main()
the method ToBeChanged.print()
gets called. This differs for version V2
. Here the method ToBeChanged.print
now calls the method ToBeChanged$$DPBF0gVl.print()
. Please also note that the line number for the call ToBeChanged.print()
has changed from 8 to -1, indicating that the line is not known.
The new line number -1 is a strong indication that the Java Agent has instrumented the method ToBeChanged.print()
in a way that allows it to call the new method instead of executing the old code. To prove this assumption, I have added a few logging statements to the code of spring-loaded and a feature that dumps each instrumtend file to the local hard drive. This way we can inspect how the method ToBeChanged.print()
looks like after instrumentation:
0 getstatic #16 <com/martinsdeveloperworld/springloaded/ToBeChanged.r$type> 3 ldc #72 <0> 5 invokevirtual #85 <org/springsource/loaded/ReloadableType.changed> 8 dup 9 ifeq 42 (+33) 12 iconst_1 13 if_icmpeq 26 (+13) 16 new #87 <java/lang/NoSuchMethodError> 19 dup 20 ldc #89 <com.martinsdeveloperworld.springloaded.ToBeChanged.print()V> 22 invokespecial #92 <java/lang/NoSuchMethodError.<init>> 25 athrow 26 getstatic #16 <com/martinsdeveloperworld/springloaded/ToBeChanged.r$type> 29 invokevirtual #56 <org/springsource/loaded/ReloadableType.fetchLatest> 32 checkcast #58 <com/martinsdeveloperworld/springloaded/ToBeChanged__I> 35 aload_0 36 invokeinterface #94 <com/martinsdeveloperworld/springloaded/ToBeChanged__I.print> count 2 41 return 42 pop 43 getstatic #100 <java/lang/System.out> 46 ldc #102 <V1> 48 invokevirtual #107 <java/io/PrintStream.println> 51 invokestatic #113 <java/lang/Thread.currentThread> 54 invokevirtual #117 <java/lang/Thread.getStackTrace> 57 astore_1 ... 152 return
The getstatic
opcode retrieves the value for the new field r$type
and pushes it on the stack (opcode ldc
). Then the method ReloadableType.changed()
gets called for the object reference that was pushed on the stack before. As the name indicates, the method ReloadableType.changed()
checks whether a new version of this type exists. It returns 0 if the method did not change and 1 if it has changed. The following opcode ifeq
jumps to line 42 if the returned value was zero, i.e. the method has not changed. From line 42 on we see the original implementation which I have shortened here a little bit.
If the value is 1, the if_icmpeq
instruction jumps to line 26, where the static field r$type
is read once again. This reference is used to invoke the method ReloadableType.fetchLatest()
on it. The following checkcast
instruction verifies that the returned reference is of type ToBeChanged__I
. Here we stumble for the first time over this artifical interface that spring-loaded generates for each type. It reflects the methods the original class had when it was instrumented. Two lines later this interface is used to invoke the method print()
on the reference that was returned by ReloadableType.fetchLatest()
.
This reference is not the reference to the new version of the class but to a so called dispatcher. The dispatcher implements the interface ToBeChanged__I
and implements the method print()
with the following instructions:
0 aload_1 1 invokestatic #21 <com/martinsdeveloperworld/springloaded/ToBeChanged$$EPBF0gVl.print> 4 return
The dynamically generated class ToBeChanged$$EPBF0gVl
is the so called executor and embodies the new version of the type. For each new version a new dispatcher and executor is created, only the interface remains the same. Once a new version is available, the interface method is invoked on the new dispatcher and this one forwards in the simplest case to the new version of the code embodied in the executor. The reason why the interface method is not called directly on the exeuctor is the fact that spring-loaded can also handle cases in which methods are added in a new version of the class. As this methods do not exist in the old version, a generic method __execute()
is added to the interface and the dispatcher. This dynamic method can then dispatch calls to new methods as shown in the following instruction set taken from the generated dispatcher:
0 aload_3 1 ldc #25 <newMethod()V> 3 invokevirtual #31 <java/lang/String.equals> 6 ifeq 18 (+12) 9 aload_2 10 checkcast #33 <com/martinsdeveloperworld/springloaded/ToBeChanged> 13 invokestatic #36 <com/martinsdeveloperworld/springloaded/ToBeChanged$$EPBFaboY.newMethod> 16 aconst_null 17 areturn 18 aload_3 ... 68 areturn
In this case I have added a new method called newMethod()
to the class ToBeChanged
. The beginning of the __execute()
method compares whether the descriptor invoked matches the new method. If this is the case, it forwards the invocation to the new executor. In order to let this work, all invocations of the new method have to be rewritten to the __execute()
method. This is also done via instrumentation of the original classes and does also work for reflection.
Conclusion
spring-loaded demonstrates that it is possible to “replace” a class with a newer version at runtime. To achieve this, a series of Java technologies like the Java Agent and bytecode instrumentation are utilized. By taking a closer look at the implementation, one can learn a lot of things about the JVM and Java in general.
Reference: | Updating code at runtime (spring-loaded demystified) from our JCG partner Martin Mois at the Martin’s Developer World blog. |