REST avec Spring 3.0 et Solr (3ème partie)

Voici la dernière partie de notre série d’articles sur le support REST de Spring 3.0 afin d’encapsuler des appels à un serveur Solr. Nous avons dans les précédentes parties configuré un serveur Solr et implémenté un contrôleur REST avec Spring 3.0 pour attaquer Solr avec l’API SolrJ. Cette dernière partie couvre comment utiliser le RestTemplate de Spring 3.0 pour consulter le contrôleur REST mais propose aussi quelques améliorations RESTful pour ce dernier.

Utilisation basique du RestTemplate

Le RestTemplate correspond à la partie cliente du support REST de Spring : il permet d’accéder à des Web Services REST (implémentés avec Spring ou non). REST est basé sur HTTP, donc pourquoi ne pas utiliser les nombreuses bibliothèques HTTP déjà existantes en Java (dans le JDK, Commons HTTP, etc.) ? Car comme d’autres API d’accès aux données (JDBC, JMS…), ces API sont bas niveau et nécessitent du code technique fastidieux, qui nuit à la productivité et à la robustesse. Le RestTemplate se propose donc de gérer une bonne partie de la « plomberie » (gestion des connexions HTTP, conversion des réponses en objets Java), tout en restant très paramétrable pour chaque aspect (possibilité d’utiliser le support HTTP du JDK ou Commons HTTP en interne, branchement sur différents frameworks de binding pour gérer des formats XML, JSON). Bref, le RestTemplate suit la philosophie des templates d’accès aux données de Spring, afin de pouvoir se concentrer sur le code fonctionnel.
Voyons immédiatement comment utiliser le RestTemplate pour effectuer la requête avec le mot clé ‘solr’ (requête effectuée dans la deuxième partie avec un navigateur Web) :

  1. RestTemplate restTemplate = new RestTemplate();
  2. String res = restTemplate.getForObject(
  3. « http://localhost:8080/springsolr/catalogue/search/{query} »,
  4. String.class, « solr »
  5. );

La variable res contient alors le code suivant (indenté pour une meilleure lisibilité) :

  1. <catalogue>
  2. <products>
  3. <product>
  4. <id>SOLR1000</id>
  5. <name>Solr, the Enterprise Search Server</name>
  6. <manufacturer>Apache Software Foundation</manufacturer>
  7. </product>
  8. </products>
  9. </catalogue>

Voici les éléments à retenir de cette première utilisation du RestTemplate :

  • il dispose de méthodes pour les différentes opérations HTTP (dans notre cas, GET, mais il y a aussi POST, PUT, DELETE, etc)
  • comme les contrôleurs REST Spring MVC, il accepte les « URI templates » (ex. : http://localhost:8080/springsolr/catalogue/search/{query}), qui contiennent des paramètres avec la syntaxe {param}.
  • les paramètres des URI templates peuvent être passés via des varargs mais aussi avec une Map (pas de problème d’ordre alors)
  • on peut préciser le type de retour souhaité, le RestTemplate effectuant alors la conversion de la réponse (nous verrons comment paramétrer les conversions par la suite)

Nous avons fait une première opération GET, voyons maintenant comment effectuer un POST, ce qui correspond à de l’indexation du coté de notre contrôleur REST :

  1. restTemplate.postForLocation(
  2. null, // pas d’objet dans le corps de la requête, tout est dans l’URL
  3. « sdmia »,« Spring Dynamic Modules in action »,« Cogoluegnes-Piper-Templier »
  4. );
  5. // requête pour vérifier que l’indexation a bien eu lieu
  6. String res = restTemplate.getForObject(
  7. « http://localhost:8080/springsolr/catalogue/search/{query} »,
  8. String.class, « spring dynamic modules »
  9. );
  10. Assert.assertTrue(res.contains(« Spring Dynamic Modules in action »));

Le contrôleur REST retourne du XML, ce qui donne lui donne une bonne interopérabilité mais, coté client, il n’est pas pratique de récupérer directement ce XML sous la forme d’une chaîne de caractère. L’idéal serait de pouvoir récupérer un objet métier Java, le RestTemplate se chargeant de la conversion. Nous allons voir comment faire cela immédiatement.

Conversion avec le HttpMessageConverter

Le RestTemplate maintient une liste de HttpMessageConverters qui lui permet d’effectuer des conversions entre Java et HTTP. Les conversions vont dans les deux sens : objet Java vers requête HTTP et aussi réponse HTTP vers objet Java (ce qui nous intéresse dans l’immédiat). Par défaut, le RestTemplate dispose de HttpMessageConverter effectuant des conversions simples (String, tableau de byte) mais aussi plus complexes (données de formulaire sous forme de MultiValueMap, Source XML). Nous allons voir comment écrire un HttpMessageConverter effectuant des conversions vers nos deux objets métier (Catalogue et Product), puis comment enregistrer ce convertisseur programmatiquement et déclarativement.

Ecrire un HttpMessageConverter

L’implémentation de HttpMessageConverter utilise XStream pour la conversion XML/Java, cela dans les deux sens. Il s’agit finalement du même mécanisme de sérialisation que coté serveur.

  1. package com.zenika.springsolr.web;
  2. import java.io.IOException;
  3. import java.util.Collections;
  4. import java.util.List;
  5. import org.springframework.http.HttpInputMessage;
  6. import org.springframework.http.HttpOutputMessage;
  7. import org.springframework.http.MediaType;
  8. import org.springframework.http.converter.HttpMessageConverter;
  9. import org.springframework.http.converter.HttpMessageNotReadableException;
  10. import org.springframework.http.converter.HttpMessageNotWritableException;
  11. import com.thoughtworks.xstream.XStream;
  12. i mport com.zenika.springsolr.domain.Catalogue;
  13. import com.zenika.springsolr.domain.Product;
  14. public class CatalogueHttpMessageConverter implements HttpMessageConverter<Catalogue> {
  15. private XStream xstream = new XStream();
  16. public CatalogueHttpMessageConverter() {
  17. xstream.alias(« catalogue », Catalogue.class);
  18. xstream.alias(« product », Product.class);
  19. }
  20. @Override
  21. public boolean canRead(Class<? extends Catalogue> clazz, MediaType mediaType) {
  22. return Catalogue.class.equals(clazz);
  23. }
  24. @Override
  25. public boolean canWrite(Class<? extends Catalogue> clazz,
  26. MediaType mediaType) {
  27. return Catalogue.class.equals(clazz);
  28. }
  29. @Override
  30. public Catalogue read(Class<Catalogue> clazz, HttpInputMessage inputMessage)
  31. throws IOException, HttpMessageNotReadableException {
  32. return (Catalogue) xstream.fromXML(inputMessage.getBody());
  33. }
  34. @Override
  35. public void write(Catalogue t, MediaType contentType,
  36. HttpOutputMessage outputMessage) throws IOException,
  37. HttpMessageNotWritableException {
  38. xstream.toXML(t, outputMessage.getBody());
  39. }
  40. @Override
  41. public List<MediaType> getSupportedMediaTypes() {
  42. return Collections.singletonList(new MediaType(« application »,« xml »));
  43. }
  44. }

Enregistrement programmatique du convertisseur

On peut configurer le RestTemplate et ses HttpMessageConverters de façon 100% programmatique :

  1. RestTemplate restTemplate = new RestTemplate();
  2. List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
  3. converters.add(new CatalogueHttpMessageConverter());
  4. restTemplate.setMessageConverters(converters);

Enregistrement déclaratif du convertisseur

On peut aussi compter sur Spring pour configurer le RestTemplate et ses HttpMessageConverters afin de pouvoir l’injecter ensuite :

  1. <bean id=« restTemplate » class=« org.springframework.web.client.RestTemplate »>
  2. <property name=« messageConverters »>
  3. <list>
  4. <bean class=« com.zenika.springsolr.web.CatalogueHttpMessageConverter » />
  5. </list>
  6. </property>
  7. </bean>

Attention ! Notre liste de HttpMessageConverters remplace celle par défaut : toutes les conversions correspondantes ne fonctionnent alors plus (String, byte, etc).

Utilisation du convertisseur

Le convertisseur est ensuite automatiquement utilisé par le RestTemplate, qui délègue les conversions à ses convertisseurs selon leurs capacités (méthodes canRead et canWrite) :

  1. Catalogue catalogue = restTemplate.getForObject(
  2. « http://localhost:8080/springsolr/catalogue/search/{query} »,
  3. Catalogue.class, « solr »
  4. );

L’écriture du CatalogueHttpMessageConverter a permis de se familiariser avec l’API HttpMessageConverter et de découvrir quelques mécaniques du RestTemplate. Mais ne serait-il pas plus simple d’utiliser des convertisseurs existants ? Spring propose plusieurs implémentations ré-utilisables (pour gérer du XML avec différents frameworks de binding, du JSON avec Jackson), voyons comment utiliser celle se basant sur Spring OXM.

Utilisation de Spring OXM pour la conversion

Plutôt que d’écrire un HttpMessageConverter sur mesure comme nous avons fait, il est possible d’obtenir le même résultat en effectuant seulement de la configuration, grâce au MarshallingHttpMessageConverter, qui se base sur Spring OXM. La configuration se fait en deux étapes :

  • déclarer un Marshaller (nous allons utiliser le même que celui générant la vue coté serveur)
  • injecter un MarshallingHttpMessageConverter dans le RestTemplate
  1. <bean id=« marshaller » class=« org.springframework.oxm.xstream.XStreamMarshaller »>
  2. <property name=« aliases »>
  3. <map>
  4. <entry key=« catalogue » value=« com.zenika.springsolr.domain.Catalogue » />
  5. <entry key=« product » value=« com.zenika.springsolr.domain.Product » />
  6. </map>
  7. </property>
  8. </bean>
  9. <bean id=« restTemplate » class=« org.springframework.web.client.RestTemplate »>
  10. <property name=« messageConverters »>
  11. <list>
  12. <bean class=« org.springframework.http.converter.xml.MarshallingHttpMessageConverter »>
  13. <property name=« marshaller » ref=« marshaller » />
  14. <property name=« unmarshaller » ref=« marshaller » />
  15. </bean>
  16. </list>
  17. </property>
  18. </bean>

Il est ensuite possible de faire le même appel qu’avec le CatalogueHttpMessageConverter, mais cela sans développer de classe spécifique :

  1. Catalogue catalogue = restTemplate.getForObject(
  2. « http://localhost:8080/springsolr/catalogue/search/{query} »,
  3. Catalogue.class, « solr »
  4. );

Le HttpMessageConverter est donc un point d’extension intéressant, introduit dans Spring 3.0, mais il ne se limite au RestTemplate, puisqu’on peut aussi l’utiliser dans Spring MVC.

Utilisation de Spring OXM coté serveur

Le HttpMessageConverter a été très utile pour les conversions coté client avec le RestTemplate, mais sa définition est très générique :

Strategy interface that specifies a converter that can convert from and to HTTP requests and responses.

Cela est donc directement applicable à des contrôleurs Web, puisque ceux-ci doivent potentiellement aussi convertir des requêtes HTTP en objets Java et des objets Java en réponse HTTP.

Déclaration d’un HttpMessageConverter dans Spring MVC

Spring MVC utilise des HandlerAdapters à qui il délègue les appels sur les contrôleurs Web applicatifs. Depuis Spring 2.5, Spring MVC privilégie une approche 100% annotation et donc un AnnotationMethodHandlerAdapter est automatiquement positionné dans le contexte Spring d’une DispatcherServlet. C’est pour cela que l’on peut directement utiliser des contrôleurs annotés avec RequestMapping, sans configuration spécifique. Il est possible de positionner des HttpMessageConverters sur un AnnotationMethodHandlerAdapter, donc nous allons en déclarer un qui remplacera celui par défaut.
Voilà pour la théorie, en pratique, il suffit d’ajouter le code suivant dans le contexte Spring de la DispatcherServlet :

  1. <bean class=« org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter »>
  2. <property name=« messageConverters »>
  3. <list>
  4. <bean class=« org.springframework.http.converter.xml.MarshallingHttpMessageConverter »>
  5. <property name=« marshaller » ref=« marshaller » />
  6. <property name=« unmarshaller » ref=« marshaller » />
  7. <property name=« supportedMediaTypes »>
  8. <list>
  9. <value>application/xml</value>
  10. <value>text/*</value>
  11. </list>
  12. </property>
  13. </bean>
  14. </list>
  15. </property>
  16. </bean>

Voyons maintenant comment tirer parti du MarshallingHttpMessageConverter dans le contrôleur REST.

Conversion des paramètres de la requête

Rappelez-vous comment notre requête POST permet d’indexer un nouveau produit à partir de l’URL :

  1. @RequestMapping(value = « /index/{id}/{name}/{manu} », method = RequestMethod.POST)
  2. public void index(@PathVariable String id, @PathVariable String name,@PathVariable String manu,
  3. HttpServletResponse response)
  4. throws Exception {
  5. }

Il est possible de récupérer directement un objet Product à partir du corps de la requête, en utilisant l’annotation RequestBody. Nous pouvons ajouter une méthode index qui répond aux requêtes PUT :

  1. @RequestMapping(value=« /index/ »,method=RequestMethod.PUT)
  2. public void index(@RequestBody Product product, HttpServletResponse response) throws Exception {
  3. SolrInputDocument doc = new SolrInputDocument();
  4. doc.addField(« id », product.getId());
  5. doc.addField(« name », product.getName());
  6. doc.addField(« manu », product.getManufacturer());
  7. solrServer.add(doc);
  8. solrServer.commit();
  9. response.setStatus(HttpServletResponse.SC_OK);
  10. }

Coté client, nous pouvons effectuer une requête de la manière suivante (en supposant que le RestTemplate utilise bien un convertisseur adapté, comme nous l’avons configuré précédemment) :

  1. Product product = new Product(« sia3 »,« Spring in action 3rd edition »,« Walls »);
  2. restTemplate.put(« http://localhost:8080/springsolr/catalogue/index/ », product);
  3. // requête pour vérifier que l’indexation a bien eu lieu
  4. Catalogue catalogue = restTemplate.getForObject(
  5. « http://localhost:8080/springsolr/catalogue/search/{query} »,
  6. Catalogue.class, « walls »
  7. );
  8. Assert.assertTrue(catalogue.getProducts().size() > 0);

On remarque immédiatement que le code est beaucoup plus épuré, car il manipule directement des objets métiers, et cela aussi bien coté client que serveur.
Appliquons cela maintenant à une réponse HTTP du contrôleur REST.

Conversion de la réponse

L’implémentation actuelle de la méthode search utilise le mécanisme de génération de vue de Spring MVC :

  1. @RequestMapping(value=« /search/{query} »,method=RequestMethod.GET)
  2. public ModelAndView search(@PathVariable String query) throws Exception {
  3. Catalogue catalogue = new Catalogue();
  4. return new ModelAndView(« default »,« catalogue »,catalogue);
  5. }

Il est possible de court-circuiter le système de génération de vue de Spring MVC, car il n’est pas vraiment utile dans notre cas (sérialisation XML). Nous pouvons donc demander au AnnotationMethodHandlerAdapter de générer la vue (il délèguera à ses HttpMessageConverters). Il suffit pour cela de retourner directement l’objet métier (et plus un ModelAndView) et d’annoter la méthode avec ResponseBody :

  1. @ResponseBody
  2. @RequestMapping(value = « /search/{query} », method = RequestMethod.GET)
  3. public Catalogue search(@PathVariable String query) throws Exception {
  4. SolrQuery solrQuery = new SolrQuery(query);
  5. SolrResponse response = solrServer.query(solrQuery);
  6. SolrDocumentList docs = (SolrDocumentList) response.getResponse().get(« response »);
  7. Catalogue catalogue = new Catalogue();
  8. for (SolrDocument doc : docs) {
  9. catalogue.add(map(doc));
  10. }
  11. return catalogue;
  12. }

L’utilisation de ResponseBody et du MarshallingHttpMessageConverter permet d’obtenir exactement le même résultat XML, mais le contrôleur REST n’a maintenant plus de références à l’API Spring (à part les annotations).
Cette dernière amélioration conclut cette série d’articles. Nous avons vu l’utilisation du support REST de Spring, cotés serveur et client, avec un exemple d’application concret : proposer une abstraction sous forme de services Web REST à un serveur d’indexation. Le support de Spring est très souple puisqu’il propose de nombreux points d’extension et permet de s’affranchir de la plupart des problématiques techniques (connexions HTTP, sérialisation dans différents formats).

2 pensées sur “REST avec Spring 3.0 et Solr (3ème partie)

  • 11 janvier 2010 à 12 h 40 min
    Permalink

    Article intéressant, je ne connaissais pas RequestBody et ResponseBody, c’est apparu avec les dernières milestones de Spring 3.0 ?

    Je m’interroge aussi sur la meilleure manière de faire coexister la négociation de contenu et la sérialisation « magique » apportée par ResponseBody.

    Prenons par exemple la méthode search. Supposons qu’en fonction du header « Accept » de la requête, on souhaite récupérer du XML, du JSON, ou même une vue HTML. Et bien entendu, on souhaite disposer d’un contrôleur avec un code unique, quel que soit le type attendu en réponse.

    A priori, je dirai qu’il faut utiliser un ContentNegotiatingViewResolver. Cependant, est-ce que l’utilisation de @ResponseBody ne désactive pas l’utilisation du ViewResolver ? La doc dit « @ResponseBody indicates that the return type should be written straight to the HTTP response body (and not placed in a Model, or interpreted as a view name) ».

    L’alternative pourrait donc bien être de ne pas utiliser @ResponseBody sur la méthode, et de retourner quand même un Catalogue. Dans ce cas, Spring va créer un attribut de modèle nommé « catalogue ». Reste plus qu’à configurer un view resolver spécifique pour chaque type négociable… Finalement, ça ne demande peut-être pas beaucoup plus de travail que d’utiliser @ResponseBody, à voir. Je n’ai pas creusé pour le moment, pour en savoir plus => Doc de Spring => Chapitre 15.5 Resolving Views => ContentNegociatingViewResolver .

    Si j’ai dis des bêtises, corrigez-moi 🙂 .

    Répondre
  • 11 janvier 2010 à 17 h 10 min
    Permalink

    oui, RequestBody et ResponseBody sont des nouveautés Spring 3.0.

    en effet, si l’on veut renvoyer plusieurs formats avec le même contrôleur, il vaut mieux abandonner ResponseBody et se rabattre vers le système de vue de Spring MVC et le ContentNegociatingViewResolver (qui permet de déléguer des ViewResolver selon un algorithme mettant en jeu notamment des headers dans la requête HTTP).

    Répondre

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.

%d blogueurs aiment cette page :