Overview

This plugin helps you configure your application for Hot Swapping (also called "Hot Reloading") and let you easily register listeners so you are able to run custom code when modifications are made on classes or on regular files.

Hot Swapping is really useful during development: you change a class and the modifications are immediately available, without having to restart the application or the server. This leads to a good development experience!

Java doesn't provide hot swapping out-of-the-box, and this is why third-party solutions have emerged. The most known of those solutions is JRebel which is a very nice product but that is way, way too expensive. From the other available solutions, the more mature is HotswapAgent + DCEVM, which is free and works very well too! It is the solution used by this plugin...

In short, here's what this plugin has to offer:

  • Help with installing DCEVM and HotswapAgent
    Those two are both required for class redefinitions and associated listeners to work.
  • Classes modifications listening mechanism
    In addition to classes being automatically reloaded by HotswapAgent, you can register some listeners to be called when classes are modified. This allows you to run custom code to react to modifications (for example by clearing some cache, reloading routes, etc.).
  • Files modifications listening mechanism
    HotswapAgent is about classes redefinition, but sometimes you also need to react to modifications made to regular files ( .yaml or .properties files, for example). This plugin provides an easy way to register listeners for files modifications.

Usage

Classes redefinitions

HotswapAgent, when properly installed, will make most of the changes made to your code available immediately, without having to restart the application! In addition to this, this current plugin allows you to register listeners, that will be called when classes are redefined by HotswapAgent.

Note that if the application is not started with the HotswapAgent agent, all features related to classes redefinitions and the associated listeners will be automatically disabled. This is the behavior you want for an application running on any environment other than locally. Classes redefinitions is indeed something in general only done during development.

Registering a class redefinition listener

You can register listeners to be called when some specific classes are redefined by HotswapAgent. To do so, you simply bind them in your application Guice module using the associated HotSwapClassesRedefinitionsListener multibinder:

Multibinder<HotSwapClassesRedefinitionsListener> multibinder =
        Multibinder.newSetBinder(binder(), HotSwapClassesRedefinitionsListener.class);
        
multibinder.addBinding().to(AppMainClassClassesRedefinitionsListener.class)
                        .in(Scopes.SINGLETON);

A classes redefinitions listener (such as "AppMainClassClassesRedefinitionsListener" in the above code) is a class that implements the HotSwapClassesRedefinitionsListener interface. There are three methods to provide when you create such listeners:

  • Set<Class<?>> getClassesToWatch()

    This is where you specify for which classes you want to be called, when modifications are made. A listener can be interested in a single class or in multiple classes.

  • void classRedefined(Class<?> redefinedClass)

    The method that is called when a class is modified. This is your hook! In this method, you can reload/call some code to make use of the newly redefined class.

  • boolean isEnabled()

    The listener will only be registered if this method returns true.

    Note that all classes redefinitions listeners are automatically disabled if the application is started without HotswapAgent! But this method allows you fine grain control, during development.

Classes redefinitions listener example

Here's an example of a classes redefinitions listener. It listens to changes to an "AppRoutes" class which may be the place where the routes of an application are defined. When this class changes, the listener makes sure the routes cache is cleared and routes are reloaded!

public class AppMainClassClassesRedefinitionsListener implements HotSwapClassesRedefinitionsListener {

    private final AppRouter appRouter;

    @Inject
    public AppMainClassClassRedefinitionListener(AppRouter appRouter) {
        this.appRouter = appRouter;
    }

    protected AppRouter getAppRouter() {
        return this.appRouter;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public Set<Class<?>> getClassesToWatch() {
        return Sets.newHashSet(AppRoutes.class);
    }

    @Override
    public void classRedefined(Class<?> redefinedClass) {
        getAppRouter().clearCacheAndReloadRoutes();
    }
}

Explanation :

  • 1 : The listener implements HotSwapClassesRedefinitionsListener.
  • 14-17 : The listener is always enabled (as long as the application was started with HotswapAgent!).
  • 19-22 : We are only interested in modifications made to the AppRoutes class, which is responsible (in this example) for defining the application's routes.
  • 24-27 : When the AppRoutes class is modified, the classRedefined() method is called. In this method, we tell our router to reload the cached routes. Note that we only watch one class ("AppRoutes") in this example, so there is no need to use the "redefinedClass" parameter to validate if the current modification has been made on that class or on another!

Remember that you may register as many listeners as you want. Each can listen to a different class or they may share some. If one of your listeners is interested in more than one class, you then may have to use the "redefinedClass" parameter, provided when the classRedefined() method is called, to validate which class was actually modified!

Files modifications

In addition to classes redefinitions listeners, this plugin also provides an easy way of listening to modifications made on regular files.

For example, during development, you may change a app-config.yaml file to tweak some configurations of your application. By registering a listener for this file, you would be able to refresh the AppConfigs object uses to access the configurations in your Java code.

Registering a file modifications listener

To register a new files modifications listener, you simply bind it in your application Guice module using the associated HotSwapFilesModificationsListener multibinder:

Multibinder<HotSwapFilesModificationsListener> multibinder =
        Multibinder.newSetBinder(binder(), HotSwapFilesModificationsListener.class);
        
multibinder.addBinding().to(AppConfigFileModificationsListener.class)
                        .in(Scopes.SINGLETON);

A files modifications listener (such as "AppConfigFileModificationsListener" in the above example), is a class that implements HotSwapFilesModificationsListener. There are three methods to provide when you create such listener:

  • Set<FileToWatch> getFilesToWatch()

    This is where you specify which files you want to watch. A listener can be interested in a single file or in multiple files. The files can be on the file system or on the classpath. We'll have a look at FileToWatch and how to define the watched files soon.

  • void fileModified(File modifiedFile)

    The method is called when a file is modified. This is your hook! For example, you can programmatically delete some cache in your application.

  • boolean isEnabled()

    The listener will only be registered if this method returns true. Most of the time, you only want to listen to files modifications during development. A common pattern is therefore to make use of isDevelopmentMode() :

    @Override
    public boolean isEnabled() {
        return getSpincastConfig().isDevelopmentMode();
    }

Files modifications listener example

Here's an example of a files modifications listener. It listens on changes to the "app-configs.yaml" configurations file. When this file changes, it then clears the configurations cache so the new values are used:

public class AppConfigFileModificationsListener implements HotSwapFilesModificationsListener {

    private final AppConfigs appConfigs;

    @Inject
    public AppConfigFileModificationListener(AppConfigs appConfigs) {
        this.appConfigs = appConfigs;
    }

    protected AppConfigs getAppConfigs() {
        return this.appConfigs;
    }

    @Override
    public boolean isEnabled() {
        return getAppConfigs().isDevelopmentMode();
    }

    @Override
    public Set<FileToWatch> getFilesToWatch() {
        return Sets.newHashSet(FileToWatch.ofClasspath("app-config.yaml"));
    }

    @Override
    public void fileModified(File modifiedFile) {
        getAppConfigs().clearConfigCache();
    }
}

Explanation :

  • 1 : The listener implements HotSwapFilesModificationsListener.
  • 14-17 : The listener will be enabled only during development!
  • 19-22 : We are interested by changes made to the "app-config.yaml" file, which is located at the root of the classpath. In the next section, we'll learn how to use FileToWatch to define the files to watch!
  • 24-27 : When the "app-config.yaml" file is modified, we tell AppConfigs to clear its cache. Note that, in this example, we only listen to one file so there is no need to use the "modifiedFile" parameter to validate what file was modified.

You may have as many listeners as you want. Each one can listen to different files or share some of them. If one of your listeners is interested in more than one file, you may have to use the "modifiedFile" parameter provided when the fileModified() method is called, to validate which one exactly has been modified.

Using FileToWatch

The getFilesToWatch() method has to return a set of FileToWatch. A FileToWatch is able to specify if the files to watch are on the file system or on the classpath, and it also allows you to target files using a regular expression.

Let's see the three methods you can use to create an instance of FileToWatch:

  • FileToWatch ofFileSystem(String fileAbsolutePath)

    Creates a FileToWatch from the absolute path of a file on the file system. For example:

    
    FileToWatch file = FileToWatch.ofFileSystem("/home/stromgol/dev/some-file-to-watch.json");
    

  • FileToWatch ofClasspath(String classpathFilePath)

    Creates a FileToWatch from a classpath path. The path can start with a "/" or not, it doesn't make any difference.

    
    FileToWatch file = FileToWatch.ofClasspath("app-configs.yaml");
    

  • FileToWatch ofRegEx(String dirPath, String fileNameRegEx, boolean isClassPath)

    This factory is the most powerful of the three! It allows you to specify a regular expression to use for the names of the files to watch. Those files can be on the file system or on the classpath.

    Note that using a regular expression is available for the names of the files, but you still have to specify the directory where those files are located.

    
    FileToWatch fileToWatch = FileToWatch.ofRegEx("/home/stromgol/news/", 
                                                  "news-[0-9]+\\.yaml", 
                                                  false);
    

    As you can see in this example, a FileToWatch may represent more than one file! Here, any files matching the provided regular expression (such as "news-12.yaml", "news-687.yaml", etc.) will be watched. In this situation, you may have to check what file actually changed by using the modifiedFile parameter provided when the fileModified() hook is called!

Installation

Installing the plugin itself is very simple, but you also need to have DCEVM and HotswapAgent for it to work. The time you spend installing those will be well rewarded, as you'll then have in place the best free Java Hot Swapping solution out there!

The plugin itself

1. Add this Maven artifact to your project:

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-plugins-hotswap</artifactId>
    <version>2.2.0</version>
</dependency>

2. Add an instance of the SpincastHotSwapPlugin plugin to your Spincast Bootstrapper:


Spincast.configure()
        .plugin(new SpincastHotSwapPlugin())
        // ...

DCEVM

The suggested way to use DCEVM with Java 17 is to download a JetBrains JDK release. These already include DCEVM. Download a recent JDK 17 from that page (search for "jbr17"). At the time of writing this document, the "Release 17_0_2-b315.1" version was used.

In other words, if you want to use the HotSwap plugin, you currently need to use such JDK from Jetbrain during development (of course any JDK 17 will be fine when your application is deployed and hot swapping is not required anymore).

Hotswap Agent

1. Download the latest Hotswap Agent version .jar file. At the time of writing this document, the latest release is hotswap-agent-1.4.2-SNAPSHOT.jar and is the one to use for Java 17. Hopefully, by the time you read this, a stable version compatible with Java 17 will have been released.

2. Rename the .jar file you downloaded to hotswap-agent.jar and move it to lib/hotswap/hotswap-agent.jar inside the Jetbrain JDK you unzipped/installed. For example, if you downloaded and installed jbrsdk-17_0_2-windows-x64-b315.1 from Jetbrains, you have to copy the .jar file to C:\jbrsdk-17_0_2-windows-x64-b315.1\lib\hotswap\hotswap-agent.jar (Windows example). Create the "hotswap" subfolder if required.

2. When you start your application inside an IDE, during development, add those VM arguments: "-XX:+AllowEnhancedClassRedefinition -XX:HotswapAgent=fatjar".

Here's an example in Eclipse:

When you start your application with this in place, any modification to a class will make this class being automatically redefined. And if you have registered some listeners, they will be called.

Make sure you read the HotswapAgent documentation if you have issues.