Custom JRebel plugins¶
The purpose of customized plugins¶
JRebel plugins are easiest to understand in the example of some well-known framework. For example, JRebel ships with a built-in JRebel Spring plugin. Spring can store its bean configuration in the XML files. These files are then used to initialize the application state during startup. Pure Java class reloading would not be enough to reload changes to this configuration, as the initialization code is only run once – when the framework starts – and is then stored in memory for the rest of the runtime. On the other hand, the Spring developer experience would not be complete if reloading the bean configuration changes was not supported. JRebel Spring plugin helps by remembering all Spring’s XML files on startup and listening for changes to those files at runtime. If any files are changed, the plugin triggers a reinitialization of Spring framework’s configuration, making sure that updates to bean configuration take effect.
Similar integration plugins exist for the vast majority of popular Java frameworks. These are shipped within the JRebel Agent. When your application is based on a public framework stack (like most Java applications), you should never need to write a JRebel plugin.
However, when using proprietary, home-grown or in-house Java frameworks, chances are that the built-in integration does not exist for some of your reloading scenarios. If that is the case, this guide will help you with writing a custom JRebel plugin to solve those issues.
Implementing a custom plugin¶
A JRebel plugin is a small JAR file, containing at a minimum a class that implements the Plugin interface of the JRebel SDK and a special manifest entry, telling JRebel the name of that class. It is highly recommended to start creating any plugin project using this template
.
The key API in plugin development is JRebel SDK. This provides the means to communicate with JRebel’s private APIs. Your plugin code can also make use of anything else available on the JVM you are currently running in. This means that if you are developing a custom JRebel plugin for your home-grown framework X, you can safely import and use your framework’s classes in your plugin (without providing them yourself).
The Plugin interface you need to implement looks like this:
public interface Plugin {
void preinit();
boolean checkDependencies(ClassLoader cl, ClassResourceSource crs);
String getId();
String getName();
String getDescription();
String getAuthor();
String getWebsite();
String getSupportedVersions();
String getTestedVersions();
}
The most important methods here are:
preinit()
– The main method. Use this to set up the bytecode processors (CBPs), event listeners and anything else.checkDependencies(ClassLoader cl, ClassResourceSource crs)
–- Checks whether this plugin should be loaded for this particular class loader. The safest way to do this is by checking whether a framework class that you rely on is available in that class loader:public boolean checkDependencies(ClassLoader cl, ClassResourceSource crs) { return crs.getClassResource("my.package.MyClass") != null; }
Refer to the example plugin contents for an example. Check out the JRebel SDK API for additional details.
Sample application with a sample plugin¶
This section will explain the JRebel SDK using a simple web application and a sample plugin that reloads the web application changes. You can download and open the code here
.
The web application monitors free system memory by polling the value of Runtime.getRuntime().freeMemory()
. The application has a configuration class that specifies the polling rate and the amount of samples to store. The data is published using a plain Java servlet. A GET request to the servlet will return the current timestamp and the average free memory based on the collected samples. Run the sample application with mvn -f demo-application/pom.xml jetty:run
and open http://localhost:8080/ in your browser to see it in action.
Sample response from localhost:8080/: [2017-09-04T10:23:24] average memory usage is 249083536
The sample plugin will reload two aspects of the application:
The format of the timestamp, specified in a properties file.
The polling rate of the memory usage monitor, specified in a configuration class.
Reloading a properties file¶
The format of the timestamp is specified in the file WEB-INF/MemoryUsageServlet.properties
. The properties file is loaded in the servlet’s init
method and called at server startup. When the file changes during the application runtime, the changes would not be reflected, because the servlet’s init method has already been called.
WEB-INF/MemoryUsageServlet.properties: time.format=yyyy-MM-dd'T'HH:mm:ss
A custom JRebel plugin can be used to check the properties file for changes and reinitialize the servlet when needed. The recommended way to do this is to use the RequestListener API from JRebel SDK. RequestListener allows the plugin to run arbitrary code right after a request arrives in the server (but before it’s dispatched to a servlet/Spring controller/JAX-RS endpoint).
The sample plugin implements the following steps:
Patches the servlet class to add a new method
reloadProperties
. The method is used to refresh the properties loaded from the file.Registers a RequestListener which checks the last modified timestamp of the properties file before each request. The
reloadProperties
method is called when the timestamp has changed.
Implementation¶
The servlet class loads the properties file in the init
method, called by the server on startup. The servlet has no logic for reloading the properties later. The plugin will add the necessary functionality using a Class Bytecode Processor (CBP).
A CBP is used to modify the class before it is loaded by the classloader at application runtime. A CBP can add or change methods in the class, add or change fields and make the class implement interfaces it did not implement originally. CBPs use the Javassist bytecode manipulation library, which is fairly popular and well documented.
Step 1 – declare a new interface in the plugin project:
public interface ReloadableServlet {
void reloadProperties() throws Exception;
ServletContext getServletContext();
}
Step 2 – create a new CBP for patching the servlet class:
public class MemoryUsageServletCBP extends JavassistClassBytecodeProcessor {
@Override
public void process(ClassPool cp, ClassLoader cl, CtClass ctClass) {
// some details omitted; see the sample code zip for details
ctClass.addMethod(CtMethod.make("" +
"public void reloadProperties() throws IOException {" +
" this.properties = loadProperties();" +
"}", ctClass));
ctClass.addInterface(cp.get(ReloadableServlet.class.getName()));
ctClass.getDeclaredMethod("init")
.insertAfter("ReloadHelper.addRequestListener(this);");
}
}
The CBP adds a new method reloadProperties
to the MemoryUsageServlet
class, makes the class implement the ReloadableServlet
interface and inserts the snippet ReloadHelper.addRequestListener(this);
at the end of the existing init
method of the MemoryUsageServlet
class. Note that the code in the strings is compiled by Javassist and injected into the original class when the class is being loaded.
Step 3 - register the CBP in the plugin’s preinit
method:
@Override
public void preinit() {
Integration integration = IntegrationFactory.getInstance();
ClassLoader cl = getClass().getClassLoader();
integration.addIntegrationProcessor(cl,
"demoapp.MemoryUsageServlet",
new MemoryUsageServletCBP());
}
Step 4 - add the ReloadHelper class to the plugin project:
public class ReloadHelper {
public static void addRequestListener(ReloadableServlet servlet) throws Exception {
RequestIntegrationFactory.getInstance()
.addRequestListener(new RequestListenerAdapter(0) {
private volatile long lastModified;
@Override
public void beforeRequest() throws Exception {
if (hasPropertiesFileChanged()) {
System.out.println("Reloading properties");
servlet.reloadProperties();
}
}
private boolean hasPropertiesFileChanged() throws Exception {
URL resource = servlet.getServletContext()
.getResource("/WEB-INF/MemoryUsageServlet.properties");
long lastModified = ResourceUtil.asResource(resource).lastModified();
if (lastModified > this.lastModified) {
this.lastModified = lastModified;
return true;
}
return false;
}
});
}
}
What will happen¶
The CBP is applied when the
MemoryUsageServlet
class is first loaded. The CBP adds the newreloadProperties
method and a call toaddRequestListener
at the end of the servlet’sinit
method.The server starts and calls the servlet’s
init
method. The RequestListener is registered.When the server receives a request, JRebel will call the
beforeRequest
method in the RequestListener. If the properties file has changed, then thereloadProperties
method is called from the RequestListener.
Monitoring class changes¶
The polling configuration of the web application is defined in a configuration class:
public class ApplicationConfiguration {
public static int getMaxSamples() {
return 500;
}
public static long getSampleDelayMillis() {
return 333;
}
}
The memory usage polling is started in the servlet’s init
method:
@Override
public void init() throws ServletException {
memoryUsageMonitor = new MemoryUsageMonitor(
ApplicationConfiguration.getMaxSamples(),
ApplicationConfiguration.getSampleDelayMillis());
}
When the constants in the ApplicationConfiguration
class later change, then the changes are not reflected in the running application – the MemoryUsageMonitor
object is already created and won’t be automatically updated.
A custom plugin can monitor a class for changes and call arbitrary code after the class changes have been loaded. Use the ClassEventListener API from JRebel SDK to implement this.
The sample plugin implements the following steps:
Patches the
MemoryUsageServlet
class to add a new methodreloadMonitor
. The method is used to reinitialize theMemoryUsageMonitor
object.Registers a
ClassEventListener
to track changes in theApplicationConfiguration
class. When the class changes, then thereloadMonitor
method is called.
Implementation¶
Step 1 – update the ReloadableServlet
interface and add the reloadMonitor
method:
public interface ReloadableServlet {
void reloadMonitor() throws Exception;
...
}
Step 2 – update the MemoryUsageServletCBP and add the reloadMonitor
method:
public class MemoryUsageServletCBP extends JavassistClassBytecodeProcessor {
@Override
public void process(ClassPool cp, ClassLoader cl, CtClass ctClass) {
// some details omitted; see the full code for details
ctClass.addMethod(CtMethod.make("" +
"public void reloadMonitor() {" +
" if (memoryUsageMonitor != null) {" +
" memoryUsageMonitor.close();" +
" }" +
" memoryUsageMonitor = new MemoryUsageMonitor(" +
" ApplicationConfiguration.getMaxSamples()," +
" ApplicationConfiguration.getSampleDelayMillis());" +
"}", ctClass));
ctClass.getDeclaredMethod("init").insertAfter("" +
"ReloadHelper.addClassChangeListener(this, ApplicationConfiguration.class);");
}
}
Step 3 – update the ReloadHelper
class and add the addClassChangeListener
method:
public class ReloadHelper {
public static void addClassChangeListener(ReloadableServlet servlet,
Class<?> configClass) {
ReloaderFactory.getInstance()
.addClassReloadListener(configClass, new ClassEventListenerAdapter(0) {
@Override
public void onClassEvent(int eventType, Class<?> klass) throws Exception {
System.out.println("Reloading monitor");
servlet.reloadMonitor();
}
});
}
}
What will happen¶
The CBP will add the
reloadMonitor
method to theMemoryUsageServlet
class when it is first loaded. A call to theaddClassReloadListener
method is appended to theinit
method.The server will start and call the
init
method of the servlet. This will register the ClassEventListener for theApplicationConfiguration
class.When the
ApplicationConfiguration
class is changed, JRebel will automatically reload the changes and call theonClassEvent
method of the ClassEventListener. TheonClassEvent
will reinitialize the memory usage monitor by calling thereloadMonitor
method.
Running the web app with the custom plugin¶
Compile the JRebel plugin like a regular maven project: mvn -f jrebel-plugin/pom.xml clean package
. This will produce a plugin jar in jrebel-plugin/target/jrebel-plugin.jar. To enable the plugin, pass the plugin’s path to JRebel using a JVM argument: -Drebel.plugins=/path/to/jrebel-plugin.jar
.
Running from the IDE¶
Compile the sample application and the sample plugin using mvn clean package
. Configure your favourite web server with the JVM argument -Drebel.plugins=/path/to/jrebel-plugin.jar
, start it with JRebel and deploy the sample application.
Running from the command line¶
Compile the sample application and the sample plugin using mvn clean package
. Attach the JRebel agent as usual, but also add the path to the sample plugin:
Linux and Mac OS:
export MAVEN_OPTS="<agentpath argument> -Drebel.plugins=/path/to/jrebel-plugin.jar"
mvn -f demo-application/pom.xml jetty:run
Windows:
set MAVEN_OPTS="<agentpath argument> -Drebel.plugins=/path/to/jrebel-plugin.jar"
mvn -f demo-application\pom.xml jetty:run
Check that the plugin works:
Change the timestamp format in the properties file. The “Reloading properties” message should appear in the console at the start of the next request and the timestamp format changes should be reflected in the servlet response.
Change the constants in the
ApplicationConfiguration
class and rebuild the demo application. JRebel should reload the class and the “Reloading monitor” message should appear in the console. The rate of “Sampled at <current time>” messages in the console should change.
Example: Liferay plugin¶
The JRebel plugin for Liferay is open sourced and can be used as an example. The plugin includes monitoring the resource files and re-running parts of the framework code when they change to update the metadata. This functionality can be found in org.zeroturnaround.jrebel.liferay.LiferayReloader
.
A good example of making your plugin work with different versions of a framework is contained in org.zeroturnaround.jrebel.liferay.cbp.PortletLocalServiceImplCBP
.