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

11

Voici la suite 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 la première partie présenté le cas d’utilisation et mis en place le serveur Solr. Nous allons voir dans cette deuxième partie comment écrire un contrôleur REST avec Spring.

Configuration de l’infrastructure de Spring MVC

Le support REST de Spring est basé sur Spring MVC : les contrôleurs REST sont en fait des contrôleurs Spring MVC annotées d’une façon spécifique. La première étape consiste donc à configurer une DispatcherServlet traditionnelle :

  1. <web-app xmlns:xsi=« http://www.w3.org/2001/XMLSchema-instance »
  2. xmlns=« http://java.sun.com/xml/ns/javaee » xmlns:web=« http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd »
  3. xsi:schemaLocation=« http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd »
  4. id=« rest » version=« 2.5 »>
  5. <display-name>Support REST de Spring avec Solr</display-name>
  6. <servlet>
  7. <servlet-name>catalogue</servlet-name>
  8. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  9. </servlet>
  10. <servlet-mapping>
  11. <servlet-name>catalogue</servlet-name>
  12. <url-pattern>/catalogue/*</url-pattern>
  13. </servlet-mapping>
  14. </web-app>

Par défaut, le fichier de configuration du contexte Spring de notre DispatcherServlet est /WEB-INF/catalogue-servlet.xml (établi selon le nom donné à la Servlet). Ce fichier va contenir pour l’instant la déclaration des contrôleurs Web, en utilisant du component scanning :

  1. <?xml version=« 1.0 » encoding=« UTF-8 »?>
  2. <beans xmlns=« http://www.springframework.org/schema/beans »
  3. xmlns:xsi=« http://www.w3.org/2001/XMLSchema-instance »
  4. xmlns:context=« http://www.springframework.org/schema/context »
  5. xsi:schemaLocation=« 
  6. http://www.springframework.org/schema/beans
  7. http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  8. http://www.springframework.org/schema/context
  9. http://www.springframework.org/schema/context/spring-context-3.0.xsd »>
  10. <context:component-scan base-package=« com.zenika.springsolr.web » />
  11. </beans>

Voyons maintenant comment implémenter une première version de notre contrôleur REST.

Contrôleur REST

Le contrôleur REST va permettre de consulter un catalogue de produit (les classes de domaine sont Catalogue et Produit). Comme dit précédemment, nous allons brancher notre contrôleur sur le serveur Solr, mais chaque chose en son temps, commençons par une version simplifiée, qui retourne une liste de produits en dur. Voici le code de ce premier jet :

  1. package com.zenika.springsolr.web;
  2. import org.springframework.stereotype.Controller;
  3. import org.springframework.web.bind.annotation.RequestMapping;
  4. import org.springframework.web.bind.annotation.RequestMethod;
  5. import org.springframework.web.servlet.ModelAndView;
  6. import com.zenika.springsolr.domain.Catalogue;
  7. import com.zenika.springsolr.domain.Product;
  8. @Controller
  9. public class CatalogueController {
  10. @RequestMapping(value=« /search/ »,method=RequestMethod.GET)
  11. public ModelAndView search() throws Exception {
  12. Catalogue catalogue = new Catalogue()
  13. .add(new Product(« splp », « Spring par la pratique », « Cogoluegnes-Templier »))
  14. .add(new Product(« sdmia », « Spring DM in action », « Cogoluegnes-Piper-Templier »));
  15. return new ModelAndView(« default »,« catalogue »,catalogue);
  16. }
  17. }

Nous avons constitué notre « modèle », il est temps de s’atteler à la « vue ». Afin d’offrir la meilleure interopérabilité possible, les résultats seront rendus en XML. Nous allons compléter le contexte Spring de notre servlet avec la définition d’un Marshaller du module OXM de Spring, utilisant XStream pour sérialiser nos objets métier en XML :

  1. <bean ()>
  2. (…)
  3. <bean class=« org.springframework.web.servlet.view.BeanNameViewResolver » />
  4. <bean id=« default » class=« org.springframework.web.servlet.view.xml.MarshallingView »>
  5. <property name=« marshaller » ref=« marshaller » />
  6. </bean>
  7. <bean id=« marshaller » class=« org.springframework.oxm.xstream.XStreamMarshaller »>
  8. <property name=« aliases »>
  9. <map>
  10. <entry key=« catalogue » value=« com.zenika.springsolr.domain.Catalogue » />
  11. <entry key=« product » value=« com.zenika.springsolr.domain.Product » />
  12. </map>
  13. </property>
  14. </bean>
  15. </bean>

Une fois le serveur démarré, allons voir le résultat de notre travail à l’adresse http://localhost:8080/springsolr/catalogue/search/ :

  1. <catalogue>
  2. <products>
  3. <product>
  4. <id>splp</id>
  5. <name>Spring par la pratique</name>
  6. <manufacturer>Cogoluegnes-Templier</manufacturer>
  7. </product>
  8. <product>
  9. <id>sdmia</id>
  10. <name>Spring DM in action</name>
  11. <manufacturer>Cogoluegnes-Piper-Templier</manufacturer>
  12. </product>
  13. </products>
  14. </catalogue>

Nous avons mis en place l’ossature de notre contrôleur REST, branchons maintenant notre contrôleur REST sur Solr avec l’API client Java, SolrJ.

Implémentation du contrôleur REST avec SolrJ

Solr propose une API permettant d’accéder à une instance de Solr, qu’elle soit embarquée (dans le même process Java) ou distante (comme dans notre cas). Voici un exemple :

  1. SolrServer solrServer = new CommonsHttpSolrServer(« http://localhost:8983/solr/ »);
  2. SolrQuery solrQuery = new SolrQuery(« solr »);
  3. SolrResponse response = solrServer.query(solrQuery);

Nous allons utiliser SolrJ dans le contrôleur REST, mais commençons par déclarer le SolrServer dans le contexte Spring de notre DispatcherServlet :

  1. <bean id=« solrServer » class=« org.apache.solr.client.solrj.impl.CommonsHttpSolrServer »>
  2. <constructor-arg value=« http://localhost:8983/solr/ » />
  3. </bean>

Nous pouvons ensuite l’injecter dans le contrôleur, avec l’annotation standard @Inject :

  1. import javax.inject.Inject;
  2. import org.apache.solr.client.solrj.SolrServer;
  3. @Controller
  4. public class CatalogueController {
  5. @Inject
  6. private SolrServer solrServer;
  7. ()
  8. }

Notez que l’utilisation de l’interface SolrServer nous permet de changer d’implémentation à tout moment : l’implémentation distante (celle que nous utilisons) pour la production, car elle permet d’attaquer un serveur Solr dédié, ou une implémentation locale pour le développement, qui attaquerait une instance embarquée de Solr.

Modifions maintenant la méthode search pour qu’elle récupère depuis l’URL la requête Solr et qu’elle utilise le SolrServer pour récupérer les documents et les transformer en Produit :

  1. import org.apache.solr.client.solrj.SolrQuery;
  2. import org.apache.solr.client.solrj.SolrResponse;
  3. import org.apache.solr.client.solrj.SolrServer;
  4. import org.apache.solr.common.SolrDocument;
  5. import org.apache.solr.common.SolrDocumentList;
  6. import org.springframework.web.bind.annotation.PathVariable;
  7. ()
  8. @Controller
  9. public class CatalogueController {
  10. @Inject
  11. private SolrServer solrServer;
  12. @RequestMapping(value=« /search/{query} »,method=RequestMethod.GET)
  13. public ModelAndView search(@PathVariable String query) throws Exception {
  14. SolrQuery solrQuery = new SolrQuery(query);
  15. SolrResponse response = solrServer.query(solrQuery);
  16. SolrDocumentList docs = (SolrDocumentList) response.getResponse().get(« response »);
  17. Catalogue catalogue = new Catalogue();
  18. for(SolrDocument doc : docs) {
  19. catalogue.add(map(doc));
  20. }
  21. return new ModelAndView(« default »,« catalogue »,catalogue);
  22. }
  23. private Product map(SolrDocument doc) {
  24. return new Product(doc.get(« id »).toString(),doc.get(« name »).toString(),doc.get(« manu »).toString());
  25. }
  26. }

Notez l’utilisation de l’annotation @PathVariable, qui permet de récupérer une partie de l’URL afin de l’utiliser comme paramètre dans le contrôleur. On peut tester cette première fonction REST avec un navigateur Web en exécutant la requête ‘solr’ : http://localhost:8080/springsolr/catalogue/search/solr (attention de bien avoir le serveur Solr lancé comme vu dans la première partie ! ). La réponse est la suivante :

  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>

Pour avoir plus de résultats (limités à 10 toutefois, mais cela est réglable dans la SolrQuery), utiliser l’URL suivante : http://localhost:8080/springsolr/catalogue/search/*:*.

Et pourquoi ne pas permettre à notre contrôleur d’indexer des produits ? Suivons les principes de REST et implémentons cela dans le contrôleur sous la forme d’une requête POST :

  1. @Controller
  2. public class CatalogueController {
  3. ()
  4. @RequestMapping(value=« /index/{id}/{name}/{manu} »,method=RequestMethod.POST)
  5. public void index(@PathVariable String id,@PathVariable String name,@PathVariable String manu,
  6. HttpServletResponse response) throws Exception {
  7. SolrInputDocument doc = new SolrInputDocument();
  8. doc.addField(« id »,id);
  9. doc.addField(« name »,name);
  10. doc.addField(« manu »,manu);
  11. solrSe rver.add(doc);
  12. solrServer.commit();
  13. response.setStatus(HttpServletResponse.SC_OK);
  14. }
  15. }

Il n’est malheureusement pas possible de tester simplement l’indexation avec un navigateur Web, car la requête attendue doit être en POST. Nous allons tester l’indexation dans la troisième partie de l’article, grâce à la partie cliente REST de Spring, le RestTemplate.

Partagez cet article.

A propos de l'auteur

11 commentaires

  1. Cite « Il n’est malheureusement pas possible de tester simplement l’indexation avec un navigateur Web, car la requête attendue doit être en POST. »

    Pourtant le problème entre les navigateurs et REST, c’est plutôt que les navigateurs n’implémentent que GET et POST.

    En l’occurrence, pour tester comme ça, une API REST avec la méthode POST, en ce qui me concerne, j’utilise curl (http://curl.haxx.se/).

  2. Article intéressant, qui montre que Spring fait bien partie des « frameworks haute productivité ».

    Une interrogation (je n’ai pas les sources de Spring sous la main) : on voit ici un mapping entre les paramètres de chemin (« {id} ») et les paramètres de méthode (via @PathVariable). Vu que Java ne dispose pas des paramètres nommés, je suppose que ce mapping est du uniquement au fait que les paramètres sont dans le même ordre dans l’url et dans la signature de la méthode. Est-ce bien le cas ? Est-ce réellement une bonne pratique ?

    Si un refactoring malheureux modifie l’url « /index/{id}/{name}/{manu} » en « /index/{name}/{manu}/{id} », il peut se passer du temps avant que le bug ne soit détecté (sauf que, bien entendu, TDD inside et on s’en rendre vite compte :-P)

    Dans ce cas, ne vaut-il pas mieux nommer les paramètres : @PathVariable(« id ») String id ?

    Je n’affirme rien, c’est une question ouverte.

  3. Arnaud Cogoluègnes on

    Spring se base par défaut sur le nom du paramètre de la méthode, qui est disponible dans le bytecode si la compilation a été faite avec l’option debug (ce qui est le cas par défaut). Il est aussi possible de préciser le nom du paramètre directement dans @PathVariable, comme vous l’indiquez. Il n’y a donc pas de problème d’ordre.

  4. Merci pour cet article.
    Est-ce qu’à votre connaissance le support REST de Spring 3.0 propose une gestion élégante des HTTP status codes? Je ne vois rien à ce sujet dans la doc de Spring 3.0.
    Si ce n’est pas le cas, est-ce vraiment un support REST que nous offre Spring 3.0?!

  5. Arnaud Cogoluègnes on
    Spring propose bien un support pour les status codes avec l’annotation ResponseStatus. On peut la poser sur une méthode. Dans notre méthode POST, on peut supprimer le paramètre de la réponse HTTP de la façon suivante :

    Plus besoin de préciser le status code dans la méthode. On peut aussi apposer ResponseStatus sur une méthode de gestion d’exception. On pourrait rajouter dans notre contrôleur la méthode suivante qui serait appelé lorsqu’une HttpMediaTypeNotAcceptableException est levée :

    Enfin, ResponseStatus peut aussi être apposée sur une classe d’exception qui fera que le status code correspondant sera renvoyé si l’exception est lancée au sein du contrôleur.
  6. Bonjour, Je reviens sur l’histoire du GET et du POST.

    Dans l’article qui vient de sortir sur Developpez.com http://kmdkaci.developpez.com/tutor
    il est expliqué comment transformer un formulaire de GET en POST avec la barre développeur de Firefox, sans même utiliser Firebug.

    A bientôt pour la suite et encore merci pour cette série d’article.

  7. Bonjour,
    Tout d’abord merci pour ces tutoriaux sur REST et Spring.

    Je me pose une question : A quoi se réfère le paramètre « modelName » du constructeur ModelAndView (« catalogue » dans votre exemple) ?
    En effet, si on souhaite retourner un Product, quelle valeur doit-on passer à modelName ?

    J’ai essayé le code suivant qui ne fonctionne pas :
    @RequestMapping(method = RequestMethod.GET)
    public ModelAndView search()
    throws Exception
    {
    return new ModelAndView( « default », « product », new Product( « test », « Test », « a » ) );
    }

    Bizarrement, ça fonctionne correctement si je passe « catalogue » ou même « Product », « Produit »…

    Merci de vos idées.

  8. Bonjour
    Merci pour ce tuto,

    Cordialement
    Bruno

Ajouter un commentaire