I. Introduction▲
L'objet de cet article est de présenter les différentes options qui permettent d'exposer des services REST à l'aide de Spring MVC 3.2.
Spring propose deux approches pour exposer des services REST.
- La première est basée sur les principes de fonctionnement du framework MVC de Spring, à savoir l'utilisation du ContentNegotiatingViewResolver.
- La seconde, plus récente, fait appel aux HttpMessageConverters et à l'annotation @ResponseBody.
II. Méthode 1 : utiliser le ContentNegotiatingViewResolver▲
L'utilisation du ContentNegotiatingViewResolver pour exposer des API REST a été la première approche proposée, car elle s'appuie sur le mécanisme de Spring MVC existant.
Pour rappel, SpringMVC permet de définir des ViewResolver qui permettent d'expliquer la manière dont seront affichées les données retournées par le Controller MVC.
La déclaration de la servlet Spring dans le XML est nécessaire :
<web-app>
<servlet>
<servlet-name>
dispatchServlet</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>
disptachServlet</servlet-name>
<url-pattern>
/rest/*</url-pattern>
</servlet-mapping>
</web-app>
Dans le cas classique d'affichage d'une JSP, on utilise l'InternalViewResolver (qui permet d'appeler une JSP pour afficher les données) que l'on déclare dans la configuration Spring :
<bean
class
=
"org.springframework.web.servlet.view.InternalResourceViewResolver"
>
<property
name
=
"prefix"
value
=
"/WEB-INF/views/"
/>
<property
name
=
"suffix"
value
=
".jsp"
/>
</bean>
Note : la servlet de Spring (DispatcherServlet) référence les différents ViewResolver en parcourant les beans définis dans la configuration Spring.
Lorsque vous utilisez le ContentNegotiatingViewResolver, votre controller web retourne un ModelView ou le nom d'une vue, et selon certains critères paramétrables, le ContentNegotiatingViewResolver va choisir la vue à utiliser pour retourner le résultat :
@Controller
@RequestMapping
(
value=
"/todo"
)
public
class
TodoRestService2 {
@Autowired
private
TodoService todoService;
@RequestMapping
(
value=
"/{id}"
, method=
RequestMethod.GET)
public
ModelAndView find
(
@PathVariable
(
"id"
) Long id) {
Todo todo =
todoService.find
(
id);
return
new
ModelAndView
(
"todo"
, "todo"
, todo);
}
}
Pour l'exposition de service REST, Spring propose différentes implémentations de ces vues pour les formats de contenus les plus courants :
- org.springframework.web.servlet.view.json.MappingJacksonJsonView : format de type JSON ;
- org.springframework.web.servlet.view.xml.MarshallingView : format de type XML ;
- org.springframework.web.servlet.view.documentClass.AbstractPdfView : format de type PDF ;
- etc.
Il est bien sur possible de créer des implémentations spécifiques ou d'étendre celles existant.
Les critères qui permettent au ContentNegotiatingViewResolver de choisir la bonne vue sont définis à l'aide de stratégies. Ces stratégies permettent de définir un lien entre l'URL appelée par le client et le format de la réponse qui sera retournée. La vue qui correspond au format sera alors appelée.
Spring fournit principalement les implémentations suivantes :
- org.springframework.web.accept.PathExtensionContentNegotiationStrategy : stratégie basée sur l'extension de l'URL (.json, .xml, .html, etc) ;
- org.springframework.web.accept.ParameterContentNegotiationStrategy : stratégie basée sur le passage d'un paramètre dans la requête ;
- org.springframework.web.accept.HeaderContentNegotiationStrategy : stratégie basée sur l'analyse sur l'attribut « accept » du header de la requête.
Prenons l'exemple d'un service RESTFul qui permettrait de retourner le contenu d'un bean Java en XML ou en JSON en fonction de l'extension de l'URL.
Ci-dessous la configuration qui permet de le faire :
<bean
class
=
"org.springframework.web.servlet.view.ContentNegotiatingViewResolver"
>
<property
name
=
"contentNegotiationManager"
>
<bean
class
=
"org.springframework.web.accept.ContentNegotiationManager"
>
<constructor-arg>
<bean
class
=
"org.springframework.web.accept.PathExtensionContentNegotiationStrategy"
>
<constructor-arg>
<map>
<entry
key
=
"json"
value
=
"application/json"
/>
<entry
key
=
"xml"
value
=
"application/xml"
/>
</map>
</constructor-arg>
</bean>
</constructor-arg>
</bean>
</property>
<property
name
=
"defaultViews"
>
<list>
<!-- Renders JSON View -->
<bean
class
=
"org.springframework.web.servlet.view.json.MappingJacksonJsonView"
/>
<!-- Renders XML View -->
<bean
class
=
"org.springframework.web.servlet.view.xml.MarshallingView"
>
<constructor-arg>
<bean
class
=
"org.springframework.oxm.jaxb.Jaxb2Marshaller"
>
<property
name
=
"packagesToScan"
>
<list>
<value>
org.demo.domain</value>
</list>
</property>
</bean>
</constructor-arg>
</bean>
</list>
</property>
</bean>
Dans cet exemple, la stratégie PathExtensionContentNegotiationStrategy permet de définir qu'un appel vers une URL finissant par .json sera dirigé vers la vue MappingJacksonJsonView, car elle est capable de générer du contenu de type application/json.
Même chose avec les URL finissant par .xml et la vue MarshallingView.
III. Méthode 2 : utiliser les HttpMessageConverters▲
Pour répondre de manière plus spécifique aux problèmes d'exposition de service REST, Spring propose une nouvelle approche depuis la version 3 : l'utilisation de l'annotation @RequestBodyqui permet l'usage systématique d'un HttpMessageConverters.
Qu'est-ce que cela change ?
Au lieu de retourner un ModelView (ou le nom d'une vue), la méthode retourne directement le contenu à retourner, ce qui se traduit par un objet ou une collection d'objets. Ensuite, le HttpMessageConverters se charge de convertir ces objets vers le flux de données attendu par le client.
L'activation des HttpMessageConverter est réalisée en déclarant la ligne suivante dans la configuration Spring :
<
mvc
:
annotation-driven/>
Cette déclaration permet de charger les HttpMessageConverter par défaut fournis par Spring :
StringHttpMessageConverter |
Lit/écrit un String à partir d'une requête ou d'une réponse. Par défaut, il répond aux médias de type « text/* » et retourne un contenu de type « text/plain ». |
FormHttpMessageConverter |
Lit/écrit un Data Form à partir d'une requête ou d'une réponse. Par défaut, il répond aux médias de type « application/x-www-form-urlencoded » et retourne un contenu à l'aide d'un MultiValueMap<String,String>. |
MarshallingHttpMessageConverter |
Lit/écrit des données XML en utilisant les marshaller/unmarshaller fournis par Spring. Par défaut, il retourne un contenu de type « application/xml ». |
MappingJacksonHttpMessageConverter |
Lit/écrit des données JSON en utilisant la librairie Jackson's en tant qu'ObjectMapper. Il convertit des données de type « application/json ». |
AtomFeedHttpMessageConverter |
Lit/écrit des flux de données au format ATOM à l'aide de la librairie ROME. Il convertit des données de type « application/atom+xml ». |
RssChannelHttpMessageConverter |
Lit/écrit des flux ATOM à l'aide de la librairie ROME. Il convertit des données de type « application/rss+xml ». |
Si vous souhaitez déclarer vos propres HttpMessageConverter, il faut le faire ainsi :
<
mvc
:
annotation-driven>
<
mvc
:
message-converters
register-defaults
=
"false"
>
<ref
bean
=
"jsonHttpMessageConverter"
/>
</
mvc
:
message-converters>
</
mvc
:
annotation-driven>
<bean
class
=
"org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"
>
<property
name
=
"prefixJson"
value
=
"false"
/>
<property
name
=
"supportedMediaTypes"
value
=
"application/json"
/>
</bean>
À noter que l'attribut register-defaults permet de définir si les HttpMessageConverter par défaut sont également déclarés ou pas.
Si l'on reprend l'exemple d'un service REST qui permettrait de retourner le contenu d'un bean Java en XML ou en JSON en fonction de l'extension de l'URL, tout le déclaratif est réalisé au niveau du controller :
@Controller
@RequestMapping
(
value=
"/todo"
)
public
class
TodoRestServiceWithHTTPConverter {
@Autowired
private
TodoService todoService;
/*
* Return always Json data
*/
@RequestMapping
(
value=
"/{id}.json"
, method=
RequestMethod.GET,
produces=
"application/json"
)
@ResponseBody
public
Todo findAsJson
(
@PathVariable
(
"id"
) Long id) {
return
todoService.find
(
id);
}
/*
* Return always XML data
*/
@RequestMapping
(
value=
"/{id}.xml"
, method=
RequestMethod.GET,
produces=
"application/xml"
)
@ResponseBody
public
Todo findAsXML
(
@PathVariable
(
"id"
) Long id) {
return
todoService.find
(
id);
}
}
L'attribut produces permet de définir le format de sortie et de faire le lien avec le HttpMessageConverter compatible.
IV. Conclusion▲
Nous avons vu deux méthodes pour exposer des services REST avec Spring. La mise en œuvre a été réalisée sur un POC disponible sur GitHub à l'adresse suivante :
V. Remerciements▲
Cet article a été publié avec l'aimable autorisation de Netapsys.
Nous tenons à remercier Malick SECK pour la relecture orthographique attentive de cet article et Régis Pouiller pour la mise au gabarit.