Liferay 5.2.3 services in Spring IoC+MVC powered portlets

A rather important subject today: how to make Spring IoC/MVC powered portlets work with Liferay services (the ones built from service.xml by calling ant with "build-service" target in your plugins SDK) in Liferay 5.2.3.

Situation:
- Spring IoC/MVC
We’re developing portlet with Spring IoC (and maybe Spring MVC) using Plugins SDK. We call create script -> get our sample portlet -> modify web.xml to have Spring WebApp context listener -> if we’re using Spring MVC, modify portlet.xml to use Spring Dispatcher Portlet.
Also in WEB-INF/lib we put spring.jar, and if we’re using Spring MVC also spring-webmvc.jar and spring-webmvc-portlet.jar (and maybe utility stuff like velocity jars etc).

- Liferay services
Now we want to have some persistent data and be able to manipulate it, and since Liferay provides nice facilitation for this in form of so-called "Liferay services" we’ll use it. So we create service.xml and run "ant build-service".

After that we add some logic that will use generated services, hit "ant deploy" and… get some exceptions about Spring context already being initialized for this webapp!


First efforts
After examining web.xml of deployed webapp we see that it was modified by Liferay during WAR deployment, and some custom listeners that deal with Spring context were added (like PortletContextListener and PortletContextLoaderListener). We remove our listener from web.xml, and hook up our Spring config file to one of the Spring files generated when adding Liferay services (they are added to docroot/WEB-INF/src/META-INF), for example to portlet-spring.xml, by adding import tag.

Now it’s turn for Class Not Found / No Class Def Found exceptions.
After having second look on everything we notice that spring.jar is present in Portal (ROOT) webapp’s WEB-INF/lib, but not in server’s lib (Tomcat 6 in my case BTW). So there must be some classloading trick that Liferay does to make spring.jar classes available to Liferay services in our webapps usually.
So what to do now?

Key to salvation: using Portal webapp classloader
Copying all libs from Portal (ROOT) webapp isn’t an option – hibernate session will either not be created for one of webapps, or there’ll be two sessions, or something else might conflict. The goal is to make libs from Portal (ROOT) webapp to be available for our webapp.
And solution for this exists:
Make the portlet application use the portal webapps classloader. In tomcat you achieve this by creating a file called context.xml in the META-INF folder of your portlet webapp with the following content:

<Context>
	<Loader 
		loaderClass="com.liferay.support.tomcat.loader.PortalClassLoader"
	/>
</Context>

As we’ve done this we remove from our webapp’s WEB-INF/lib the JARs that are present in Portal (ROOT) libs – spring JARs, velocity etc.
In our case we also added spring-webmvc-portlet.jar to Portal (ROOT) webapp libs (used EXT environment for this, but it doesn’t matter much how exactly you do it). Or else some nasty classloading-related exceptions occurred (like Hibernate exception class not being found etc).
Looks like there are still different classloaders used for our webapp and Portal (ROOT) webapp at some point.
Anyway, we should be done. But still we get exceptions…

JAR hell and pure magic
One of the tricks that are "pure magic" to me so far is preservation of util-bridges.jar, util-java.jar and util-taglib.jar in our webapp’s WEB-INF/lib – this fixed the classloading exceptions, and at the moment I have no idea why. I guess I’ll find it out later some day…

In any case, we found out that if those JARs are present in WEB-INF/lib of our deployed application exceptions go away. Well, not all of them yet – another thing to do is make sure commons-logging.jar and log4j.jar won’t be present. Or else we get classloading conflicts on initializing log4j on startup of our webapp.
So what’s the deal – let’s kill off log4j.jar and commons-logging.jar and add util* JARs and here we go!
Not so fast…

Let’s remind ourselves what’s happening as we build+deploy our portlet webapp with services.xml. Well, obviously major phases here are build – when WAR file is made of our webapp by Liferay-provided ant script, and deploy – when our WAR is deployed by Liferay portal.
It turns out that on both phases Liferay scripts play with content of our webapp’s WEB-INF/lib: Liferay ant scripts for building and Liferay portal logic for deploying – both shamelessly remove abovementioned util* JARs from our webapp, and on deploy phase log4j.jar and commons-logging.jar are added. What shall we do?

Preserving util* JARs
To trick scripts that run on both phases we did a simple workaround: renamed util-bridges.jar, util-java.jar and util-taglib.jar to util-bridges-renamed.jar, util-java-renamed.jar and util-taglib-renamed.jar. That’s it – util* JARs that participate in our conspiracy pass all scripts incognito under different names (-:

Getting rid of logging libs JARs
This part is trickier – the log4j.jar and commons-logging.jar are added by Liferay 5.2.3 itself when it deploys our WAR, and we don’t want to change anything in Liferay – we want changes to be done only inside our portlet plugin. So what do we do now?

Time for next trick. Let’s create the JARs with same names, but empty (yet real JAR archives – not 0 size files, just JARs with no .class files in them).

To create such files you can do something like this in terminal:

echo "This empty JAR file is here as a placeholder in order to make Liferay portlet deployer not to copy JAR with this name to WEB-INF/lib of our web-application." >> info.txt
jar -cf log4j.jar info.txt
jar -cf commons-logging.jar info.txt

We put the “empty” log4j.jar and commons-logging.jar files in our WEB-INF/lib. But on assembling WAR file Plugins SDK build scripts remove those files!
Well, there is workaround for this problem too. We modify the build.xml in our portlet plugin folder:

<?xml version="1.0"?>

<project name="btst" basedir="." default="deploy">
	<import file="../build-common-portlet.xml" />
</project>

will become

<?xml version="1.0"?>

<project name="btst" basedir="." default="deploy">
	<property name="required.portal.jars" value="paceholder_to_not_match_any_existing_file_in_required_portal_jars" />
	<import file="../build-common-portlet.xml" />
</project>

Now build scripts don’t remove those JARs, and they successfully end up in assembled WAR file in WEB-INF/lib. And when Liferay deploys it it doesn’t touch those files anymore.

And our webapp is working!

Build classpath for ant tasks
Remember at a point when we set PortalClassLoader to be used for our webapp we removed JARs like spring.jar/spring-webmvc.jar from our webapp’s WEB-INF/lib? Well, obviously now when we trigger build with ant scripts these JARs are no longer present in classpath, and some of our code doesn’t build.
Of course we could use Eclipse, configure Web Project build output to docroot/WEB-INF/classes, add all JARs from Portal (ROOT) webapp to classpath of Eclipse project, and have our classes built that way. Then ant script will work Ok because classes will already be compiled by eclipse. But don’t call "ant clean" (-:
All in all, we want build script to work again if possible. And it is possible.

We need to override "plugin.classpath" path in ant scripts with path that will contain all JARs from Portal (ROOT) webapp’s WEB-INF/lib.
(I’m not very good with Ant and didn’t knew how to do this properly, but one of my colleagues helped me with this. Thanks Yaroslav! (-: )

Again we modify the build.xml in our portlet plugin folder (only this time override of path somehow has to be defined after import directive that calls the actual build logic, not before as it was with "required.portal.jars" property):

<?xml version="1.0"?>

<project name="btst" basedir="." default="deploy">
	<property name="required.portal.jars" value="paceholder_to_not_match_any_existing_file_in_required_portal_jars" />
	<import file="../build-common-portlet.xml" />
	<path id="plugin.classpath">
		<fileset dir="${app.server.lib.global.dir}" includes="*.jar" />
		<fileset dir="${app.server.lib.portal.dir}" includes="*.jar" />
		<fileset dir="${project.dir}/lib" includes="activation.jar,jsp-api.jar,mail.jar,servlet-api.jar" />
		<path refid="plugin-lib.classpath" />
	</path>
</project>

Finally, everything compiles and works ok!

0 OK, 0:1
So what do we have now?
- Spring MVC powered portlet that is using Liferay services generated from service.xml
- Completely working build/deploy scripts
- All modifications/customizations done only in portlet plugin directory (well, except for spring-webmvc-portlet.jar added to Portal (ROOT) webapp WEB-INF/lib).
Mission accomplished.

Tips/attention points
None of deployment scripts remove existing files )-:
If you had file xx.jar in WEB-INF/lib of your portlet webapp and at some point you removed it, when deploying to Liferay 5.2.3 the new WAR file without xx.jar in WEB-INF/lib it will still remain in WEB-INF/lib of deployed webapp.
So the best deployment practice is to remove deployed webapp (in Tomcat just go to webapps folder, locate the one with name matching your WAR name and delete it – you’ll see in console output that Liferay noticed that and undeployed corresponding portlets).
Only after that let Liferay deploy the new version of your WAR.

Also keep in mind that Liferay deployment script stores some parts of deployed webapp in OS’s temporary directory (specified via "java.io.tmpdir" system property) – you might need to clean those sometimes too.

About these ads

One Response to Liferay 5.2.3 services in Spring IoC+MVC powered portlets

  1. I will follow your quest from now on ;)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: