Reloading a Java class definition at runtime

When developing the UI for a web application in Java, the following workflow might be common:
-
make a change at some source file;
-
recompile the project;
-
restart the server; and
-
test the change.
Restarting the server each time a change is made is not ideal. Even if your application starts fast.
The restart might not always be required if we are able to reload class definitions at runtime.
In this blog post I will show how to reload the definition of a single class at runtime.
Let's learn.
A Class reloader
We need a class that can load and reload a single class definition at runtime.
We will name our class ClassReloader
.
Let's start with the test.
The Subject class
In our test we will use our reloader to load the following class definition:
package reloader;public class Subject { @Override public String toString() { return "I'm the original!"; }}
We'll then create an instance of the loaded class.
We expect its toString
method to return the "I'm the original!" value.
Next, we'll reload the second version of our Subject
type:
package reloader;public class Subject { @Override public String toString() { return "Reloading successful!"; }}
We will then create a new instance of the, hopefully, reloaded class definition.
We expect its toString
to now return the "Reloading successful!" value.
Creating hex dumps of our test class
We manually compile the two versions of our Subject
class.
Next we:
-
use the
xxd
tool to create hex dumps of the class files; and -
use a text block to store the hex dumps in our test.
Here's the current iteration of our test class:
public class ClassReloaderTest { private static final String ORIGINAL = """ cafebabe0000004100110a000200030700040c000500060100106a617661\ 2f6c616e672f4f626a6563740100063c696e69743e010003282956080008\ 01001149276d20746865206f726967696e616c2107000a01001072656c6f\ 616465722f5375626a656374010004436f646501000f4c696e654e756d62\ 65725461626c65010008746f537472696e6701001428294c6a6176612f6c\ 616e672f537472696e673b01000a536f7572636546696c6501000c537562\ 6a6563742e6a617661002100090002000000000002000100050006000100\ 0b0000001d00010001000000052ab70001b100000001000c000000060001\ 000000030011000d000e0001000b0000001b00010001000000031207b000\ 000001000c000000060001000000060001000f000000020010\ """; private static final String UPDATED = """ cafebabe0000004100110a000200030700040c000500060100106a617661\ 2f6c616e672f4f626a6563740100063c696e69743e010003282956080008\ 01001552656c6f6164696e67207375636365737366756c2107000a010010\ 72656c6f616465722f5375626a656374010004436f646501000f4c696e65\ 4e756d6265725461626c65010008746f537472696e6701001428294c6a61\ 76612f6c616e672f537472696e673b01000a536f7572636546696c650100\ 0c5375626a6563742e6a6176610021000900020000000000020001000500\ 060001000b0000001d00010001000000052ab70001b100000001000c0000\ 00060001000000030011000d000e0001000b0000001b0001000100000003\ 1207b000000001000c000000060001000000060001000f000000020010\ """;}
The ORIGINAL
text block holds the first version of our class.
And the UPDATED
text block, the second.
The test method
With the hex dumps of our Subject
class we write the following test method:
@Test(description = """It should be able to load and reload the subject class.""")public void testCase01() throws Exception { Path classOutput; classOutput = Files.createTempDirectory("reloader-"); ClassReloader reloader; reloader = new ClassReloader(classOutput); try { Path packageDir; packageDir = classOutput.resolve("reloader"); Files.createDirectories(packageDir); Path classFile; classFile = packageDir.resolve("Subject.class"); HexFormat hexFormat; hexFormat = HexFormat.of(); Files.write(classFile, hexFormat.parseHex(ORIGINAL)); assertEquals(execute(reloader), "I'm the original!"); Files.write(classFile, hexFormat.parseHex(UPDATED)); assertEquals(execute(reloader), "Reloading successful!"); } finally { deleteRecursively(classOutput); }}
We begin by creating a temporary directory representing the "class output" of a Java project.
In a Maven project, for example, it would represent the target/classes
directory.
We use this directory to create our ClassReloader
instance:
Path classOutput;classOutput = Files.createTempDirectory("reloader-");ClassReloader reloader;reloader = new ClassReloader(classOutput);
Our reloader will try to find .class
files in this directory.
Next, using the ORIGINAL
hex dump, we create the class file of our Subject
class:
Path packageDir;packageDir = classOutput.resolve("reloader");Files.createDirectories(packageDir);Path classFile;classFile = packageDir.resolve("Subject.class");HexFormat hexFormat;hexFormat = HexFormat.of();Files.write(classFile, hexFormat.parseHex(ORIGINAL));
It first creates the directory representing the package of the class.
It then uses the HexFormat
class to convert the hex dump into a byte array.
Next, we write the first assertion:
assertEquals(execute(reloader), "I'm the original!");
Where the execute
method is given by:
private String execute(ClassReloader reloader) throws Exception { Class<?> clazz; clazz = reloader.loadClass("reloader.Subject"); Constructor<?> constructor; constructor = clazz.getConstructor(); Object instance; instance = constructor.newInstance(); return instance.toString();}
Using our reloader, it tries to load our Subject
class by giving its binary name.
Assuming the load is successful, it then:
-
creates a new instance of the class; and
-
return the object's
toString
value.
Next, the test simulates the recompilation of our class:
Files.write(classFile, hexFormat.parseHex(UPDATED));assertEquals(execute(reloader), "Reloading successful!");
It tries to reload the class definition and verify if the new instance returns the expected toString
value.
Implementation
And here's an implementation that makes our test pass:
public class ClassReloader { private final Path classOutput; public ClassReloader(Path classOutput) { this.classOutput = classOutput; } public final Class<?> loadClass(String binaryName) throws ClassNotFoundException { ThisLoader loader; loader = new ThisLoader(); return loader.loadClass(binaryName); } private class ThisLoader extends ClassLoader { public ThisLoader() { super(null); // no parent } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { try { String fileName; fileName = name.replace('.', File.separatorChar); fileName += ".class"; Path path; path = classOutput.resolve(fileName); byte[] bytes; bytes = Files.readAllBytes(path); return defineClass(name, bytes, 0, bytes.length); } catch (NoSuchFileException e) { ClassLoader systemLoader; systemLoader = ClassLoader.getSystemClassLoader(); return systemLoader.loadClass(name); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } }}
In the loadClass
method we create a new instance of the internal ClassLoader
subclass.
This subclass overrides the findClass
method:
-
it tries to read the class definition from the
classOutput
directory; -
if it fails because the file does not exist, it delegates to the system class loader; and
-
if it fails because of another I/O error, it rethrows the exception as a
ClassNotFoundException
.
As mentioned, this implementation makes our test pass.
Conclusion
Our ClassLoader
implementation is capable of reloading a single Java class definition at runtime.
It eagerly loads the class definition on each invocation which is not ideal. So it is probably not the best candidate to be used during the development of a Java web application.
But I hope it shows that reloading a single class definition at runtime can be done with a few lines of Java code.
Granted, it leaves a number of open questions:
-
why does the
ClassReloader::loadClass
method creates a new instance of the internal class loader each time it is invoked? -
what happens if our
Subject
class references other classes? -
what if the classes referenced were already loaded by another class loader?
-
why does the
ThisLoader
constructor sets the parent class loader tonull
? -
what about Java modules?
These questions were intentionally left open as I wanted this to be a short blog post.
I might revisit them in a future post.
You can find the source code of the examples in this GitHub repository.