It is sometimes desirable to run Spring MVC within a Sling container. However, since Sling is a web application, you may not be able to utilize the features within Spring Dynamic Modules and drop a WAR file within the container. In this case, you need to create an application which will bootstrap the Spring Web container within the Sling context.
The first thing you're going to need are the dependencies to get Spring MVC up and running within the Sling context. Some dependencies are already released with the OSGi headers. However, for those that are not, the Spring bundle repository is a great resource for downloading versions with the headers already modified so you do not have to re-wrap them. The dependencies are:
Spring AOP v3.0.6.RELEASE
Spring ASM v3.0.6.RELEASE
Spring Aspects v3.0.6.RELEASE
Spring Beans v3.0.6.RELEASE
Spring Context v3.0.6.RELEASE
Spring Context Support v3.0.6.RELEASE
Spring Core v3.0.6.RELEASE
Spring Expression v3.0.6.RELEASE
Spring Web v3.0.6.RELEASE
Spring Web Servlet v3.0.6.RELEASE
Spring OSGi IO v1.2.1
Spring OSGi Core v1.2.1
Spring OSGi Extender v1.2.1
Spring OSGi Annotation v1.2.1
Spring OSGi Web v1.2.1
AOP Alliance v1.0.0 (need OSGi version)
CGLib 2.2.0 (need OSGi version)
Commons Lang v2.6
Commons Codec v1.5
Commons Logging v1.1.1
ASM v3.2.0 (need OSGi version)
JSR 330 (javax.inject) v1.0.0 (need OSGi version)
JSR 250 (javax.annotation) v1.0.0 (need OSGi version)
Apache Sling API v2.2.0
Apache Sling Auth Core v1.0.6
Apache Sling Engine v2.2.0
Additionally, you will need an OSGi version of AspectJ Tools v1.6.6. Unfortunately, the exports in the version available from the Spring Bundle repository may conflict with some OSGi containers (like Felix), therefore we'll need to re-wrap AspectJ Tools.
Naturally, you're going to need a Sling container to run within. Sling offers a few options for download or building yourself. Alternative containers exist, such as Adobe CRX and Adobe CQ5.
Next, let's create our Maven project in Eclipse. I chose the maven-archetype-quickstart archetype as a base, but you can choose whatever type you'd prefer. Once your Maven project in Eclipse is created, open up the pom.xml file to start making our changes.
Add the Maven Compiler Plugin so we can set up the source and target Java version:
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<!-- Let's compile to JDK 6 -->
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
...
Once you've added the Maven Compiler Plugin, we can update the packaging to "bundle". Thiswill tell Maven to package this jar file with the OSGi headers required to be present in the META-INF/MANIFEST.MF file.
Next, let's start the configuration of the Maven Bundle Plugin in order to properly bundle the jar file with the OSGi headers. Right now, we just need to export our primary package. There are no dependencies yet, so we don't need to import anything either.
...
<build>
<plugins>
...
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<version>2.3.5</version>
<extensions>true</extensions>
<configuration>
<instructions>
<Export-Package>
<!-- Let's export our primary package -->
net.jasonday.examples.sling.spring.mvc,
<!-- By default, don't export any other packages -->
!*
</Export-Package>
<Import-Package>
<!-- We don't have any packages to import yet... -->
!*
</Import-Package>
</instructions>
</configuration>
</plugin>
</plugins>
</build>
...
Now let's add the Spring Bundle Repositories so Maven can find the Spring dependencies. Add the Spring release and external repositories in the <repositories> pom.xml element.
...
<repositories>
<repository>
<id>com.springsource.repository.bundles.release</id>
<name>SpringSource Enterprise Bundle Repository - SpringSource Bundle Releases</name>
<url>http://repository.springsource.com/maven/bundles/release</url>
</repository>
<repository>
<id>com.springsource.repository.bundles.external</id>
<name>SpringSource Enterprise Bundle Repository - External Bundle Releases</name>
<url>http://repository.springsource.com/maven/bundles/external</url>
</repository>
</repositories>
...
Let's now add Spring Dynamic Modules and it's dependencies. Add each dependency to your <dependencies> pom.xml element.
...
<properties>
...
<spring.maven.artifact.version>3.0.6.RELEASE</spring.maven.artifact.version>
<spring.osgi.maven.artifact.version>1.2.1</spring.osgi.maven.artifact.version>
</properties>
...
<dependencies>
...
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.aop</artifactId>
<version>${spring.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.asm</artifactId>
<version>${spring.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.aspects</artifactId>
<version>${spring.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.beans</artifactId>
<version>${spring.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.context</artifactId>
<version>${spring.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.context.support</artifactId>
<version>${spring.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.core</artifactId>
<version>${spring.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.expression</artifactId>
<version>${spring.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.web</artifactId>
<version>${spring.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>org.springframework.web.servlet</artifactId>
<version>${spring.maven.artifact.version}</version>
</dependency>
<!-- Spring OSGi -->
<dependency>
<groupId>org.springframework.osgi</groupId>
<artifactId>spring-osgi-io</artifactId>
<version>${spring.osgi.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.osgi</groupId>
<artifactId>spring-osgi-core</artifactId>
<version>${spring.osgi.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.osgi</groupId>
<artifactId>spring-osgi-extender</artifactId>
<version>${spring.osgi.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.osgi</groupId>
<artifactId>spring-osgi-annotation</artifactId>
<version>${spring.osgi.maven.artifact.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.osgi</groupId>
<artifactId>spring-osgi-web</artifactId>
<version>${spring.osgi.maven.artifact.version}</version>
</dependency>
<!-- Other various dependencies -->
<dependency>
<groupId>org.aopalliance</groupId>
<artifactId>com.springsource.org.aopalliance</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>net.sourceforge.cglib</groupId>
<artifactId>com.springsource.net.sf.cglib</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.1</version>
</dependency>
<dependency>
<groupId>org.objectweb.asm</groupId>
<artifactId>com.springsource.org.objectweb.asm</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>javax.inject</groupId>
<artifactId>com.springsource.javax.inject</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>com.springsource.javax.annotation</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Sling dependencies -->
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.api</artifactId>
<version>2.2.0</version>
<type>bundle</type>
</dependency>
<dependency>
<groupId>org.apache.sling</groupId>
<artifactId>org.apache.sling.auth.core</artifactId>
<version>1.0.6</version>
<type>bundle</type>
</dependency>
<!-- Our modified version of AspectJ Tools -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>net.jasonday.org.aspectj.tools</artifactId>
<version>1.6.6</version>
</dependency>
</dependencies>
...
First, let's add the OSGi Bundle imports that we'll need to start the Spring OSGi ApplicationContext. Add the Spring imports to the <Import-Package> element within the Maven Bundle Plugin configuration in the pom.xml like so:
...
<Import-Package>
<!-- Spring required imports -->
net.sf.cglib.beans,
net.sf.cglib.core,
net.sf.cglib.proxy,
net.sf.cglib.reflect,
net.sf.cglib.transform,
net.sf.cglib.transform.impl,
net.sf.cglib.util,
org.aopalliance.aop,
org.aopalliance.intercept,
org.apache.commons.lang,
org.apache.commons.lang.builder,
org.apache.commons.lang.time,
org.apache.commons.lang.math,
org.apache.commons.codec,
org.apache.commons.codec.binary,
org.apache.commons.logging,
org.aspectj.lang.annotation,
org.aspectj.lang,
org.objectweb.asm,
org.objectweb.asm.signature,
org.springframework.aop,
org.springframework.aop.aspectj,
org.springframework.aop.aspectj.annotation,
org.springframework.aop.aspectj.autoproxy,
org.springframework.aop.config,
org.springframework.aop.framework,
org.springframework.aop.framework.adapter,
org.springframework.aop.framework.autoproxy,
org.springframework.aop.framework.autoproxy.target,
org.springframework.aop.interceptor,
org.springframework.aop.scope,
org.springframework.aop.support,
org.springframework.aop.support.annotation,
org.springframework.aop.target,
org.springframework.aop.target.dynamic,
org.springframework.beans,
org.springframework.beans.factory,
org.springframework.beans.factory.annotation,
org.springframework.beans.factory.config,
org.springframework.context,
org.springframework.context.annotation,
org.springframework.context.support,
org.springframework.core,
org.springframework.core.io,
org.springframework.stereotype,
org.springframework.util,
<!-- Spring OSGi -->
org.springframework.osgi.extensions.annotation,
org.springframework.osgi.web.context.support,
<!-- Spring Web MVC -->
org.springframework.web.bind.annotation,
org.springframework.web.bind.support,
org.springframework.web.context,
org.springframework.web.context.request,
org.springframework.web.context.support,
org.springframework.web.servlet,
org.springframework.web.servlet.mvc.annotation,
org.springframework.web.servlet.handler,
org.springframework.web.util,
!*
</Import-Package>
...
Now let's create the Spring configuration files needed to create an ApplicationContext for our bundle. Create two files, META-INF/spring/spring.xml and META-INF/spring/spring-osgi.xml. The spring.xml file will be used to configure general Spring settings. The spring-osgi.xml file will contain Spring Dynamic Modules configurations, so we can keep track of the OSGi configurations in a separate file.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:osgi="http://www.springframework.org/schema/osgi"
xmlns:osgix="http://www.springframework.org/schema/osgi-compendium"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi.xsd
http://www.springframework.org/schema/osgi-compendium http://www.springframework.org/schema/osgi-compendium/spring-osgi-compendium.xsd">
<!-- Nothing configured yet... -->
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<!-- Tell Spring to process annotations -->
<context:annotation-config />
<!-- Tell Spring to scan our sling configuration package -->
<context:component-scan base-package="net.jasonday.examples.sling.spring.mvc.sling" />
<!-- Initialize AOP to use proxies -->
<aop:aspectj-autoproxy />
<!-- Tell Spring to process OSGi annotations -->
<bean
class="org.springframework.osgi.extensions.annotation.ServiceReferenceInjectionBeanPostProcessor" />
</beans>
Now that Spring itself is configured, we need an instance of OsgiBundleXmlWebApplicationContext within the Sling context. Since our OSGi environment is running within Sling and we want to utilize Slings resource resolution, we're going to avoid using the typical ContextLoaderListener and DispatcherServlet that would need to be configured within the web.xml. Instead, we're going to create our own ContextLoader extension that will load the OsgiBundleXmlWebApplicationContext. Create a class named SlingContextLoader that extends ContextLoader as so:
package net.jasonday.examples.sling.spring.mvc.sling;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.servlet.ServletContext;
import org.springframework.context.ApplicationContext;
import org.springframework.osgi.extensions.annotation.ServiceReference;
import org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.web.context.ContextLoader;
/**
* Create this as a bean so it's created when the bundle ApplicationContext is
* initialized.
*
*/
@Component
public class SlingContextLoader extends ContextLoader {
private ServletContext servletContext;
private ApplicationContext applicationContext;
public ApplicationContext getApplicationContext() {
return applicationContext;
}
@Inject
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public ServletContext getServletContext() {
return servletContext;
}
/**
* Retrieve a reference to the {@link javax.servlet.ServletContext
* ServletContext} from the container for the
* {@link org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext
* OsgiBundleXmlWebApplicationContext}
* @param servletContext Instance of ServletContext
*/
@ServiceReference
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
/**
* Return the type of {@link org.springframework.context.ApplicationContext
* ApplicationContext} that we want to create. In our case, we want an
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* that is OSGi aware and configured for Spring MVC.
*/
@Override
protected Class<?> determineContextClass(ServletContext currentServletContext) {
return OsgiBundleXmlWebApplicationContext.class;
}
/**
* Return the bundle {@link org.springframework.context.ApplicationContext
* ApplicationContext} in order to properly access the bundles' classpath.
*/
@Override
protected ApplicationContext loadParentContext(ServletContext currentServletContext) {
return applicationContext;
}
/**
* Initialize the actual
* {@link org.springframework.web.context.WebApplicationContext
* WebApplicationContext} instance.
*/
@PostConstruct
public void init() {
initWebApplicationContext(servletContext);
}
/**
* Since we'll be running within an OSGi environment, the bundle may be
* unregistered. This means we need to make sure we clean up the
* {@link javax.servlet.ServletContext ServletContext} if this bundle is
* unregistered.
*/
@PreDestroy
public void destroy() {
closeWebApplicationContext(servletContext);
}
}
Make sure you add your newly required packages to the pom.xml Maven Bundle Plugin <Import-Package> element.
...
<Import-Package>
...
<!-- JSR 250 && 330 -->
javax.annotation,
javax.inject,
<!-- JEE Servlet -->
javax.servlet,
javax.servlet.http,
...
!*
</Import-Package>
...
We'll also need to now create an WEB-INF/applicationContext.xml file for the OsgiBundleXmlWebApplicationContext because it will be used to build the local WebApplicationContext.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<!--
This configuration file is required since the
OsgiBundleXmlWebApplicationContext class looks for it by default
-->
<!--
Define any cross-web application beans here.
-->
</beans>
Now that we have the OsgiBundleXmlWebApplicationContext configured, we need to initialize the DispatcherServlet. Since we're in an OSGi environment using a shared ServletContext, we will need to create a subclass of DispatcherServlet that cleans up the ServletContext of the servlet name. Create a new subclass named SlingDispatcherServlet.
package net.jasonday.examples.sling.spring.mvc.sling;
import javax.annotation.PreDestroy;
import javax.servlet.Servlet;
import org.springframework.web.servlet.DispatcherServlet;
/**
* Subclass of a {@link org.springframework.web.servlet.DispatcherServlet
* DispatcherServlet} that will remove the instances' name from the
* {@link javax.servlet.ServletContext ServletContext} attributes on
* destruction.
*
*/
public class SlingDispatcherServlet extends DispatcherServlet implements Servlet {
/**
* Destory the servlet and remove the name from the
* {@link javax.servlet.ServletContext ServletContext}'s attributes
*/
@PreDestroy
@Override
public void destroy() {
// don't forget to call the super destroy method
super.destroy();
// remove the ServletContext attribute for this servlets' ApplicationContext
String attrName = getServletContextAttributeName();
getServletContext().removeAttribute(attrName);
}
}
Additionally, since we're going to have to export our DispatcherServlet as an OSGi service so Sling can find it, we'll need to create an instance.
package net.jasonday.examples.sling.spring.mvc.sling;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
@Configuration
public class SlingConfiguration {
/**
* Create and return an instance of {@link SlingDispatcherServlet}.
* <p>
* This is required to be instantiated as a Spring bean in order for it to be
* exported as an OSGi service by Spring.
* <p>
* This bean must depend on the {@link SlingContextLoader} finishing first,
* else this instance will attempt to create it's own
* {@link org.springframework.web.context.WebApplicationContext
* WebApplicationContext} rather than using the one that will be set up by
* {@link SlingContextLoader}.
* <p>
* We will need the bean name in order to configure the Spring DM service
* exportation. Additionally, since we may create multiple instances of a
* {@link SlingDispatcherServlet} for different purposes, we should give each
* one a unique bean name.
* @return SlingDispatcherServlet A new instance of SlingDispatcherServlet
*/
@Bean
@DependsOn("slingContextLoader")
public SlingDispatcherServlet globalSlingDispatcherServlet() {
return new SlingDispatcherServlet();
}
}
We will need a Spring configuration file for every instance of SlingDispatcherServlet created. Alternatively, you could override this behavior when creating an instance of each SlingDispatcherServlet. By default the SlingDispatcherServlet will look for a WEB-INF/[servlet-name]-servlet.xml file. In our case we'll use the servlet-name globaldispatcher.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">
<!-- Tell Spring to process annotations -->
<context:annotation-config />
<!--
Do not scan the package that contains the Spring classes required to
initialize the WebApplicationContext
-->
<context:component-scan base-package="net.jasonday.examples.sling.spring.mvc">
<context:exclude-filter type="regex" expression="net\.jasonday\.examples\.sling\.spring\.mvc\.sling\..*"/>
</context:component-scan>
<!-- Initialize AOP to use proxies -->
<aop:aspectj-autoproxy />
<!-- Tell Spring to process OSGi annotations -->
<bean
class="org.springframework.osgi.extensions.annotation.ServiceReferenceInjectionBeanPostProcessor" />
</beans>
Now that we've created the necessary DispatcherServlet instance, we need to expose our SlingDispatcherServlet as an OSGi service. To do this, we'll update the META-INF/spring/spring-osgi.xml file to export our SlingDispatcherServlet under the ServletContext interface.
...
<!--
Expose an instance of the SlingDispatcherServlet as an OSGi service. This is exposed in the global Spring
OSGi configuration file since this instance will initialize the configurations within the WEB-INF folder.
-->
<osgi:service ref="globalSlingDispatcherServlet">
<!--
Expose this OSGi service as a javax.servlet.Servlet so that Sling will pick it up and register it as a Servlet
-->
<osgi:interfaces>
<value>javax.servlet.Servlet</value>
</osgi:interfaces>
<osgi:service-properties>
<!--
The sling.servlet.resourceTypes value is the value that we will put onto nodes under the sling:resourceType
property in order to register them to use this DispatcherServlet instance. If multiple SlingDispatcherServlet
instances are created, this is what will identify which one Sling should use.
-->
<entry key="sling.servlet.resourceTypes" value="examples/sling/spring/mvc/dispatcher/global" />
<!--
The extensions on which we want to listen. Add/remove any other extensions.
-->
<entry key="sling.servlet.extensions">
<array>
<value>json</value>
<value>html</value>
</array>
</entry>
<!--
The methods on which we want to listen. Add/remove any other methods (eg. PUT, DELETE, etc.).
-->
<entry key="sling.servlet.methods">
<array>
<value>GET</value>
<value>POST</value>
</array>
</entry>
<!-- Tell the SlingDispatcherServlet what kind of ApplicationContext to build -->
<entry key="contextClass" value="org.springframework.osgi.web.context.support.OsgiBundleXmlWebApplicationContext"/>
<!--
Tell the SlingDispatcherSerlvet this servlet context name. This can be made up, but a
WEB-INF/<servletName>-servlet.xml file must be created for each.
-->
<entry key="sling.core.servletName" value="globaldispatcher" />
</osgi:service-properties>
</osgi:service>
...
Now that we've configured Spring to work in a Sling context, let's create a Spring MVC Controller to test it out. We'll make our controller simple for now to test it out. At this point, we'll use the @Controller annotation to mark our controller. The @Controller annotation requires the use of a @RequestMapping annotation to map URLs to requests. Since we haven't built anything to change this behavior, we'll map our helloWorld method to /content/examples/sling/spring/mvc/mySlingController.
package net.jasonday.examples.sling.spring.mvc.controller;
import java.io.IOException;
import javax.servlet.ServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Example Spring MVC Controller class.
*
*/
@Controller
public class MySlingController {
/**
* Hello world method that will listen for requests at the URL
* <strong>/content/examples/sling/spring/mvc/mySlingController</strong>.
* <p>
* Right now, we'll map to a specific URL to ensure everything is working
* properly.
* @param servletResponse ServletResponse object for writing out to the
* response
* @throws IOException Thrown if there is an exception writing out to the
* response
*/
@RequestMapping("/content/examples/sling/spring/mvc/mySlingController")
public void helloWorld(ServletResponse servletResponse) throws IOException {
// write out "Hello world!" to test the response
servletResponse.getWriter().println("Hello world!");
}
}
Now that we have a Spring MVC Controller created, we need to create the node in the JCR repository at the /content/examples/sling/spring/mvc/mySlingController URL location that will use our SlingDispatcherServlet to handle requests. Let's create the following structure:
/content [sling:Folder] /content/examples [sling:Folder] /content/examples/sling [sling:Folder] /content/examples/sling/spring [sling:Folder] /content/examples/sling/spring/mvc [sling:Folder] /content/examples/sling/spring/mvc/mySlingController [nt:unstructured]
Now, to use our SlingDispatcherServlet for requests to this node, we must add a sling:resourceType property to the mySlingController node with the resource type we configured in our spring-osgi.xml file, in our case examples/sling/spring/mvc/dispatcher/global. We do this because Sling needs to know what script to use for handling requests to the /content/examples/sling/spring/mvc/mySlingController.
Even though we've configured our MySlingController class to map requests to this URL, only Spring knows about this mapping. In order to hook the Spring mapping into Sling, we need a sling:resourceType property on our /content/examples/sling/spring/mvc/mySlingController node that will tell Sling to map requests for our node to the SlingDispatcherServlet. Add a property to the /content/examples/sling/spring/mvc/mySlingController node named sling:resourceType with a value of examples/sling/spring/mvc/dispatcher/global. When requests for /content/examples/sling/spring/mvc/mySlingController come into Sling, it will check the sling:resourceType property and see it's configured to use the examples/sling/spring/mvc/dispatcher/global script. This sling:resourceType resolves to the SlingDispatcherServlet we configured in our spring-osgi.xml file (the sling.servlet.resourceTypes service property). Sling will dispatch the request to the SlingDispatcherServlet, which will then check for a mapping to the /content/examples/sling/spring/mvc/mySlingController URL. We configured our MySlingController Controller class to map to /content/examples/sling/spring/mvc/mySlingController through the use of the @RequestMapping annotation, so the SlingDispatcherServlet will dispatch the request to our controller. Test this out by going to /content/examples/sling/spring/mvc/mySlingController.html in your browser, and you should see the following:
Hello world!
Now that we've verified Sling is resolving to our SlingDispatcherServlet to handle requests when the URLs are properly mapped, we can update SlingDispatcherServlet to be Sling aware. What does this mean? Well, right now our Controller class has to know the exact URL to map to. This creates a tight coupling between the Controller and the possible nodes we configure in the JCR to be handled by Spring MVC, which we would like to avoid. What we would really like to do is to allow our Controller classes to map to specific controller types we configure on our JCR nodes. For example, instead of mapping the MySlingController helloWorld method only to the /content/examples/sling/spring/mvc/mySlingController URL, we would rather add a property to our JCR node that identifies the Spring MVC Controller class to which requests should be handled. To do this, we're going to need to change the how the SlingDispatcherServlet maps Controller classes. Fortunately, Spring gives us a hook in order to do this through the configuration of the HandlerMapping and HandlerAdapter interfaces. We will need to create a subclass of the UrlPathHelper class that changes the lookup path Spring uses to map Controllers.
First, we need to create a utility class with a method that will unrwap the HttpServletRequest to find the SlingHttpServletRequest.
package net.jasonday.examples.sling.spring.mvc.spring;
import javax.servlet.ServletRequest;
import javax.servlet.ServletRequestWrapper;
import org.apache.sling.api.SlingHttpServletRequest;
/**
* Utility class for working with
* {@link org.apache.sling.api.SlingHttpServletRequest SlingHttpServletRequest}.
*
*/
public class SlingHttpServletRequestUtils {
/**
* Unwrap the {@link org.apache.sling.api.SlingHttpServletRequest
* SlingHttpServletRequest} from the given
* {@link javax.servlet.ServletRequest ServletRequest}. If an instance of
* {@link org.apache.sling.api.SlingHttpServletRequest
* SlingHttpServletRequest} is not found, returns null.
*
* @param servletRequest Wrapped instance of
* {@link org.apache.sling.api.SlingHttpServletRequest
* SlingHttpServletRequest}
* @return SlingHttpServletRequest The unwrapped
* {@link SlingHttpServletRequest} instance if found, else null
*/
public static SlingHttpServletRequest unwrap(ServletRequest servletRequest) {
while (servletRequest instanceof ServletRequestWrapper) {
servletRequest = ((ServletRequestWrapper) servletRequest).getRequest();
// immediate termination if we found one
if (servletRequest instanceof SlingHttpServletRequest) {
return (SlingHttpServletRequest) servletRequest;
}
}
return null;
}
}
Now we can create a subclass of UrlPathHelper that will check for a property on the node corresponding to the URL requested for a property named controllerType that will give us the mapping for the Spring MVC Controller class/method.
package net.jasonday.examples.sling.spring.mvc.spring;
import javax.servlet.http.HttpServletRequest;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ValueMap;
import org.springframework.web.util.UrlPathHelper;
/**
*
* Sub-class of {@link org.springframework.web.util.UrlPathHelper UrlPathHelper}
* that will check for a property named {@link #CONTROLLER_TYPE_PROPERTY} on a
* Sling {@link org.apache.sling.api.resource.Resource Resource} and return it
* as the lookup path if found. Defaults to returning the super methods' lookup
* path value if a Sling {@link org.apache.sling.api.resource.Resource Resource}
* with a {@link #CONTROLLER_TYPE_PROPERTY} property is not found.
*
*/
public class SlingUrlPathHelper extends UrlPathHelper {
public static final String CONTROLLER_TYPE_PROPERTY = "controllerType";
/**
* Override the method to check for a Sling
* {@link org.apache.sling.api.resource.Resource Resource} with a property
* named {@link #CONTROLLER_TYPE_PROPERTY} and return it's value as the lookup
* path. If not found, defaults to super methods behavior.
*/
@Override
public String getLookupPathForRequest(HttpServletRequest request) {
// get the SlingHttpServletRequest instance from the given
// HttpServletRequest
SlingHttpServletRequest slingRequest = SlingHttpServletRequestUtils.unwrap(request);
if (null != slingRequest) {
// get the Resource for this request, then pull a property from it
Resource resource = slingRequest.getResource();
if (null != resource) {
ValueMap properties = resource.adaptTo(ValueMap.class);
if (properties.containsKey(CONTROLLER_TYPE_PROPERTY)) {
return properties.get(CONTROLLER_TYPE_PROPERTY, null);
}
}
}
// default to super behavior
return super.getLookupPathForRequest(request);
}
}
Once our SlingUrlPathHelper class is created, we need to configure Spring to use our SlingUrlPathHelper for mapping requests rather than the UrlPathHelper class. We do this by configuring our own HandlerMapping and HandlerAdapter instances. We can easily do this with a Spring @Configuration file.
package net.jasonday.examples.sling.spring.mvc.spring;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter;
import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping;
/**
* Configure the Sling aware handler classes for Spring MVC.
*
*/
@Configuration
public class SlingHandlerConfiguration {
/**
* Configures a {@link org.springframework.web.servlet.HandlerMapping
* HandlerMapping} instance for use in a Sling environment by overriding the
* {@link org.springframework.web.util.UrlPathHelper UrlPathHelper} with a
* {@link SlingUrlPathHelper} instance.
*
* @return HandlerMapping An instance of HandlerMapping for use in a Sling
* environment
*/
@Bean
public HandlerMapping handlerMapping() {
// override the DefaultAnnotationHandlerMappings' UrlPathHelper
DefaultAnnotationHandlerMapping handlerMapping = new DefaultAnnotationHandlerMapping();
handlerMapping.setUrlPathHelper(new SlingUrlPathHelper());
return handlerMapping;
}
/**
* Configures a {@link org.springframework.web.servlet.HandlerAdapter
* HandlerAdapter} instance for use in a Sling environment by overriding the
* {@link org.springframework.web.util.UrlPathHelper UrlPathHelper} with a
* {@link SlingUrlPathHelper} instance.
*
* @return HandlerAdapter An instance of HandlerAdapter for use in a Sling
* environment
*/
@Bean
public HandlerAdapter handlerAdapter() {
// override the AnnotationMethodHandlerAdapters' UrlPathHelper
AnnotationMethodHandlerAdapter handlerAdapter = new AnnotationMethodHandlerAdapter();
handlerAdapter.setUrlPathHelper(new SlingUrlPathHelper());
return handlerAdapter;
}
}
With our customized HandlerMapping and HandlerAdapter instances, we can now modify our MySlingController classes to map to anything we want. In our case, we're going to map them to /examples/sling/spring/mvc/mySlingController controller types. Note: The DefaultAnnotationHandlerMapping and AnnotationMethodHandlerAdapter classes both expect mappings to begin with "/", therefore we'll need our controllerType properties to be configured with this or else Spring won't properly find the mappings.
...
public class MySlingController {
/**
* Hello world method that will map to
* <strong>examples/sling/spring/mvc/mySlingController</strong> controller
* types.
* <p>
* @param servletResponse ServletResponse object for writing out to the
* response
* @throws IOException Thrown if there is an exception writing out to the
* response
* @see net.jasonday.examples.sling.spring.mvc.spring.SlingUrlPathHelper
*/
@RequestMapping("examples/sling/spring/mvc/mySlingController")
public void helloWorld(ServletResponse servletResponse) throws IOException {
// write out "Hello world!" to test the response
servletResponse.getWriter().println("Hello world!");
}
}
Finally, we can create any node in the repository with a property named controllerType with a value of /examples/sling/spring/mvc/mySlingController in order to have our MySlingController instance handle requests. Create a new node anywhere you would like in your repository. I'll use /content/mySlingController.
/content [sling:Folder] /content/mySlingController [nt:unstructured]
We still need to add a property named sling:resourceType with a value of examples/sling/spring/mvc/dispatcher/global in order to tell Sling to use the SlingDispatcherServlet to handle requests. However, now we'll need to add an additional property named controllerType with a value of /examples/sling/spring/mvc/mySlingController in order to have our MySlingController instance handle requests.
Lastly, we have to add the Sling imports to our <Import-Package> element within the Maven Bundle Plugin configuration in the pom.xml so our bundle can use the Sling classes we need.
...
<Import-Package>
...
<!-- Sling -->
org.apache.sling.api,
org.apache.sling.api.resource,
org.apache.sling.auth.core,
...
!*
</Import-Package>
...
Now, you should be able to pull up /content/mySlingController.html and see MySlingController handling the response.
Hello world!
Now that our Controller is Sling aware, we may want to inject some Sling classes into our Controller methods. For example, we may want access to the SlingHttpServletRequest or ResourceResolver. Fortunately, Spring makes it easy to add this functionality through the use of the WebArgumentResolver interface. First up, let's allow the SlingHttpServletRequest class to be injected into our Controller methods. Create an implementation of WebArgumentResolver to unwrap the SlingHttpServletRequest from the given HttpServletRequest.
package net.jasonday.examples.sling.spring.mvc.spring.resolver;
import javax.servlet.http.HttpServletRequest;
import net.jasonday.examples.sling.spring.mvc.spring.SlingHttpServletRequestUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.context.request.NativeWebRequest;
/**
* {@link org.springframework.web.bind.support.WebArgumentResolver Resolver} for
* resolving the {@link org.apache.sling.api.SlingHttpServletRequest
* SlingHttpServletRequest} for injection into
* {@link org.springframework.stereotype.Controller Controller} methods.
*
*/
public class SlingRequestArgumentResolver implements WebArgumentResolver {
/**
* Implementation of the resolveArgument method to search for an instance of
* {@link org.apache.sling.api.SlingHttpServletRequest
* SlingHttpServletRequest}
*/
@Override
public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) throws Exception {
// verify we're looking for an instance of SlingHttpServletRequest
if (!methodParameter.getParameterType().equals(SlingHttpServletRequest.class)) {
return UNRESOLVED;
}
// check if the request is an instance of SlingHttpServletRequest
HttpServletRequest httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest();
SlingHttpServletRequest slingRequest = SlingHttpServletRequestUtils.unwrap(httpServletRequest);
if (null != slingRequest) {
// we found it, return it
return slingRequest;
}
return UNRESOLVED;
}
}
Now, let's create an implementation of WebArgumentResolver to pull the ResourceResolver instance out of the SlingHttpServletRequest.
package net.jasonday.examples.sling.spring.mvc.spring.resolver;
import javax.servlet.http.HttpServletRequest;
import net.jasonday.examples.sling.spring.mvc.spring.SlingHttpServletRequestUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.auth.core.AuthenticationSupport;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.context.request.NativeWebRequest;
/**
* {@link org.springframework.web.bind.support.WebArgumentResolver Resolver} for
* resolving the {@link org.apache.sling.api.resource.ResourceResolver
* ResourceResolver} for injection into
* {@link org.springframework.stereotype.Controller Controller} methods.
*
*/
public class ResourceResolverArgumentResolver implements WebArgumentResolver {
/**
* Implementation of the resolveArgument method to search for an instance of
* {@link org.apache.sling.api.resource.ResourceResolver ResourceResolver}
*/
@Override
public Object resolveArgument(MethodParameter methodParameter, NativeWebRequest webRequest) throws Exception {
// verify we're looking for an instance of ResourceResolver
if (!methodParameter.getParameterType().equals(ResourceResolver.class)) {
return UNRESOLVED;
}
// first check if the request is an instance of SlingHttpServletRequest
HttpServletRequest httpServletRequest = (HttpServletRequest) webRequest.getNativeRequest();
SlingHttpServletRequest slingRequest = SlingHttpServletRequestUtils.unwrap(httpServletRequest);
if (null != slingRequest) {
return slingRequest.getResourceResolver();
}
// check for it in the request attributes
if (null != httpServletRequest.getAttribute(AuthenticationSupport.REQUEST_ATTRIBUTE_RESOLVER)) {
return httpServletRequest.getAttribute(AuthenticationSupport.REQUEST_ATTRIBUTE_RESOLVER);
}
return UNRESOLVED;
}
}
All we need to do now is register our WebArgumentResolver with the AnnotationMethodHandlerAdapter we configured. We can do this by using the setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) method.
...
public class SlingHandlerConfiguration {
...
/**
* Configures a {@link org.springframework.web.servlet.HandlerAdapter
* HandlerAdapter} instance for use in a Sling environment by overriding the
* {@link org.springframework.web.util.UrlPathHelper UrlPathHelper} with a
* {@link SlingUrlPathHelper} instance.
*
* @return HandlerAdapter An instance of HandlerAdapter for use in a Sling
* environment
*/
@Bean
public HandlerAdapter handlerAdapter() {
// override the AnnotationMethodHandlerAdapters' UrlPathHelper
AnnotationMethodHandlerAdapter handlerAdapter = new AnnotationMethodHandlerAdapter();
handlerAdapter.setUrlPathHelper(new SlingUrlPathHelper());
// configure the custom WebArgumentResolvers
handlerAdapter.setCustomArgumentResolvers(new WebArgumentResolver[] { new ResourceResolverArgumentResolver(),
new SlingRequestArgumentResolver() });
return handlerAdapter;
}
}
Now you can pass SlingHttpServletRequest and ResourceResolver as arguments to your Controller methods just like any of the other pre-configured class types.
...
public class MySlingController {
/**
* Hello world method that will map to
* <strong>examples/sling/spring/mvc/mySlingController</strong> controller
* types.
* <p>
* @param servletResponse ServletResponse object for writing out to the
* response
* @param slingRequest SlingHttpServletRequest object
* @param resourceResolver ResourceResolver object
* @throws IOException Thrown if there is an exception writing out to the
* response
* @see net.jasonday.examples.sling.spring.mvc.spring.SlingUrlPathHelper
*/
@RequestMapping("examples/sling/spring/mvc/mySlingController")
public void helloWorld(ServletResponse servletResponse, SlingHttpServletRequest slingRequest,
ResourceResolver resourceResolver) throws IOException {
// write out "Hello world!" to test the response
servletResponse.getWriter().println("Hello world!");
servletResponse.getWriter().println("My slingRequest is '" + (null == slingRequest ? "null" : slingRequest) + "'");
servletResponse.getWriter().println("My resourceResolver is '" + (null == resourceResolver ? "null" : resourceResolver) + "'");
}
}
Comments
Maxim Gubin (not verified)
Wed, 12/21/2011 - 1:50pm
Permalink
Maven OSGified dependencies
What repo do you specify in order to get those OSGified dependencies?I remember Spring used to have their repo of all of the OSGified jars, but it's not there anymore.
jasonday
Wed, 01/04/2012 - 11:37pm
Permalink
SpringSource Enterprise Bundle Repository
From the SpringSource Enterprise Bundle Repository
Add new comment