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:
.yaml
or .properties
files, for
example). This plugin provides an easy way to register listeners
for files modifications.
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 :
HotswapAgent
!).
AppRoutes
class, which is responsible (in this example) for defining
the application's routes.
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!
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 :
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!
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.
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!
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!
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()) // ...
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).
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.