Reloading a Java class definition at runtime

Marcio EndoMarcio EndoFeb 18, 2024

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:

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 to null?

  • 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.