JRebel Plugins
1. Why and when
What JRebel does is the reloading of classes. When you change something in your code, your changes get picked up automatically and are instantly reflected in your application's behavior. But sometimes, merely reloading the classes falls short of what really needs to be done to fully update the application – without restarting it.
That's because all the objects in the memory remain the same – they are not recreated. For most cases, this is a desired thing (avoiding the system re-initialization penalty is what JRebel is all about!). But when you alter something in your code that has to do with the way the application initializes (and that would results in memory objects being in a different state after a normal application restart), those changes will of course not be reflected in your application state, as the initialization code is not re-run.
This is exactly when creating a custom JRebel plugin could help you. With a plugin, you can monitor the reloading of resources, and execute any desired custom code in reaction. Basically you'll use a JRebel integration plugin to say: "After this certain kind of a reloading event occurs, go through these-and-these initialization steps to update my application's state."
A typical scenario would be the following: your application is using a neat third-party framework. When the application
starts up, the framework initializes by loading its configuration from various resources (external XML files,
Java annotations, etc). Some of these resources change as you continue developing the application (e.g. you add a new
bean in your spring applicationContext.xml and wire it with others).
Without an integration plugin explicitly telling what to do, these changes won't reflect in your application's
running state without a restart. But using an integration plugin, you could for example say: "Hey, if you see that
I've changed something in applicationContext.xml, please be kind and perform a full reloading of all
my beans!".
As that kind of scenarios are common for all applications using some given framework, the plugins should usually be created per framework, not per application!
2. Implementing a plugin
A JRebel plugin is nothing more than a small .jar-file containing at least 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 from the provided
template.
The key API in plugin development is the JRebel SDK which provides means to communicate with the JRebel private APIs. Your plugin code can also make use of whatever else available on the JVM you are running in. For example, if you are developing a plugin for Struts2 framework (that will get activated only if Struts2 really is available in the JVM), you can safely import and use classes from Struts2 in your plugin (without providing them yourself).
The Plugin interface you need to implement looks like that:
public interface Plugin { void preinit(); boolean checkDependencies(ClassLoader cl, ClassResourceSource crs); String getId(); String getName(); String getDescription(); String getAuthor(); String getWebsite(); }
The most important methods here are:
-
preinit()– The main method. Here, set up the CBPs, listeners and everything else. -
checkDependencies(ClassLoader cl, ClassResourceSource crs)– Checks if this plugin should be loaded for this particular classloader. The safest way to do it is like it's been done in the example plugin (by just checking whether a framework class that you rely on is available in that classloader):
public boolean checkDependencies(ClassLoader classLoader, ClassResourceSource classResourceSource) { return classResourceSource.getClassResource( "org.zeroturnaround.javarebel.sdkDemo.AbstractCanvas") != null; }
Refer to JRebel SDK API reference for additional details.
2.1. The demo application
To demonstrate working with JRebel plugins, we have created a very simple Swing desktop application (get the source).
It just draws three Image objects on a Canvas object, which is in turn placed inside
a JFrame object and displayed. It looks like that:
.. and its class hierarchy looks like this:
As discussed above, a typical situation for writing a JRebel plugin is when you want to provide JRebel integration for a framework
maintained by a third party. Let's imagine that the class AbstractCanvas is our third-party "framework" that we want
to provide better JRebel integration for. Therefore, we have a restriction that we can't directly edit its code (if we had full
control over the code in AbstractCanvas, we could just add our modifications directly into it and problem solved).
Let's further imagine that FlagsCanvas is our "application" that we are developing. Our goal is to build a plugin that
provides better JRebel support for hotloading any client code of the AbstractCanvas "framework" (i.e. while modifying
the FlagsCanvas class, in our case).
Start off by download the demo app and making yourself familiar with it. To run it with JRebel, you need to edit the file
build.properties and make jrebel.jar.path point to jrebel.jar on your local machine. There
are three Ant targets that execute the application:
-
ant run– runs the app without JRebel -
ant run-with-jrebel– runs the app with JRebel, but without the JRebel plugin -
ant run-with-jrebel-and-plugin– runs the app with JRebel, and with our ready-made JRebel integration plugin fromlib/jr-plugin-template.jar
The code that controls where the flags get painted to is in these two methods of FlagsCanvas.java:
private void init() { estoniaX = 65; estoniaY = 50; switzerlandX = 400; switzerlandY = 50; } public void paint(Graphics g) { System.out.println(" Painting the canvas .."); g.drawImage(switzerlandImg, switzerlandX, switzerlandY, this); g.drawImage(estoniaImg, estoniaX, estoniaY, this); g.drawImage(iranImg, 650, 50, this); }
Execute the app with JRebel but without the plugin. The paint() method gets called by Swing every
now-and-then when some event happen with the frame (it gains or looses focus, etc). Try modifying the coordinates of the
flags and see what happens (of course, something needs to re-build your .class files after editing them..
it's best to configure your IDE to do that automatically whenever you save your source files). When you re-focus
to the demo-application, paint() gets called and the Iranian flag should indeed move if you changed its
coordinates in the Java code. The other flags demonstrate now reaction whatsoever. That's because their locations are controlled
by the instance variables of the FlagsCanvas class that get initialized by the init() method when
a new instance of the class is created. So, although you might have changed their values in the init() method,
the values in the memory remain the same as nobody is calling that method after the instance is already created.
2.2. Class bytecode processors (CBPs)
So the class reloading works, but this doesn't do the whole job here as the in-memory state of the application doesn't get
updated. Notice that all that we would need to do to fix that is make an additional call to init() whenever
we call paint(). The JRebel SDK fortunately provides suitable tools for the job. Whenever you need to modify the
behavior of third-party code before it runs in your JVM, you'll use the so-called class bytecode processors (CBPs),
extending the JavassistClassBytecodeProcessor class. When set up properly, the CBPs intercept the initial class
loading in the JVM and load the third-party classes with our modifications already in place. Let's see what a CBP looks like:
public class ReloadAbstractCanvasStateCBP extends JavassistClassBytecodeProcessor { public void process(ClassPool cp, ClassLoader cl, CtClass ctClass) throws Exception { LoggerFactory.getInstance().echo("Patching the AbstactCanvas class .."); try { CtMethod paintMethod = ctClass.getDeclaredMethod("repaint"); paintMethod.insertBefore( "org.zeroturnaround.javarebel.integration.pluginTemplate.DemoAppConfigReloader.reinitialize($0);" ); } catch (NotFoundException e) { LoggerFactory.getInstance().error(e); } } }
What this CBP does is it inserts an extra statement into the top of the repaint() method
of the AbstractCanvas class, transforming the control flow to a public static method implemented in our plugin. The current
instance is passed as an argument ($0 stands for this in Javassist code-strings).
The method reinitialize() implemented in our plugin can now make arbitrary customizations and then hand the control back to
AbstractCanvas#paint(). Currently, it all it does is just calling the init() method with Reflection API.
Notice that here it would actually made more sense to call the init() method right away:
paintMethod.insertBefore("init()"); |
That approach might be reasonable when the customization you need to do is trivial. Anyhow, as things get any more complicated, forwarding the call might be more comfortable than maintaining larger chunks of code inside Java strings.
The CBP also has to be registered in the plugin class for things to work:
public class PluginTemplate implements Plugin { public void preinit() { // Register the CBP Integration i = IntegrationFactory.getInstance(); ClassLoader cl = PluginTemplate.class.getClassLoader(); i.addIntegrationProcessor( cl, "org.zeroturnaround.javarebel.sdkDemo.AbstractCanvas", new ReloadAbstractCanvasStateCBP() ); // .. } // .. }
Integration is the class through which CBPs can be set up; instance of it is acquired through IntegrationFactory ..
sounds simple enough.
A word of warning: be very careful with referencing the classes you are about to patch in the CBP code itself (or other code that is called by the CBP). Remember: the CBP is all about intercepting a class before it gets loaded. If your CBP now tries to do something with the very same class, JVM would have to load it to do it. This would create a cyclic dependency, blowing the thing!
2.3. Class reload listeners
If you are prepared to spend some time, I recommend you take the demo application and the plugin template, reduce the plugin to
what we have done so far (basically just cutting the rest of the preinit() method), re-build the plugin jar and
replace the old plugin with the new one in demo application's ./lib directory. If you are not, you'll just have to
believe me for now. Our AbstractCanvas-to-JRebel integration still has a small flaw. Namely, if you change the coordinates of
flags and recompile the code, repainting of the canvas happens when Swing wants it to be repainted, not when you
actually changed anything. (Try splitting your desktop between Eclipse and the application frame and editing the code in Eclipse
while you application is inactive. Whatever you do with the code, Swing won't repaint until you focus back to the demo app.)
It is obviously not a very big problem, as Swing wants to repaint things almost always when you actually do anything with the app,
but let's still try to fix that – at least for the educational purpose.
This is an ideal moment for introducing reload listeners. The JRebel SDK provides an API to register listeners that get notified
when JRebel reloads anything. By creating an implementor of the ClassEventListener interface and registering it through
Reloader, we can implement custom reactions to class reload events. This is exactly what we'll do:
public void preinit() { // .. // Set up the reload listener ReloaderFactory.getInstance().addClassReloadListener( new ClassEventListener() { public void onClassEvent(int eventType, Class klass) { try { String cn = "org.zeroturnaround.javarebel.sdkDemo.AbstractCanvas"; Class abstractCanvasClass = Class.forName(cn); // Check if it is child of AbstractCanvas if (abstractCanvasClass.isAssignableFrom(klass)) { DemoAppConfigReloader.repaint(); LoggerFactory.getInstance().echo("Repainted the canvas"); } } catch (Exception e) { LoggerFactory.getInstance().error(e); } } // .. } ); }
What this chunk of code does is actually simple: if we see that a class has been reloaded, and it happens to be a subclass of
AbstractCanvas, we force the repainting of the canvas (without waiting for Swing to do it at some later time).
The repainting itself is implemented in method DemoAppConfigReloader.repaint().
Now our AbsractCanvas JRebel integration is starting to look much better. Our plugin added the following functionality:
- If any subclass of
AbstractCanvasgets reloaded, we'll repaint the canvas. - At any time we repaint, we'll also call the
init()method to update the application state.
We demonstrated some cornerstones of the JRebel SDK API:
- class bytecode processors (CBPs)
- class reload listeners
Some basic things can already be done with this knowledge, and you are of course encouraged to browse the rest of the API to make yourself familiar with the rest of its possibilites – to create better JRebel integration plugins!
3. Enabling a plugin
While playing around with the demo application, we used the ready-made Ant targets to enable the plugin. Anyhow, it would be nice to know how to enable your plugins yourself.
All that the Ant target run-with-jrebel-and-plugin actually does is it passes some additional JVM arguments to java through
the command line. The full command to do the same thing directly from command line would be:
Everything else is as always, except for that two new parameters have appeared:
-
-Drebel.plugins=/path/to/plugin1,/path/to/plugin2– a comma-separated list of paths to plugin jarfiless that you want JRebel to load -
-Drebel.name-of-your-plugin=true– turns the specified plugin on. The name has to match with whatever is returned by thegetId()method of your main plugin class.
4. Examples
4.1 Liferay plugin
The JRebel plugin for Liferay is open sourced and can be used as an example.
The plugin includes monitoring resource files and re-running parts of the frameworks 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


