Reloading a Java class definition at runtime

Marcio Endo
February 18, 2024

When developing the UI for a web application in Java, the following workflow might be common:

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:

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:

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:

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.