Running Spring MVC Web Applications in OSGi
For the past couple of weeks, I've been developing a web application that deploys into an OSGi container (Equinox) and uses Spring DM's Spring MVC support. The first thing I discovered was that Spring MVC's annotations weren't supported in the M1 release. This was apparently caused by a bug in Spring 2.5.3 and not Spring DM. Since Spring DM 1.1.0 M2 was released with Spring 2.5.4 today, I believe this is fixed now.
The story below is about my experience getting a Spring MVC application up and running in Equinox 3.2.2, Jetty 6.1.9 and Spring DM 1.1.0 M2 SNAPSHOT (from last week). If you want to read more about why Spring MVC + OSGi is cool, see Costin Leau's Web Applications and OSGi article.
To get a simple "Hello World" Spring MVC application working in OSGi is pretty easy. The hard part is setting up a container with all the Spring and Jetty bundles installed and started. I imagine SSAP might solve this. Luckily for me, this was done by another member of my team.
After you've done this, it's simply a matter of creating a MANIFEST.MF for your WAR that contains the proper information for OSGi to recognize. Below is the one that I used when I first tried to get my application working.
Manifest-Version: 1 Bundle-ManifestVersion: 2 Spring-DM-Version: 1.1.0-m2-SNAPSHOT Spring-Version: 2.5.2 Bundle-Name: Simple OSGi War Bundle-SymbolicName: myapp Bundle-Classpath: .,WEB-INF/classes,WEB-INF/lib/freemarker-2.3.12.jar, WEB-INF/lib/sitemesh-2.3.jar,WEB-INF/lib/urlrewritefilter-3.0.4.jar, WEB-INF/lib/spring-beans-2.5.2.jar,WEB-INF/lib/spring-context-2.5.2.jar, WEB-INF/lib/spring-context-support-2.5.2.jar,WEB-INF/lib/spring-core-2.5.2.jar, WEB-INF/lib/spring-web-2.5.2.jar,WEB-INF/lib/spring-webmvc-2.5.2.jar Import-Package: javax.servlet,javax.servlet.http,javax.servlet.resources,javax.swing.tree, javax.naming,org.w3c.dom,org.apache.commons.logging,javax.xml.parsers;resolution:=optional, org.xml.sax;resolution:=optional,org.xml.sax.helpers;resolution:=optional
Ideally, you could generate this MANIFEST.MF using the maven-bundle-plugin. However, it doesn't support WARs in its 1.4.0 release.
You can see this is an application that uses Spring MVC, FreeMarker, SiteMesh and the URLRewriteFilter. You should be able to download it, unzip it, run "mvn package" and install it into Equinox using "install file://<path to war>".
That's all fine and dandy, but doesn't give you any benefits of OSGi. This setup works great until you try to import OSGi services using a context file with an <osgi:reference> element. After adding such a reference, it's likely you'll get the following error:
SEVERE: Context initialization failed org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Unable to locate Spring NamespaceHandler for XML schema namespace [http://www.springframework.org/schema/osgi]
To fix this, add the following to your web.xml (if you're using ContextLoaderListener, as an <init-parameter> on DispatcherServlet if you're not):
<context-param> <param-name>contextClass</param-name> <param-value>org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext</param-value> </context-param>
After doing this, you might get the following error on startup:
SEVERE: Context initialization failed org.springframework.context.ApplicationContextException: Custom context class [org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext] is not of type [org.springframework.web.context.ConfigurableWebApplicationContext]
To fix this, I change from referencing the Spring JARs in WEB-INF/lib to importing the packages for Spring (which were already installed in my Equinox container).
Bundle-Classpath: .,WEB-INF/classes,WEB-INF/lib/freemarker-2.3.12.jar, WEB-INF/lib/sitemesh-2.3.jar,WEB-INF/lib/urlrewritefilter-3.0.4.jar Import-Package: javax.servlet,javax.servlet.http,javax.servlet.resources,javax.swing.tree, javax.naming,org.w3c.dom,org.apache.commons.logging,javax.xml.parsers;resolution:=optional, org.xml.sax;resolution:=optional,org.xml.sax.helpers;resolution:=optional, org.springframework.osgi.web.context.support, org.springframework.context.support, org.springframework.web.context, org.springframework.web.context.support, org.springframework.web.servlet, org.springframework.web.servlet.mvc, org.springframework.web.servlet.mvc.support, org.springframework.web.servlet.view, org.springframework.ui, org.springframework.web.servlet.view.freemarker
After rebuilding my WAR and reloading the bundle in Equinox, I was confronted with the following error message:
SEVERE: Context initialization failed org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'freemarkerConfig' defined in ServletContext resource [/WEB-INF/myapp-servlet.xml]: Instantiation of bean failed; nested exception is java.lang.NoClassDefFoundError: freemarker/cache/TemplateLoader at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:851)
As far as I can tell, this is because the version of Spring MVC installed in Equinox cannot resolve the FreeMarker JAR in my WEB-INF/lib directory.
To prove I wasn't going insane, I commented out my "freemarkerConfig" and "viewResolver" beans in myapp-servlet.xml and changed to a regular ol' InternalResourceViewResolver:
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/"/> <property name="suffix" value=".jsp"/> </bean>
This worked and I was able to successfully see "Hello World" from a JSP in my browser. FreeMarker/SiteMesh worked too, but FreeMarker didn't work as a View for Spring MVC.
To attempt to solve this, I create a bundle for FreeMarker using "java -jar bnd-0.0.249.jar wrap freemarker-2.3.12.jar" and installed it in Equinox. I then change my MANIFEST.MF to use FreeMarker imports instead of referencing the JAR in WEB-INF/lib.
Bundle-Classpath: .,WEB-INF/classes,WEB-INF/lib/sitemesh-2.3.jar,WEB-INF/lib/urlrewritefilter-3.0.4.jar Import-Package: javax.servlet,javax.servlet.http,javax.servlet.resources,javax.swing.tree, javax.naming,org.w3c.dom,org.apache.commons.logging,javax.xml.parsers;resolution:=optional, org.xml.sax;resolution:=optional,org.xml.sax.helpers;resolution:=optional, org.springframework.osgi.web.context.support, org.springframework.context.support, org.springframework.web.context, org.springframework.web.context.support, org.springframework.web.servlet, org.springframework.web.servlet.mvc, org.springframework.web.servlet.mvc.support, org.springframework.web.servlet.view, org.springframework.ui, org.springframework.web.servlet.view.freemarker, freemarker.cache,freemarker.core,freemarker.template,freemarker.ext.servlet
Unfortunately, this still doesn't work and I still haven't been able to get FreeMarker to work with Spring MVC in OSGi. The crazy thing is I actually solved this at one point a week ago. Shortly after, I rebuilt Equinox from scratch and I'm been banging my head against the wall over this issue ever since. Last week, I entered an issue in Spring's JIRA, but thought I'd fixed it a few hours later.
I've uploaded the final project that's not working to the following URL:
http://static.raibledesigns.com/downloads/myapp-osgi.zip
If you'd like to see this project work with Spring MVC + JSP, simply modify myapp-servlet.xml to remove the FreeMarker references and use the InternalResourceViewResolver instead.
I hope Spring DM + Spring MVC supports more than just JSP as a view technology. I hope I can't get FreeMarker working because of some oversight on my part. If you have a Spring DM + Spring MVC application working with Velocity or FreeMarker, I'd love to hear about it.