Java Class Reloading

The-Thirteenth-Floor

lua-resty-ffi provides an efficient and generic API to do hybrid programming in openresty with mainstream languages (Go, Python, Java, Rust, Nodejs, etc.).

https://github.com/kingluo/lua-resty-ffi

I already implement code hot-reload for Python and Nodejs in lua-resty-ffi.

The Java class reloading is the last piece of the puzzle.

No out-of-box API to do class reloading, so it’s more tricky.

What is classloader?

All classes in a Java application are loaded using some subclass of java.lang.ClassLoader.

Loading classes are on-demand and lazy, i.e. when the java program starts, it doesn’t load all classes constructing this program.

When a class is loaded, all classes it references are loaded too. This is done by the same classloader for the entry class. This class loading pattern happens recursively, until all classes needed are loaded. This may not be all classes in the application. Unreferenced classes are not loaded until the time they are referenced.

The instances of the same class loaded by different class loaders is in different type.

Classloader chain

Class loaders in Java are organized into a hierarchy.

The default class loader chain of openjdk-11:

org.codehaus.mojo.exec.URLClassLoaderBuilder$ExecJavaClassLoader@5aae8eb5
jdk.internal.loader.ClassLoaders$AppClassLoader@659e0bfd
jdk.internal.loader.ClassLoaders$PlatformClassLoader@25b342cd

Note that the bootstrap classloader (parent of PlatformClassLoader) is null, so it’s not shown in the chain.

Parent delegation

As known, the process of class loading applies the parent delegation model.

When the class is not found, the class loader would try to load class from its parent class loader, if not ok, it turns to do it itself, and so on.

parent-delegation

Pseudo code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Class loadClass(String name) {
    // if loaded
    Class cls = findFromLoaded(name);
    if(cls == null) {
        // try parent first
        cls = this.parent.loadClass(name)
    }
    if(cls == null) {
        // if parent failes, try to do on its own
        cls = this.findClass(name);
    }
    return cls;
}

Class Reloading

By default, a loaded class is cached forever by JVM.

To reload a class, it has two conditions:

  • crate a new class loader to load class each time
  • the class loader breaks the parent delegation model

I implement such class loader for lua-resty-ffi, based on this blog.

Let’s see how to satisfy above two conditions.

Check the source code here.

new ClassLoader to load new class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// determine where to load classes
// here it uses the CLASSPATH env var
var cfgItems = cfg.split(",");
var clsName = cfgItems[0];
clsName = clsName.substring(0, clsName.length() - 1);
String classpath = System.getenv("CLASSPATH");
var paths = classpath.split(":");

// determine the package of init class
var tmp = clsName.split("\\.");
var prefix = String.join(".", Arrays.copyOfRange(tmp, 0, tmp.length-1));

// reload the init class
Class<?> cls = new DynamicClassLoader(paths, prefix).load(clsName);
var method = cls.getDeclaredMethod(cfgItems[1], String.class, long.class);
Object ret = method.invoke(null, cfgItems.length > 2 ? cfgItems[2] : "", tq);

load classes from the child first

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
    // if I don't care about this class, pass it to the parent completely,
    // otherwise, load it by myself first.
    // in practice, lua-resty-ffi only cares about the classes within
    // the same package of the init class, but other dependent classes,
    // e.g. gson, freemarker classes should be handled in the deafult JVM way,
    // so that they would be loaded once.
    if (!name.startsWith(prefix)) {
        return super.loadClass(name);
    }
...
    // load classes from class directory or JAR file.
    byte[] newClassData = loadNewClass(name);
...
}

With new class loader, the chain becomes:

DynamicClassLoader@55731ba7
jdk.internal.loader.ClassLoaders$AppClassLoader@659e0bfd
jdk.internal.loader.ClassLoaders$PlatformClassLoader@23a187ca

Why not Thread.contextClassLoader?

I thought by setting contextClassLoader of the polling thread, every new class from that thread would use that context class loader automatically.

But I am wrong. The context class loader should be used explicitly, otherwise, it’s useless.

The fact that the linked classes will be loaded by the same class loader perfectly meets my need, so that I do not need to change the code of each JAVA app, i.e. the class reloading is non-intrusive.

How to use?

Put a question mark as the class suffix when you calls ngx.load_ffi():

1
2
3
4
-- java
-- libffi_java.so
-- demo/http2/App.class
local demo = ngx.load_ffi("ffi_java", "demo.http2.App?,init,")

Please look at the doc in lua-resty-ffi for more details:

https://github.com/kingluo/lua-resty-ffi/blob/main/hot-reload.md