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 { |
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) {
|
Refer to JRebel SDK API reference for additional details.
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 from lib/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() {
|
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.
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 { |
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 { |
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!
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() {
|
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:
AbstractCanvas
gets reloaded, we'll repaint the canvas.init()
method to update the application state.