Yann Moisan

Scala, Web, Linux…

Jersey vs Spring MVC

En Java, il existe de nombreux frameworks pour implémenter un service REST : Spring MVC, Jersey, RESTEasy, Restlet, Play framework, … Le but de ce billet est de comparer Spring MVC et Jersey. Ce n'est pas un tutoriel sur REST, il en existe déjà d'excellents sur la toile1. Il suppose que le lecteur a déjà une connaissance des architectures REST.

Afin d'effectuer la comparaison, j'ai développé le même service avec les deux frameworks. Ce service est accessible en GET, par l'URI /fib/{n} et retourne en JSON les n premiers termes de la suite de Fibonacci.

Introduction

Jersey est l'implémentation de référence de la JSR-311 (JAX-RS). Spring MVC, bien que n'implémentant pas cette JSR, couvre quasimment les mêmes fonctionnalités. J'ai utilisé les dernières versions disponibles, à savoir 1.12 pour Jersey et 3.1.1 pour Spring. Depuis la version 3.0, il est conseillé d'utiliser la nouvelle manière de créer des services en utilisant conjointement @ResponseBody avec HttpMessageConverter2.

Aperçu du code

Regardons à présent ce qui intéresse tous les développpeurs, le code du service.

Jersey

package com.yannmoisan.restcmp;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.yannmoisan.restcmp.service.FibService;

@Controller
@RequestMapping("/fib")
public class FibController {

    @Autowired
    private FibService service;
    
    @RequestMapping(value = "/{n}", method = RequestMethod.GET, produces = "application/json")
    @ResponseBody
    public Fib getFib(@PathVariable String n) {
        return new Fib(service.calculate(Integer.valueOf(n)));
    }

}

Spring

package com.yannmoisan.restcmp;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.yannmoisan.restcmp.service.FibService;

@Component
@Path("/fib/{n}")
public class FibResource {

    @Autowired
    private FibService service;
    
    @GET
    @Produces({ MediaType.APPLICATION_JSON })
    public Fib getFib(@PathParam("n") String n) {
        return new Fib(service.calculate(Integer.valueOf(n)));
    }
}

Le code est assez ressemblant, seul les termes changent. Par exemple, @Path est l'équivalent de @RequestMapping.

Transformation en JSON

Jersey a un module jersey-json pour la transformation en JSON, qui offre 3 approches différents : POJO, JAXB et bas niveau. Dans notre exemple, on utilise l'approche POJO, la plus simple, basée sur jackson. Il suffit simplement d'activer le paramètre POJOMappingFeature sur la servlet.

<init-param>
    <param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
    <param-value>true</param-value>
</init-param>

Avec Spring, il suffit d'indiquer <mvc:annotation-driven /> et d'ajouter la librairie jackson dans le classpath. Spring utilise alors automatiquement un MappingJacksonHttpMessageConverter.

Intégration avec Spring

Dans les 2 cas, nous utilisons Spring pour l'injection de dépendances.

Jersey fournit un module jersey-spring qui contient une servlet com.sun.jersey.spi.spring.container.servlet.SpringServlet qui réalise l'intégration avec Spring.

Avec Spring, bien évidemment, c'est natif.

Tests automatisés

Jersey embarque nativement un framework de test : jersey-test-framework, qui permet de faire des tests avec différents conteneurs.

package com.yannmoisan.restcmp;

import org.junit.Assert;
import org.junit.Test;
import org.springframework.web.context.ContextLoaderListener;

import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.spi.spring.container.servlet.SpringServlet;
import com.sun.jersey.test.framework.JerseyTest;
import com.sun.jersey.test.framework.WebAppDescriptor;

public class FibResourceTest extends JerseyTest {

    public FibResourceTest() throws Exception {
        super(new WebAppDescriptor.Builder("com.sun.jersey.samples.springannotations.resources.jerseymanaged")
                .contextPath("restcmp")
                .contextParam("contextConfigLocation", "classpath:mvc-dispatcher-servlet.xml")
                .servletClass(SpringServlet.class)
                .initParam("com.sun.jersey.api.json.POJOMappingFeature", "true")
                .contextListenerClass(ContextLoaderListener.class).build());
    }

    @Test
    public void testGetFib() {
        WebResource webResource = resource();
        String responseMsg = webResource.path("fib/10").get(String.class);
        Assert.assertEquals("{\"fib\":[1,1,2,3,5,8,13,21,34,55]}", responseMsg);
    }
}

Pour Spring, il faut utiliser un autre projet : spring-test-mvc qui a vocation à être intégré au module spring-test de Spring Framework. Ce projet se base sur l'utilisation de Mock et permet de tester sans conteneur de Servlet. Pour récupérer la dépendance avec Maven, il faut ajouter le repository maven snapshot de Spring.

package com.yannmoisan.restcmp;

import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.server.setup.MockMvcBuilders.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.*;

import org.junit.Test;
import org.springframework.test.web.server.MockMvc;

public class FibControllerTest {

    @Test
    public void testGetFib() throws Exception {
        String contextLoc = "classpath:mvc-dispatcher-servlet.xml";
        String warDir = "src/main/webapp";

        MockMvc mockMvc = xmlConfigSetup(contextLoc).configureWebAppRootDir(warDir, false).build();

        mockMvc.perform(get("/fib/10")).andExpect(content().string("{\"fib\":[1,1,2,3,5,8,13,21,34,55]}"));
    }
}

Tests de bout en bout

Les tests de bout en bout permettent de vérifier que le résulat est identique, avec les 2 webapps déployées sur un serveur tomcat.

$ curl http://localhost:8080/restcmp-jersey/fib/10
{"fib":[1,1,2,3,5,8,13,21,34,55]}
$ curl http://localhost:8080/restcmp-spring/fib/10
{"fib":[1,1,2,3,5,8,13,21,34,55]}

Conclusion

Ma préférence va à Jersey car il est simple, standard et testable.

Ressources externes