 |
Integrating Jersey and Spring
Posted by mhadley on September 14, 2007 at 09:18 AM | Comments (10)
To further test the new resource provider SPI I described earlier I thought I'd try building a new resource provider that defers to Spring for resource creation. Note that I've never used Spring before so if there are better ways to accomplish what I did please let me know. I also ran into a problem that limited the integration more than I'd prefer (details at the end of this post). The Jersey dev list would be a great place to post any suggestions on how to improve the integration.
Implementation
The first task was to write a ResourceProvider implementation that uses Spring for resource creation. This is pretty straightforward and the getInstance method turned out to require only a few lines of code:
public Object getInstance(ResourceProviderContext context) {
try {
initSpringContext(context);
initBeanName();
Object resource = springContext.getBean(beanName, resourceClass);
context.injectDependencies(resource);
return resource;
} catch (Exception ex) {
throw new ContainerException("Unable to create resource", ex);
}
}
The ResourceProvider interface only supplies a ResourceProviderContext to the getInstance method so I had to defer some initialization code to that method that would have fit better in the init method. I'll check if it makes sense to add a ResourceProviderContext to the init method but for this experiment I stuck with the SPI as is. The initSpringContext method uses the ResourceProviderContext to get the ServletContext which is then used to get the Spring ApplicationContext which has been initialized in the suggested way.
protected static synchronized void initSpringContext(
ResourceProviderContext context) {
if (springContext==null) {
DummyResource r = new DummyResource();
context.injectDependencies(r);
springContext = WebApplicationContextUtils.getRequiredWebApplicationContext(
r.servletConfig.getServletContext());
}
}
DummyResource is a simple annotated static inner class that is used as an injection target:
public static class DummyResource {
@Resource
public ServletConfig servletConfig;
}
Jersey is based on annotated classes whereas Spring is based on named beans. The Spring ResourceProvider needs to map a class name to a Spring bean name which is accomplished with the initBeanName method:
protected synchronized void initBeanName() {
if (beanName==null) {
String names[] = springContext.getBeanNamesForType(resourceClass);
if (names.length==0)
throw new RuntimeException("No configured bean for "+resourceClass.getName());
else if (names.length>1)
throw new RuntimeException("Multiple configured beans for "+resourceClass.getName());
beanName=names[0];
}
}
I couldn't see any straightforward way to determine which bean to use for a given resource class if there is more than one defined at the top level so the provider requires a single top-level bean for each resource class.
The final task was to define a new annotation that instructs the Jersey runtime to use the Spring resource provider:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ResourceFactory(SpringProvider.class)
public @interface SpringFactory {}
Example
With the above implemented you can now write a resource class whose instantiation is controlled by Spring:
@UriTemplate("{id}")
@SpringFactory
public class SpringResource {
private String name;
public SpringResource() {
name="unset";
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@HttpMethod("GET")
@ProduceMime("text/plain")
public String getDescription() {
return "Name: "+getName();
}
}
With the following applicationContext.xml in the WEB-INF directory the resource will report its name as "Mr. Bean" thus demonstrating Spring-provided resource injection.
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="bean1" scope="prototype" class="com.sun.ws.rest.spring.resources.SpringResource">
<property name="name" value="Mr. Bean"/>
</bean>
</beans>
The complete code is available here. No libraries are included in the ZIP file so you'll have to patch up the references to the required Jersey and Spring libraries.
Integration Shortcoming
I couldn't see any way to add support for Jersey-defined resources as constructor parameters. In an earlier post I described the new support for non-empty constructors. When using the Spring resource provider, the constructor can only include resources defined within the Spring configuration, not values supplied by Jersey. Is there a Spring API I could use to programatically add support for Jersey-defined constructor parameters ?
Bookmark blog post: del.icio.us Digg DZone Furl Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment
-
Hi Marc,
Thanks for posting the example. I've tried it out and it works nicely.
I did find problems with it when I wanted to apply some AOP to my resources. Basically,
context.injectDependencies(resource); didn't work. For example, context and uriInfo were null on this resource:
@SpringFactory
@Component("TestResource")
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
@UriTemplate("/test")
public class TestResource {
@HttpContext
HttpContextAccess context;
@HttpContext
UriInfo uriInfo;
@HttpMethod(HttpMethod.GET)
@ProduceMime("text/plain")
public Response getHelloWorld() {
return Response.Builder.ok("Hello World!").build();
}
}
This was due to the resource being enhanced by CGLib for AOP. In the hope that this will help someone else, a change to the getInstance() method solved this:
...
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
...
public Object getInstance(ResourceProviderContext context) {
try {
initSpringContext(context);
initBeanName();
Object resource = springContext.getBean(beanName, resourceClass);
if (AopUtils.isAopProxy(resource)) {
Advised aopResource = (Advised) resource;
context.injectDependencies(aopResource.getTargetSource().getTarget());
} else {
context.injectDependencies(resource);
}
return resource;
} catch (Exception ex) {
throw new ContainerException("Unable to create resource", ex);
}
}
However, this solution doesn't work for JDK proxied resources.
Thanks,
Duncan Eley
Posted by: djeley on January 03, 2008 at 12:51 PM
-
Here's a spring service bean that automatically loads Resource Classes for jersey on context startup. It uses the SpringFactory annotation above to identify resources. It's probably not the "best" way, but I found the whole apt/WebResources step somewhat unpalatable.
import com.sun.ws.rest.api.core.DefaultResourceConfig;
import com.sun.ws.rest.api.core.ResourceConfig;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import java.util.Map;
import java.util.Set;
/**
* A Spring service that initializes the Jersey ResourceConfig by iterating through beans in the spring application
* context and adding beans that have the {@link SpringFactory} annotation.
*
* This service class should be used in the jersey servlet entry in web.xml to provide a "webresources" entry:
*
*
* <init-param>
* <param-name>webresourceclass</param-name>
* <param-value>com.matchmine.rs.ws.jersey.SpringResourceConfigService</param-value>
* </init-param>
*
*
*/
public class SpringResourceConfigService
implements ApplicationContextAware, InitializingBean, ResourceConfig {
protected static ApplicationContext context;
private static final DefaultResourceConfig defaultResourceConfig = new DefaultResourceConfig();
/**
* Declare the set of root resource classes
*/
protected void initResourceClasses() {
for (String beanName : context.getBeanDefinitionNames()) {
Object bean = context.getBean(beanName);
if (bean.getClass().getAnnotation(SpringFactory.class) != null) {
getResourceClasses().add(bean.getClass());
}
}
}
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public void afterPropertiesSet() throws Exception {
initResourceClasses();
}
public Set getResourceClasses() {
return defaultResourceConfig.getResourceClasses();
}
public Map getFeatures() {
return defaultResourceConfig.getFeatures();
}
public boolean getFeature(String featureName) {
return defaultResourceConfig.getFeature(featureName);
}
public Map getProperties() {
return defaultResourceConfig.getProperties();
}
public Object getProperty(String propertyName) {
return defaultResourceConfig.getProperty(propertyName);
}
}
Thanks for the Spring work!
--Joachim
Posted by: joachimm on January 15, 2008 at 01:17 PM
-
Hi Joachim,
That is really nice. Marc and I found the whole apt/WebResources unpalatable too, so we have changed it for Jersey 0.5 and removed all those steps. Now, by default, things work dynamically using a ResourceConfig implementation that scans for root resource classes given a classpath (there is also one to scan for classes given a list of packages).
Your approach with Spring fits very nicely with the general concept of Jersey configuration.
Paul.
Posted by: sandoz on January 16, 2008 at 01:07 AM
-
Small update for 0.5 API change:
public void init(AbstractResource abstractResource, Map stringBooleanMap, Map stringObjectMap) {
this.resourceClass = abstractResource.getResourceClass();
}
I couldn't get the new asm stuff to work with Spring, so I am going to stick with this solution.
Also need to change the init param from "webresourceclass" to
"com.sun.ws.rest.config.property.resourceConfigClass"
I would rather go with the asm approach, it is very elegant. Maybe you could take a look at backporting it to an asm release compatible with Spring 2.x?
Thanks --Joachim
Thanks --Joachim
Posted by: joachimm on January 18, 2008 at 03:22 PM
-
Hi Joachim
Perhaps the following link could help you with ASM incompatibilities as similar things occur with Spring and Hibernate:
http://blog.interface21.com/main/2007/06/11/asm-version-incompatibilities-using-spring-autowired-with-hibernate/
Posted by: sandoz on January 28, 2008 at 09:33 AM
-
I have been able to get some small samples running using the technique above. The applications work, however, when I deploy them to my dev glassfish container, I get the following exception:
NAM0006: JMS Destination object not found: com.sun.ws.rest.spring.provider.SpringProvider/servletConfig
javax.naming.NameNotFoundException
javax.naming.NameNotFoundException
at com.sun.enterprise.naming.TransientContext.resolveContext(TransientContext.java:268)
at com.sun.enterprise.naming.TransientContext.lookup(TransientContext.java:191)
at com.sun.enterprise.naming.SerialContextProviderImpl.lookup(SerialContextProviderImpl.java:74)
at com.sun.enterprise.naming.LocalSerialContextProviderImpl.lookup(LocalSerialContextProviderImpl.java:111)
at com.sun.enterprise.naming.SerialContext.lookup(SerialContext.java:339)
at javax.naming.InitialContext.lookup(InitialContext.java:392)
at com.sun.enterprise.naming.NamingManagerImpl.bindObjects(NamingManagerImpl.java:391)
at com.sun.enterprise.web.WebModuleContextConfig.configureResource(WebModuleContextConfig.java:220)
at com.sun.enterprise.web.WebModuleContextConfig.lifecycleEvent(WebModuleContextConfig.java:161)
at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(LifecycleSupport.java:143)
at org.apache.catalina.core.StandardContext.init(StandardContext.java:6342)
at org.apache.catalina.core.StandardContext.start(StandardContext.java:4840)
at com.sun.enterprise.web.WebModule.start(WebModule.java:327)
at com.sun.enterprise.web.LifecycleStarter.doRun(LifecycleStarter.java:58)
at com.sun.appserv.management.util.misc.RunnableBase.runSync(RunnableBase.java:296)
at com.sun.appserv.management.util.misc.RunnableBase._submit(RunnableBase.java:168)
at com.sun.appserv.management.util.misc.RunnableBase.submit(RunnableBase.java:184)
at com.sun.enterprise.web.VirtualServer.startChildren(VirtualServer.java:1672)
at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1231)
at org.apache.catalina.core.StandardHost.start(StandardHost.java:955)
at com.sun.enterprise.web.LifecycleStarter.doRun(LifecycleStarter.java:58)
at com.sun.appserv.management.util.misc.RunnableBase.runSync(RunnableBase.java:296)
at com.sun.appserv.management.util.misc.RunnableBase.run(RunnableBase.java:330)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:441)
at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303)
at java.util.concurrent.FutureTask.run(FutureTask.java:138)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:885)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:907)
at java.lang.Thread.run(Thread.java:619)
The container seems to 'not like' the @Resource annotation. Any ideas on why this is happening?
Thanks,
-Dave
Posted by: dpederson on January 28, 2008 at 10:33 AM
-
Hi Dave, i think it is because we are using @Resource on a non-standard component for injecting stuff and the Jersey runtime is treating @Resource a lot looser than that used for EE that has requirements related JNDI naming (it is something we need to look at for better EE integration). I think the app server is scanning all classes for @Resource to bind things ready to support injection of stuff. As i understand it the exceptions do not result in any errant behaviour so i think they can be ignored. Also i no longer see such exceptions being logged when using FCS GF v2. Paul.
Posted by: sandoz on January 31, 2008 at 01:53 AM
-
Hi,
dpeterson - I have the same problem you have. I was surprised to see a "JMS destination" message on this application.
Did your problem only occur on Glassfish ? (only app server I tried so far)
And did you find a solution for this?
Thanks, Peter
Posted by: faithnomore on January 31, 2008 at 04:46 AM
-
I've been wondering for a while whether we should switch from using @Resource to our own @HttpContext for this type of thing.
Posted by: mhadley on January 31, 2008 at 06:26 AM
-
I have been working on getting improved Spring support in Jersey, see this blog entry
BTW related to my previous common on the exceptions, i can now reproduce it in GF and agree with Marc that we should probably use @HttpContext
Posted by: sandoz on February 01, 2008 at 05:35 AM
|