« Back

Liferay 7 + JRebel

General Blogs August 18, 2017 By Andrew Jardine

I recently responded to the thread in the forums where the poster was asking about Liferay 7 and it's compatibility with OSGI. I am a huge JRebel fan myself and, based on what I know of JRebel and how it works, couldn't think of a reason why it wouldn't work -- but, I hadn't yet tried it out. I started doing some research as well as having a chat with the infamous David Nebinger about JRebel, OSGI, whether or not it should in theory work etc. Liferay 7's achitecture is -- let's say -- is "a bit of a shift" from the past versions, so I wasn't sure. So my question was, with the new model and all this OSGI stuff, "Does this mean I can't or don't need JRebel anymore?". The short answer is no, youl use JRebel, and, for me, I think there is still heaps of value in the tool.

In this post I'm going to share what I found and what I did in hopes that it will shortcut this task for others. First, for any JRebel newcomers, I'm going to do a quick "What is JRebel". If you already know about JRebel, you can skip that section. Next we'll setup our agent configuration and logging levels stuff so that you can use to reveal some additional logging and help you figure out whether or not things are working. Last we'll wrap it up by showing how to configure/generate the rebel.xml file -- specifically how to manage this for the new Gradle project structure.

 

What the heck is a JRebel?


I remember working with a guy who would spend hours writing thousands of lines of code before he compiled it. One part of his reasoning for this was because he hated waiting for deployments. The other part was that he like to think of it as a sort of game of compiler roulette, taking his chances and hoping that everything worked. Me? I tend to be the bits and pieces type developer -- I know what I want to achieve but I take it one bite at a time. My approach means A LOT of updated deploys -- but this, for those of you who are not familiar, is where JRebel comes into the game. It might not be for everyone, but if you are a bits and pieces person like myself, this might be for you.

I started using JRebel back when I was working with Liferay 6.1 -- something that feels like an eternity ago now. For all of my 6.1 and 6.2 work, JRebel was one of me besties, saving me countless hours a day. JRebel is a product written by Zeroturnaround -- who incidentally has some of the best customer service I have ever received in my entire life. With JRebel, you can dynamically reload classes and resources at runtime which means that I can make a change in my IDE, compile it (just the class, not the entire plugin/project) and BAM! latest version of the class is live in the runtime -- no deployment necessary. "C'mon" you might say, "...deployments only take like 7 seconds to do". True, but when you do thousands of them over the course of a large engagement, those 7 seconds add up. JRebel manages to do this hot swap of the class by using a configuration file that tells it which "disk location" to monitor. The JRebel Agent does the rest -- swapping the old class for the new one when the change is detected. If you work the way I do, this is an incredible tool that allows me to build a portlet using a fraction of the number of deploys I otherwise would have. It also keeps track of deploys versus reloads and tries to give you some metrics on how much time you have saved by figuring out how many deploys you DIDN'T have to make. The licensing cost works out to be something like $1.50 a day -- I don't know how much your time is worth? but it's definitely worth it for me. I also justify the cost as considering that it makes me a more efficient developer, and since I am usually paid by the hour, it allows me to provide some added value (in terms of savings) for my clients.

So that's my pitch on JRebel. Now, let's have a look at how we can use it with Liferay 7. If you have used it before, it's not earth shattering details so don't get too excited.

 

Agent Configuration


Before we blast into this I want to take a couple of lines to just explain where my hesitation came from. All my JRebel + OSGI research referenced architectures where you as the user start the Felix container directly. Knowing that, in Liferay 7, you start Tomcat and then Liferay handles starting the OSGI bit for you at startup -- I wasn't sure how all the agent configuration settings would work. In the end though, there is no real difference because the agent is attached to the JVM process. Tomcat is what we start but the OSGI container end up running under the same process, so all the settings are still applicable. Great so let's get to it.

My preference is to manage my server outside the IDE process. I start it from the command line under it's own process. Likewise, with JRebel, my preference is to use the standalone agent configured to start with the server starts rather than trying to use the IDE plugins. As a result, I'm going to outline "My Way" -- you may need to make alterations if you don't use the same approach I do.

Start by going to your $TOMCAT_HOME/bin folder and open up the setenv.sh (or .bat for windows folks out there). In this file you want to add the following line -- updating the paths to match your local environment of course.

	# JREBEL agent configuration
	JAVA_OPTS="$JAVA_OPTS -javaagent:/home/aj/jrebel/jrebel.jar -Drebel.log.file=/home/aj/projects/liferay7/liferay-portal/tomcat/log/rebel{time}.log -Drebel.log=true -Xbootclasspath/p:/home/aj/projects/liferay7/liferay-portal/tomcat/temp/rebelboot.jar"

That's all you need -- provided you have installed JRebel already that is. There are a couple of things to note here though. First, I set the log location to be the same place Tomcat logs so that log cleanup that occurs every now and then can all be done in the one place. Second, the log is set to TRUE so which actually causes debug level logging to occur. There is a marginal (on my machine anyway) performance hit that comes with this, but using debug means that I see messages in the log confirming that locations are being monitored and that files are being reloaded. I like to work this way because it takes the "mystery" out of it for me. Sometimes a class won't get reloaded. If I have a statement output for everytime that it does then I know what the problem might be if my latest changes aren't reflected. Since I always tail my log while I am developing, this is a good approach for me. The last thing to mention here is the last parameter. If it is your first time running the agent, the startup with not complete because that process will generate that file. So for first timers, you need to do a double start.

With these settings in place, go ahead and fire up your server. Watch the log and validate that you see the JRebel output near the opening statements. If you don't see them, then you have something wrong in your configuration. Once your server has started up and you are sure the agent is running, move on to the last section.

 

JRebel Configuration


NOTE: There is an update below the conclusion that demonstrates how to you have the agent configuration added to all your modules auto-magically instead of having to do each one manually.
 
WARNING! When I started down this path I had a lot of problems and could only get PART of the JRebel process working. I was able to see the classes being reloaded but the actual class references weren't being updated until I restarted. In the end I managed to fix this by updating my version to the latest JRebel agent (as of this post) 7.0.13. Make sure you have this minimum version before you go on.

At first to get this thing going I just created my rebel.xml file manually. But then it occurred to me that there was probably a gradle plugin that I could use. Of course, there is and you can find all the details on how to use it (there are a lot of options) here: https://manuals.zeroturnaround.com/jrebel/standalone/gradle.html. Before you can use it though, you'll need to make sure that you have a repository where Gradle can find it. Open the settings.gradle for your Liferay Workspace and update the repositories block to include this maven repository --

	maven {
		url "https://plugins.gradle.org/m2/"
	}

If you want to just use the basic out of the box to get going, all you have to do is open your build.gradle file for your module and add the following to the TOP of the file. The location in the file is important because otherwise you'll anger the gradle gods. My sample project looked like this in the end --

	plugins {
	    id "org.zeroturnaround.gradle.jrebel" version "1.1.7"
	}

	jar.dependsOn(generateRebel)

	dependencies {
	    compileOnly group: "com.liferay.portal", name: "com.liferay.portal.kernel", version: "2.0.0"
	    compileOnly group: "com.liferay.portal", name: "com.liferay.util.taglib", version: "2.0.0"
	    compileOnly group: "javax.portlet", name: "portlet-api", version: "2.0"
	    compileOnly group: "javax.servlet", name: "javax.servlet-api", version: "3.0.1"
	    compileOnly group: "jstl", name: "jstl", version: "1.2"
	    compileOnly group: "org.osgi", name: "osgi.cmpn", version: "6.0.0"
	}

If you are using an IDE you should now see a task for "generateRebel" -- or alternatively you can go to your command line and use the gradle wrapper with the task name. Depending on how nested your project is it will look something like this --

	$>../../../gradlew generateRebel

If everything is configured correctly, you will see the usual BUILD SUCCESSFUL. But what happened? where is my rebel.xml? The gradle plugin works the same way the Maven one does, if you are familiar with it. It doesn't put the rebel.xml file into the main source tree, if just puts it into the compiled area. So if you go into your modules build/resource/main directory, you'll see it there. Open the file up and you should see something like this --

	<?xml version="1.0" encoding="UTF-8"?>

	<!--
	  This is the JRebel configuration file. It maps the running application to your IDE workspace, enabling JRebel reloading for this project.
	  Refer to https://manuals.zeroturnaround.com/jrebel/standalone/config.html for more information.
	-->
	<application generated-by="gradle" build-tool-version="3.0" plugin-version="1.1.7" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.zeroturnaround.com" xsi:schemaLocation="http://www.zeroturnaround.com http://update.zeroturnaround.com/jrebel/rebel-2_2.xsd">
	  <classpath>
	    <dir name="/home/aj/projects/liferay7/liferay-workspace/modules/apps/sample-web/build/resources/main">
	    </dir>
	    <dir name="/home/aj/projects/liferay7/liferay-workspace/modules/apps/sample-web/build/classes/main">
	    </dir>
	  </classpath>

	</application>

This is what we were talking about earlier, disk locations that we want the agent to monitor for changes so that we can hot swap them in without a deployment. Now that you have your rebel.xml file generated, go ahead and deploy your plugin. Based on the agent configuration above, you should see something similar to this in the log.

	13:45:22,369 INFO  [fileinstall-/home/aj/projects/liferay7/liferay-portal/osgi/modules][BundleStartStopLogger:38] STOPPED com.sample.web_1.0.0 [541]
	2017-08-18 13:45:22 JRebel: Directory '/home/aj/projects/liferay7/liferay-workspace/modules/apps/sample-web/build/resources/main' will be monitored for changes.
	2017-08-18 13:45:22 JRebel: Directory '/home/aj/projects/liferay7/liferay-workspace/modules/apps/sample-web/build/classes/main' will be monitored for changes.
	13:45:23,123 INFO  [Refresh Thread: Equinox Container: 1029feb8-1a84-0017-1c1b-d63bd3f6b422][BundleStartStopLogger:35] STARTED com.sample.web_1.0.0 [541]

.. and just like that you are good to go with this plugin module, making changes and not having to deploy it. You'll also notice (if you didn't already in the rebel.xml) that there is a reference to the resources folder as well which, yes, means you should be able to update properties files, spring configs etc. But let's test it out.

My test is simple. I have, as part of the web module a class called SamplePortlet which extends the MVCPortlet. in the render method for this portlet I added a line of code -- something obvious, I used a logging statement.

public class SamplePortlet extends MVCPortlet
{
    private static final Log _log = LogFactoryUtil.getLog(SamplePortlet.class);

		@Override
    public void render(RenderRequest renderRequest, RenderResponse renderResponse) throws IOException, PortletException
    {
        _log.info("JRebel reloaded my class!");

        super.render(renderRequest, renderResponse);
    }
}

Save the file and make sure to compile it to be sure that a new .class file is generated in the build folder. I use intellij so for me I have the recompile hot key combination as CTRL-SHIFT-S. Once the class is recompiled, I see this show up in the log.

2017-08-18 13:46:09 JRebel: Reloading class 'com.sample.portlet.SamplePortlet'.

... reloading the page with my portlet shows me the new log message I just added -- no deployment necessary.

 

Conclusion


I guess the original question -- Can I use JRebel with Liferay 7 can be answered as "yes". Now in all fairness this is a pretty rudimentary example. I have also tried it (successfully) with a more complicated scenario where I have a shared service that is used in several modules and have seen the update to the service reflected across all the modules accordingly. While two tests can't and probably shouldn't be considered enough to wipe your hands, it's certainly a promising start.

On the whole, if you are a JRebel user and want to continue to use it with Liferay 7 to help minimize your deploys and accelerate your dev cycles, looks like you're good to go.

 

Update

Adding that block to each of your modules is a bit of a pain in the butt. If you want to have the code applied to all your modules without having to edit them individually, then you can use this little trick (and grow a little love for Gradle at the same time).

In your main Liferay workspace, beside the settings.properties there is a file called build.gradle. Chances are this file is empty. We can use this file to add the following code.

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.zeroturnaround:gradle-jrebel-plugin:1.1.7"
    }
}
allprojects { project ->
    plugins.withId('java') {
        project.apply plugin: 'org.zeroturnaround.gradle.jrebel'
        def jarTask = project.tasks.findByName('jar')
        if (jarTask) {
            jarTask.dependsOn(generateRebel)
        }
        def warTask = project.tasks.findByName('war')
        if (warTask) {
            warTask.dependsOn(generateRebel)
        }
    }
}

 

The one thing that I like about gradle is that it reads nicely. All we're doing here is saying "for any project we have, make sure that the generateRebel task is available, as long as the project type is either a war or a jar." Once you have this in place you can go ahead and remove the other snippet from your individual module build.gradle files. That's it -- now you don't have to try to remember to add the snippet to each module!

Threaded Replies Author Date
Hi Andrew, Thanks for the article. I can't... Frédéric Jeanneau September 12, 2017 3:59 PM
Hey Frederic, I didn't do the JSP stuff in my... Andrew Jardine September 14, 2017 6:14 AM
Hi Andrew, I contacted the JRebel support and... Frédéric Jeanneau October 6, 2017 11:50 AM

Hi Andrew,

Thanks for the article.

I can't seem to be able to reload jsp files on my side, I was able to with 6.2.. Were you successful on your side?

Example:

<web>
<link target="/">
<dir name="C:\myWorkspace\myWar\src\main\webapp"/>
</link>
</web>

Thanks,

Frederic
Posted on 9/12/17 3:59 PM.
Hey Frederic,

I didn't do the JSP stuff in my example, but I did try it quick the following day and noticed the same thing. I have it on my list to try a couple of things to see if I can get that bit working as well and when I have it in place, I'll update the article with a Part Deux. Stay tuned.
Posted on 9/14/17 6:14 AM in reply to Frédéric Jeanneau.
Hi Andrew,

I contacted the JRebel support and they fixed an issue for me in the latest nightly build.. I was not able to make the remoting feature work. It works now both for classes and resources.

Regards,

Frederic
Posted on 10/6/17 11:50 AM in reply to Andrew Jardine.