Introduction
Ces dernières années, REST est devenu un concept important, influençant la conception des frameworks, protocoles et applications web. Si vous n'y avez pas été confronté, consultez cette brève introduction parmi les nombreuses autres que vous pouvez trouver.
L'importance sans cesse grandissante de REST n'est pas surprenante au vu du besoin, pour les entreprises, d'exposer des APIs web qui devraient être aussi simples et aussi profondément enracinées dans l'architecture du web que possible. Les clients riches communiquant via Ajax ont sensiblement le même besoin. Et il n'y a aucune raison pour que toute application web ne puisse pas bénéficier des principes architecturaux qui sont à l'origine du world wide web.
JAX-RS (JSR-311) est l'API Java pour les Web Services RESTful. JAX-RS a été créé avec la participation de Roy Fielding qui a défini REST dans sa thèse. Il fournit une alternative à JAX-WS (JSR-224), à ceux qui souhaitent créer des web services RESTful. Il existe actuellement 4 implémentations de JAX-RS, toutes pouvant être utilisées avec Spring. Jersey est l'implémentation de référence et c'est celle qui est utilisée dans cet article.
Si vous développez sous Spring, vous vous demandez peut-être (ou on vous a peut-être demandé) comment on peut comparer Spring MVC et JAX-RS. De plus, si vous disposez d'une application Spring MVC qui utilise la hiérarchie de classes de contrôleurs (SimpleFormController et ses amis), vous n'êtes peut-être pas au courant du support complet de REST désormais disponible dans Spring MVC.
Cet article vous guidera à travers les fonctionnalités REST disponibles dans Spring 3, en les comparant avec les fonctionnalités correspondantes de JAX-RS. J'espère que cet exercice vous permettra de comprendre les points communs et les différences entre les deux modèles de programmation.
Avant de commencer, il peut être utile de remarquer que JAX-RS cible le développement de web services (et non d'applications web HTML) alors que Spring MVC a ses racines dans le développement d'applications web. Spring 3 apporte un support REST complet tant pour les applications web que les web services. Néanmoins, cet article se concentrera sur les fonctionnalités liées au développement de web services. Je crois que cela rendra plus simple la discussion de Spring MVC dans un contexte JAX-RS.
Par ailleurs, notons également que les fonctionnalités REST que nous allons évoquer dans cet article font partie intégrante du Spring Framework, et sont un prolongement du modèle de programmation existant de Spring MVC. Ainsi, il n'existe pas de "framework Spring REST", comme on pourrait être tenté de le dire. Il s'agit simplement de Spring et de Spring MVC. Cela signifie concrètement que si vous disposez d'une application Spring, alors vous pourrez utiliser Spring MVC pour créer à la fois une couche web HTML et une couche web services RESTful.
À propos des extraits de code
Les extraits de code exposés tout au long de l'article sont basés sur un domaine simple, composé de deux entités annotées avec JPA, Account et Portfolio, un Account pouvant avoir plusieurs Portfolios. La couche de persistance est configurée avec Spring, et consiste en une implémentation de repository JPA pour la récupération et la persistance d'entités. Jersey et Spring MVC seront utilisés pour créer une couche de web services dont le rôle sera de traiter les requêtes clientes en appelant l'application Spring sous-jacente.
Boostrap et Wiring de la Couche Web
L'injection des dépendances s'appuiera sur Spring, à la fois dans les cas de figure Spring MVC et JAX-RS. La DispatcherServlet Spring MVC et la SpringServlet Jersey délégueront les requêtes entrantes aux composants de la couche REST (contrôleurs ou ressources), eux-mêmes reliés aux composants métiers ou de persistance :
Jersey et Spring MVC s'appuieront tous deux sur le ContextLoaderListener Spring pour charger les composants métiers ou de persistance, comme JpaAccountRepository :
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:META-INF/spring/module-config.xml
</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
Le ContextLoaderListener peut être utilisé dans le contexte de n'importe quel framework web ou REST.
Mise en place des ressources JAX-RS gérées par Spring dans Jersey
Jersey fournit un support pour Spring dans la couche REST. Ce support peut être mis en place en deux étapes simples (en fait trois si vous incluez l'ajout de la dépendance de build sur l'artefact maven com.sun.jersey.contribs:jersey-spring).
Première étape : ajoutez le code suivant à votre web.xml afin de vous assurer que les ressources racines JAX-RS puissent être créées avec Spring :
<servlet>
<servlet-name>Jersey Web Application</servlet-name>
<servlet-class>
com.sun.jersey.spi.spring.container.servlet.SpringServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Jersey Web Application</servlet-name>
<url-pattern>/resources/*</url-pattern>
</servlet-mapping>
Deuxième étape : ajoutez des annotations Spring et JAX-RS aux classes de ressources racines JAX-RS :
@Path("/accounts/")
@Component
@Scope("prototype")
public class AccountResource {
@Context
UriInfo uriInfo;
@Autowired
private AccountRepository accountRepository;
}
Voici une description des annotations :
@Component déclare AccountResource en tant que bean Spring.
@Scope en fait un bean Spring de type prototype, donc instancié à chaque appel (i.e. à chaque requête).
@Autowired demande une référence vers un AccountRepository, qui sera fournie par Spring.
@Path est une annotation JAX-RS qui déclare AccountResource en tant que ressource "racine" JAX-RS.
@Context est également une annotation JAX-RS demandant l'injection d'un objet UriInfo, spécifique à la requête.
JAX-RS définit les notions de ressources "racines" (annotées avec @Path) et de sous-ressources. Dans l'exemple ci-dessus, AccountResource est une ressource racine qui traite les chemins commençant par "/accounts/". Les méthodes de la classe AccountResource comme getAccount() n'ont besoin de spécifier que des chemins relatifs à leur type de ressource :
@Path("/accounts/")
@Component
@Scope("prototype")
public class AccountResource {
@GET
@Path("{username}")
public Account getAccount(@PathParam("username") String username) {
}
}
Le chemin "/accounts/{username}", où username est un paramètre qui peut prendre la valeur de n'importe quel nom d'utilisateur pour un compte donné, sera traité par la méthode getAccount().
Les ressources racines sont instanciées par le runtime JAX-RS (ici Spring). Les sous-ressources, en revanche, sont instanciées par l'application. Par exemple, pour traiter les requêtes vers "/accounts/{username}/portfolios/{portfolioName}", l'AccountResource, sélectionnée grâce au début du chemin ("/accounts"), créera elle-même une instance de sous-ressource vers laquelle la requête sera dirigée :
@Path("/accounts/")
@Component
@Scope("prototype")
public class AccountResource {
@Path("{username}/portfolios/")
public PortfolioResource getPortfolioResource(@PathParam("username") String username) {
return new PortfolioResource(accountRepository, username, uriInfo);
}
}
PortfolioResource elle-même est déclarée sans annotations et toutes ses dépendances lui seront fournies par sa ressource parent :
public class PortfolioResource {
private AccountRepository accountRepository;
private String username;
private UriInfo uriInfo;
public PortfolioResource(AccountRepository accountRepository, String username, UriInfo uriInfo) {
this.accountRepository = accountRepository;
this.username = username;
this.uriInfo = uriInfo;
}
}
Les ressources racines et les sous-ressources, dans JAX-RS, mettent en place une chaîne de traitements impliquant plusieurs ressources :
Gardez à l'esprit que les classes de ressource sont des composants de la couche web service, et qu'ils doivent donc se focaliser sur des traitements spécifiques aux web services, comme la conversion des entrées, la préparation de la réponse, la mise en place du code de réponse, etc. Notons également que la séparation effective des logiques web service et métier implique souvent l'encapsulation de la logique métier au sein d'une seule méthode, généralement transactionnelle.
Mise en place des classes @Controller Spring MVC
Concernant Spring MVC, nous paramétrons la DispatcherServlet avec un paramètre contextConfigLocation pointant sur la configuration Spring MVC :
<servlet>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/*.xml
</param-value>
</init-param>
</servlet>
L'amorce des annotations Spring MVC (@MVC) se fait en une seule petite configuration. L'élément component-scan suivant indique à Spring où trouver les classes annotées par @Controller :
<context:component-scan base-package="org.springframework.samples.stocks" />
Nous pouvons ensuite déclarer la classe AccountController comme suit :
@Controller
@RequestMapping("/accounts")
public class AccountController {
@Autowired
private AccountRepository accountRepository;
}
L'annotation @RequestMapping mappe ce contrôleur à toutes les requêtes commençant par "/accounts". Les méthodes de AccountController comme getAccount() ont seulement besoin de déclarer des chemins relatifs à "/accounts" :
@RequestMapping(value = "/{username}", method = GET)
public Account getAccount(@PathVariable String username) {
}
Spring MVC n'a pas de notions de ressources racines et de sous-ressources. En conséquence, tous les contrôleurs sont gérés par Spring, et jamais par l'application :
@Controller
@RequestMapping("/accounts/{username}/portfolios")
public class PortfolioController {
@Autowired
private AccountRepository accountRepository;
}
Les requêtes vers "/accounts/{username}/portfolios" sont déléguées directement au PortfolioController, sans aucune participation de AccountController. Cette requête pourrait tout aussi bien être traitée directement dans AccountController, éliminant ainsi la nécessité d'un PortfolioController.
Scope de composant dans la couche Web
Dans JAX-RS, AccountResource a été déclarée avec une sémantique de type "scope par requête" (une nouvelle instance d'AccountResource est créée à chaque requête). C'est la recommandation par défaut de JAX-RS. Cela permet l'injection et le stockage des données spécifiques à la requête (headers, paramètres ou valeurs de cookie) dans la classe de ressource elle-même. Cela s'applique aux ressources racines, lesquelles sont gérées par JAX-RS. Les sous-ressources, quant à elles, sont instanciées par l'application et n'en tirent pas profit directement.
Dans Spring MVC, les contrôleurs sont toujours créés en tant que singletons, et reçoivent les données spécifiques à la requête via les arguments des méthodes. JAX-RS propose également ce mécanisme, avec des ressources pouvant être créées sous forme de singletons.
Mapping des requêtes aux méthodes
Nous allons ci-dessous examiner la manière dont Spring MVC et JAX-RS mappent les requêtes aux méthodes. @Path et @RequestMapping supportent toutes deux l'extraction de variables de chemin à partir de l'URL :
@Path("/accounts/{username}")
@RequestMapping("/accounts/{username}")
Les deux frameworks permettent également l'usage d'expressions régulières pour l'extraction de variables de chemin :
@Path("/accounts/{username:.*}")
@RequestMapping("/accounts/{username:.*}"
L'annotation @RequestMapping de Spring MVC permet de mapper des requêtes en fonction de la présence ou l'absence de paramètres :
@RequestMapping(parameters="foo")
@RequestMapping(parameters="!foo")
... ou en fonction de la valeur de paramètres :
@RequestMapping(parameters="foo=123")
Similairement, @RequestMapping permet de mapper des requêtes en fonction de la présence ou l'absence de headers :
@RequestMapping(headers="Foo-Header")
@RequestMapping(headers="!Foo-Header")
... ou en fonction de la valeur de headers :
@RequestMapping(headers="content-type=text/*")
Manipulation des données de requête
Les requêtes HTTP contiennent des données que les applications ont besoin d'extraire et d'utiliser. Ces données incluent des headers HTTP, des cookies, des paramètres de requête, des paramètres de formulaire, ou encore de plus amples informations envoyées dans le corps de la requête (XML, JSON, etc...). Dans les applications RESTful, l'URL elle-même peut contenir des informations clés, telles que le nom de la ressource demandée (spécifié dans les paramètres de chemin), ou le type de contenu (spécifié dans l'extension de fichier, comme .html, .pdf). HttpServletRequest fournit bien un accès à toutes ces informations, mais cet accès reste de bas niveau, et relativement verbeux.
Paramètres de requête, cookies et headers HTTP
Spring MVC et JAX-RS disposent d'annotations pour l'extraction des valeurs de la requête HTTP :
@GET @Path
public void foo(@QueryParam("q") String q, @FormParam("f") String f, @CookieParam("c") String c,
@HeaderParam("h") String h, @MatrixParam("m") m) {
// JAX-RS
}
@RequestMapping(method=GET)
public void foo(@RequestParam("q") String q, @CookieValue("c") String c, @RequestHeader("h") String h) {
// Spring MVC
}
Les annotations ci-dessus sont assez semblables, mis à part le fait que JAX-RS supporte l'extraction de paramètres matriciels et dispose d'annotations distinctes pour les paramètres querystring et les paramètres de formulaire. Les paramètres matriciels sont rarement rencontrés. Ils sont similaires aux paramètres querystring, mais appliqués à des segments de chemin spécifiques (ex: GET /images;name=foo;type=gi). Les paramètres de formulaire seront traités sous peu.
JAX-RS permet de positionner les annotations ci-dessus à la fois sur des champs et des setters, tant que la ressource a été déclarée avec un scope de requête.
Spring MVC possède une fonctionnalité, diminuant la verbosité, qui permet d'omettre les noms des champs dans les annotations ci-dessus, à condition que ces noms correspondent aux noms des arguments Java. Par exemple, un paramètre de requête de nom "q" exige que l'argument de la méthode s'appelle "q" :
public void foo(@RequestParam String q, @CookieValue c, @RequestHeader h) {
}
Ceci est plutôt pratique pour les signatures des méthodes qui ont tendance à s'allonger à cause de l'utilisation d'annotations sur chaque argument. Gardez également à l'esprit que cette fonctionnalité requiert que ce code soit compilé avec des instructions de débogage ("debugging symbols").
Conversion de type et formatage des valeurs de requête HTTP
Les valeurs de requête HTTP (headers, cookies, paramètres) sont invariablement de type String, ce qui nécessite une étape de parsing.
JAX-RS traite le parsing des données de requête en recherchant, dans le type de classe cible, une méthode valueOf() ou un constructeur acceptant une String. Pour être précis, JAX-RS supporte les types suivants en tant qu'arguments annotés de méthode, pour les variables de chemin, paramètres de requête, valeurs de header HTTP et cookies :
- Types primitifs.
- Types possédant un constructeur n'acceptant qu'un seul argument de type String.
- Types possédant une méthode static, de nom valueOf, n'acceptant qu'un seul argument de type String.
- List<T>, Set<T> ou SortedSet<T> avec T satisfaisant l'une des conditions 2 ou 3 énoncées au dessus.
Ce traitement est également implémenté dans Spring 3. Par ailleurs, Spring 3 fournit un mécanisme alternatif de conversion de type et de formatage pouvant être configuré par annotations.
Données de formulaire
Comme mentionné ci-dessus, JAX-RS fait la distinction entre les paramètres de requête et les paramètres de formulaire. Bien que Spring MVC ne dispose que de l'annotation @RequestParam, il fournit également un mécanisme de data binding, bien connu des utilisateurs Spring MVC, permettant de traiter les données transmises.
Par exemple, pour traiter un formulaire soumettant 3 éléments, nous pouvons déclarer une méthode à 3 arguments :
@RequestMapping(method=POST)
public void foo(@RequestParam String name, @RequestParam creditCardNumber, @RequestParam expirationDate) {
Credit card = new CreditCard();
card.setName(name);
card.setCreditCardNumber(creditCardNumber);
card.setExpirationDate(expirationDate);
}
Toutefois, cette approche devient impraticable à mesure que le formulaire s'agrandit. Avec le data binding, un objet de formulaire de structure arbitraire composée de données imbriquées (adresse de facturation, adresse mail, etc...) peut être créé, alimenté et passé en tant qu'argument par Spring MVC :
@RequestMapping(method=POST)
public void foo(CreditCard creditCard) {
// POST /creditcard/1
// name=Bond
// creditCardNumber=1234123412341234
// expiration=12-12-2012
}
Le traitement de formulaire est important dans le cadre de la collaboration avec les navigateurs web. Les clients web service, quant à eux, sont davantage susceptibles de soumettre des données au format XML ou JSON dans le corps de la requête.
Traitement des données du corps de la requête
Spring MVC et JAX-RS automatisent tous deux le traitement des données contenues dans le corps de la requête :
@POST
public Response createAccount(Account account) {
// JAX_RS
}
@RequestMapping(method=POST)
public void createAccount(@RequestBody Account account) {
// Spring MVC
}
Données du corps de la requête dans JAX-RS
Dans JAX-RS, les classes implémentant MessageBodyReader assurent la conversion des données du corps de la requête. Les implémentations JAX-RS doivent disposer d'un MessageBodyReader de type JAXB. Des implémentations personnalisées de MessageBodyReader, annotées avec @Provider, peuvent également être fournies.
Données du corps de la requête dans Spring MVC
Dans Spring MVC, vous devez annoter un argument de méthode avec @RequestBody si vous souhaitez qu'il soit initialisé avec les données du corps de la requête. Cela contraste avec l'initialisation depuis les paramètres de formulaire évoquée ci-dessus.
Dans Spring MVC, les classes de type HttpMessageConverter assurent la conversion des données du corps de la requête. Un marshaller HttpMessageConverter Spring OXM est fourni par défaut par le framework. Il permet l'utilisation de JAXB, Castor, JiBX, XMLBeans ou XStream. Il existe également un HttpMessageConverter Jackson pour le format JSON.
Les types d'HttpMessageConverter sont spécifiés grâce à l'AnnotationMethodHandlerAdapter, qui adapte les requêtes entrantes vers les @Controllers Spring MVC. Voici la configuration :
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" >
<property name="messageConverters" ref="marshallingConverter"/>
</bean>
<bean id="marshallingConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
<constructor-arg ref="jaxb2Marshaller"/>
<property name="supportedMediaTypes" value="application/vnd.stocks+xml"/>
</bean>
<oxm:jaxb2-marshaller id="jaxb2Marshaller"/>
Le schéma suivant illustre cette configuration :
Le nouveau "namespace mvc" de Spring 3 automatise la configuration ci-dessus. Voici tout ce que vous avez à faire :
<mvc:annotation-driven />
Un converter pour la lecture/écriture du XML sera automatiquement enregistré si JAXB est trouvé dans le classpath, et similairement pour un converter JSON, avec Jackson dans le classpath.
Préparation de la réponse
Une réponse typique peut impliquer le choix du code de réponse, la définition de headers HTTP, l'ajout de données dans le corps de la réponse, ainsi que le traitement des exceptions.
Ajout de données dans le corps de la réponse avec JAX-RS
Pour ajouter des données dans le corps de la réponse avec JAX-RS, vous renvoyez simplement l'objet à partir de la méthode de la ressource :
@GET
@Path("{username}")
public Account getAccount(@PathParam("username") String username) {
return accountRepository.findAccountByUsername(username);
}
JAX-RS trouvera un MessageBodyWriter capable de convertir l'objet vers le type de contenu demandé.
Ajout de données dans le corps de la réponse avec Spring MVC
En Spring MVC, la réponse est préparée par un processus de résolution de vue. Cela permet un choix parmi un éventail de technologies de vue. Cependant, lorsque vous travaillez avec des clients web service, il peut être plus naturel d'ignorer le processus de résolution de vue et d'utiliser l'objet retourné par la méthode :
@RequestMapping(value="/{username}", method=GET)
public @ResponseBody Account getAccount(@PathVariable String username) {
return accountRepository.findAccountByUsername(username);
}
Lorsqu'une méthode de contrôleur (ou son type de retour) est annotée avec @ResponseBody, sa valeur de retour est traitée par un HttpMessageConverter, puis utilisée pour constituer le corps de la réponse. L'ensemble des classes HttpMessageConverter utilisé pour les arguments du corps de la requête est le même que celui utilisé pour le corps de la réponse. Ainsi, aucune configuration supplémentaire n'est nécessaire.
Codes de statuts & Headers de réponse
JAX-RS fournit une API "chaînée" pour la construction de la réponse :
@PUT @Path("{username}")
public Response updateAccount(Account account) {
// ...
return Response.noContent().build(); // 204 (No Content)
}
De plus, il est possible d'utiliser un UriBuilder pour indiquer des liens vers des entités dans le header Location de la réponse :
@POST
public Response createAccount(Account account) {
// ...
URI accountLocation = uriInfo.getAbsolutePathBuilder().path(account.getUsername()).build();
return Response.created(accountLocation).build();
}
L'uriInfo utilisé ci-dessus est soit injecté au niveau des ressources racines (avec l'annotation @Context), soit tiré des classes parentes pour les sous-ressources. Cela permet la concaténation de données dans le chemin de la requête courante.
Spring MVC fournit une annotation pour la définition du code de la réponse :
@RequestMapping(method=PUT)
@ResponseStatus(HttpStatus.NO_CONTENT)
public void updateAccount(@RequestBody Account account) {
// ...
}
La création d'un header Location s'effectue directement par les objets HttpServletResponse :
@RequestMapping(method=POST)
@ResponseStatus(CREATED)
public void createAccount(@RequestBody Account account, HttpServletRequest request,
HttpServletResponse response) {
// ...
String requestUrl = request.getRequestURL().toString();
URI uri = new UriTemplate("{requestUrl}/{username}").expand(requestUrl, account.getUsername());
response.setHeader("Location", uri.toASCIIString());
}
Traitement des exceptions
JAX-RS permet aux méthodes de ressource de lancer des exceptions de type WebApplicationException, contenant un objet de type Response. L'exemple de code suivant convertit une NoResultException JPA en une NotFoundException spécifique à Jersey, ce qui provoque l'envoi d'une erreur 404 :
@GET
@Path("{username}")
public Account getAccount(@PathParam("username") String username) {
try {
return accountRepository.findAccountByUsername(username);
} catch (NoResultException e) {
throw new NotFoundException();
}
}
Les instances de WebApplicationException encapsulent la logique nécessaire à la production d'une réponse spécifique, mais les exceptions doivent être capturées individuellement dans chaque méthode de classe de ressource.
En Spring MVC, le traitement des exceptions s'effectue au niveau de méthodes du contrôleur :
@Controller
@RequestMapping("/accounts")
public class AccountController {
@ResponseStatus(NOT_FOUND)
@ExceptionHandler({NoResultException.class})
public void handle() {
// ...
}
}
Si une quelconque méthode lance une exception JPA de type NoResultException, la méthode ci-dessus sera appelée pour la traiter et retourner une erreur 404. Cela permet à chaque contrôleur de traiter les exceptions de manière plus souple, depuis un endroit unique.
Résumé
J'espère que cet article vous a permis de comprendre comment Spring MVC peut être utilisé pour la mise en oeuvre de web services RESTful et comment on peut le comparer au modèle de programmation JAX-RS.
Si vous utilisez Spring MVC, alors vous développez probablement des applications web HTML. Les concepts REST s'appliquent indifféremment aux web services et aux applications web, particulièrement lors des interactions avec des clients riches. En plus des fonctionnalités discutées dans cet article, Spring 3 propose des fonctionnalités spécifiques aux applications web RESTful. En voici une liste partielle : un nouveau tag JSP pour la construction d'URLs à partir de templates, un filtre de servlet pour la simulation de soumission de formulaire en PUT et en DELETE, un ContentTypeNegotiatingViewResolver pour automatiser le choix de la vue en se basant sur le type de contenu, de nouvelles implémentations de vues, et plus encore. Dernier point, mais non des moindres, la mise à jour de la documentation de Spring, et une amélioration notable de sa qualité.
À propos de l'auteur
Rossen Stoyanchev est consultant senior chez SpringSource. Dans sa carrière, il a participé à plusieurs projets dont une application de trading, un système de comptabilité d'investissements, une application web e-commerce. Son activité au sein de SpringSource est axée sur les technologies web, notamment le consulting, la formation et le développement du cours "Rich-Web Development With Spring", lequel permet aux participants d'obtenir la certification Spring Web Application.