Web service REST avec JAXB. Simple vraiment ?

Ce billet discute de la réalisation d'un web service REST avec JAXB dans le contexte d'un code existant. Dans ce cadre, REST/JAXB est elle la solution la plus simple ?

La mise en oeuvre de REST décrite ici et est intéressante par sa simplicité et son économie.
Je l'ai utilisée pour mettre en oeuvre un service de supervision très simple, juste une opération echo() :

public interface MonitorService
{
  String echo(final String valeur);
}

@Controller
@RequestMapping(value="/monitoring")
public class MonitorServiceImpl
implements MonitorService
{

    final private Logger log = LoggerFactory.getLogger(MonitorServiceImpl.class);

    @ResponseBody
    @RequestMapping(value = "/echo/{valeur}", method = RequestMethod.GET)
    public String echo(@PathVariable final String valeur)
    {
    	log.info("echo("+valeur+")");
    	if (valeur == null) {
    		return "NULL";
    	}
    	return valeur.toUpperCase();
    }
}

Avec les déclarations Spring suivante aux "bons endroits" :
Dans le web.xml

<servlet>
  <servlet-name>monitor</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
  <servlet-name>monitor</servlet-name>
  <url-pattern>/do/*</url-pattern>
</servlet-mapping>

Dans le fichier Spring monitor-servlet.xml de configuration de la servlet :

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
  <property name="messageConverters">
    <list>
       <bean class="org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter"/>
    </list>
  </property>
</bean>

<bean id="monitor.service.http"  class="org.netapsys.monitor.impl.MonitorServiceImpl"/>

Le tout est assemblé dans un war, puis déployé sur un serveur d'application.

Il ne reste plus qu'à écrire la partie cliente :

public class MonitoringClientImpl 
implements MonitorService
{
    private RestTemplate restTemplate  = null;
    
    private String urlServeur = StringUtils.EMPTY;

    @Override
    public String echo(final String valeur) {
        return getRestTemplate().getForObject(
                getUrlServeur() + "/do/monitoring/echo/{valeur}",
                String.class,
                reference);
    }
}

Avec le fichier Spring de configuration pour le test :

<bean id="monitor.client" class="org.netapsys.monitor.client.MonitoringClientImpl">
  <property name="restTemplate" ref="rstemp.rest.template"/>
  <property name="urlServeur" value="${rstemp.url}"/>
</bean>
<bean id="rstemp.rest.template" class="org.springframework.web.client.RestTemplate"/>
<!-- rstemp.url est une valeur substituée par un PropertyPlaceholderConfigurer -->

Et le code de test :

public class MonitorRestClientImplTest
extends AbstractSpringContexteTestNg
{

    private final String[] fichiersContextes = {"test-context.xml"};

    @BeforeClass
    public void chargerContexte()
    {
        chargerContexte(this.fichiersContextes);
    }

    @Test
    public void testHello()
    {
        final MonitorService service = (MonitorService) getBean("monitor.client");
        final String result = service.echo("hello");
        info(result);
        Assert.assertEquals(result, "HELLO");
    }
}

Malheureusement, le test échoue :

698 DEBUG RestTemplate(78): Created GET request for "http://localhost:8080/rstempx/do/monitoring/echo/hello"
700 DEBUG RestTemplate(520): Setting request Accept header to [text/plain, */*]
930  WARN RestTemplate(478): GET request for "http://localhost:8080/rstempx//do/monitoring/echo/hello" resulted in 406 (Not Acceptable); invoking error handler
FAILED: testHello
org.springframework.web.client.HttpClientErrorException: 406 Not Acceptable

La trace côté serveur confirme que l'invocation est bien reçue mais qu'il y a un problème dans le traitement :

904 DEBUG DispatcherServlet(693): DispatcherServlet with name 'monitor' processing GET request for [/rstempx//do/monitoring/echo/hello]
911 DEBUG DefaultAnnotationHandlerMapping(266): Matching patterns for request [/monitoring/echo/hello] are [/monitoring/echo/{valeur}]
913 DEBUG DefaultAnnotationHandlerMapping(290): URI Template variables for request [/monitoring/echo/hello] are {valeur=hello}
915 DEBUG DefaultAnnotationHandlerMapping(221): Mapping [/monitoring/echo/hello] to HandlerExecutionChain with handler [org.netapsys.monitor.impl.MonitorServiceImpl@3f3cbbbf] and 2 interceptors
919 DEBUG DispatcherServlet(769): Last-Modified value for [/rstempx//do/monitoring/echo/hello] is: -1
962 DEBUG HandlerMethodInvoker(173): Invoking request handler method: public java.lang.String org.netapsys.monitor.impl.MonitorServiceImpl.echo(java.lang.String)
962  INFO MonitorServiceImpl(51): echo(hello)
967 DEBUG AnnotationMethodHandlerExceptionResolver(132): Resolving exception from handler [org.netapsys.monitor.impl.MonitorServiceImpl@3f3cbbbf]: org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation

Le problème est que la classe java.util.String n'est pas annotée avec JAXB, et plus précisement, qu'elle ne déclare pas l'annotation @XmlRootElement. Or, c'est une condition nécessaire pour que le convertisseur Jaxb2RootElementHttpMessageConverter fonctionne comme prévu. Une solution est de rajouter un convertisseur qui prend en charge les Strings. En modifiant la liste des convertisseurs côté serveur :

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
  <property name="messageConverters">
    <list>
	<bean class="org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter"/>
	<bean class="org.springframework.http.converter.StringHttpMessageConverter"/>
     </list>
  </property>
</bean>

Le test est en succès :

667 DEBUG RestTemplate(78): Created GET request for "http://localhost:8080/rstempx//do/monitoring/echo/hello"
668 DEBUG RestTemplate(520): Setting request Accept header to [text/plain, */*]
679 DEBUG RestTemplate(465): GET request for "http://localhost:8080/rstempx//do/monitoring/echo/hello" resulted in 200 (OK)
680 DEBUG RestTemplate(71): Reading [java.lang.String] as "text/plain" using [org.springframework.http.converter.StringHttpMessageConverter@49c4a5ec]
681  INFO SupportLoggingImpl(72): HELLO
PASSED: testHello

Par défaut, le bean AnnotationMethodHandlerAdapter est initialisé pour utiliser, entre autres, le convertisseur StringHttpMessageConverter. Dans le cas où l'on souhaite mettre en oeuvre un convertisseur qui n'est pas initialisé par défaut, ce qui est le cas du convertisseur JAXB, il faut également spécifier les convertisseurs par défaut (au besoin). Il n'y a pas à ma connaissance de possibilité de rajouter un convertisseur à ceux déjà utilisés lors de la déclaration du bean, car la propriété messageConverters est un tableau et non une collection.

En fait, comme la requète est simple, on peut tester la méthode directement avec un navigateur :
Invocation echo()

Au final dans mon cas, le résultat est correct. Cependant, on peut faire plusieurs remarques:

L'invocation de la méthode echo() n'utilise à aucun moment JAXB. La déclaration du convertisseur JAXB est inutile. Conséquence on peut se contenter dans ce cas du bean par défaut :

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>

A contrario, si côté client, on attend une réponse sous la forme d'un fragment XML, le fait de retourner une chaine "brute" représente un cas particulier : parfois je retourne du XML, parfois non... De plus, comme Spring fournit un convertisseur pour les String, il n'y a pas de problème. Mais si l'on considère des opérations comme :

int getValue(final int v);
boolean getStatus(final boolean status);

les invocations de ces méthodes ne fonctionnent pas et l'erreur est du même type que précédement : 406 Not Acceptable.

Il existe plusieurs solutions :

  1. 1 Ecrire les convertisseurs pour tous les types utilisés dans les signatures des API.
  2. 2 Reformuler toutes les API pour n'utiliser que des classes annotées avec JAXB.
  3. 3 Utiliser un sérialiseur XML supportant les types simples.

La solution 1 présente l'inconvénient de ne pas répondre simplement à tous les cas, particulièrement dans le cas d'utilisation de collections dans les signatures de méthodes. J'ai choisi de ne pas l'explorer dans la suite de ce billet, car il ne me semble pas facile à généraliser.

Reformuler les API en utilisant JAXB

Ce n'est pas un problème en pratique, si l'utilisation des méthodes dans un contexte REST/JAXB est prévue lors de la conception de l'API. Au pire, il est toujours possible d'utiliser un Adaptateur au dessus de l'API existante. Par exemple l'utilisation de la classe ci-dessous permet de régler le cas des méthodes précédentes :

@XmlRootElement(name="response")
@XmlAccessorType(XmlAccessType.FIELD)
public class SimpleResponse<T> 
{
  @XmlElement(name="value")
  private T value;

  public SimpleResponse() {
    // empty
  }

  public SimpleResponse(final T v) {
    this.value = v;
  }

  public T getValue() {
    return this.value;
  }

  public void setValue(final T value) {
    this.value = value;
  }
}

La classe de service devient :

@Controller
@RequestMapping(value="/monitoring")
public class MonitorServiceImpl
implements MonitorService
{

  final private Logger log = LoggerFactory.getLogger(MonitorServiceImpl.class);

  @ResponseBody
  @RequestMapping(value = "/echo/{valeur}", method = RequestMethod.GET)
  public SimpleResponse<String> echo(@PathVariable final String valeur) {
    log.info("echo("+valeur+")");
    if (valeur == null) {
      return new SimpleResponse<String>("NULL");
    }
    return new SimpleResponse<String>(valeur.toUpperCase());
  }

  @ResponseBody
  @RequestMapping(value = "/value/{v}", method = RequestMethod.GET)
  public SimpleResponse<Integer> getValue(@PathVariable final int v) {
    log.info("value("+v+")");
    return new SimpleResponse<Integer>(v * 2);
  }

  @ResponseBody
  @RequestMapping(value = "/status/{status}", method = RequestMethod.GET)
  public SimpleResponse<Boolean> getStatus(@PathVariable final boolean status) {
    return new SimpleResponse<Boolean>(!status);
  }
}

Il faut aussi modifier le client :

public class MonitoringClientImpl 
implements MonitorService
{

  private RestTemplate restTemplate  = null;

  private String urlServeur = StringUtils.EMPTY;

  public SimpleResponse<Integer> getValue(int v) {
    return getRestTemplate().getForObject(
        getUrlServeur() + "/do/monitoring/value/{v}",
        SimpleResponse.class,
        v);
  }

  public SimpleResponse<Boolean> getStatus(final boolean status) {
    return getRestTemplate().getForObject(
        getUrlServeur() + "/do/monitoring/status/{status}",
        SimpleResponse.class,
        status);
  }

  public SimpleResponse<String> echo(final String reference) {
    return getRestTemplate().getForObject(
        getUrlServeur() + "/do/monitoring/echo/{reference}",
        SimpleResponse.class,
        reference);
  }
}

Et enfin le test :

@Test
public void testEcho()() {
  final MonitorService service = (MonitorService) getBean("monitor.client");
  final String result = service.echo("hello").getValue();
  Assert.assertEquals(result, "HELLO");
}

@Test
public void testValue() {
  final MonitorService service = (MonitorService) getBean("monitor.client");
  final int result = service.getValue(21).getValue();
  Assert.assertEquals(result, 42);
}

@Test
public void testStatus() {
  final MonitorService service = (MonitorService) getBean("monitor.client");
  final boolean result = service.getStatus(true).getValue();
  Assert.assertEquals(result, false);
}

Cela devient plus compliqué lorsque les signatures des méthodes comportent des collections et que l'on ne peut pas radicalement modifier les contrats. C'est une situation fréquente dans le contexte de réalisation d'un web service au dessus d'un code existant que l'on ne souhaite pas modifier. Il faut écrire une classe dérivée d'XmlAdapter, et "saupoudrer" le tout d'annotations judicieusement choisies. En gros pour une méthode retournant une collection "simple" style Vector<Integer>, comme :

@ResponseBody
@RequestMapping(value = "/list/{base}", method = RequestMethod.GET)
public VectorIntegerResponse getList(@PathVariable int base) {
  log.info("list(" + base + ")");
  Vector<Integer> result = new Vector<Integer>();
  result.add(base);
  result.add(base * 2);
  result.add(base * 4);
  result.add(base * 8);
  return new VectorIntegerResponse(result);
}

Cela revient à écrire 3 classes :

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(name = "VectorSupportInteger")
public class VectorSupportInteger 
{
    @XmlElement(name = "item", required = true)
    private final List<Integer> chunck = new ArrayList<Integer>();

    public List<Integer> getChunck() {
        return this.chunck;
    }
}

public class VectorIntegerAdaptor 
extends XmlAdapter<VectorSupportInteger,Vector<Integer>>
{
	@Override
	public Vector<Integer> unmarshal(VectorSupportInteger v) throws Exception {
        final Vector<Integer> result = new Vector<Integer>();
        for ( final Integer e : v.getChunck() ) {
            result.add(e);
        }
        return result;
	}

	@Override
	public VectorSupportInteger marshal(Vector<Integer> v) throws Exception {
		final VectorSupportInteger result = new VectorSupportInteger();
        for ( final Integer e : v) {
            result.getChunck().add(e);
        }
		return result;
	}
}

@XmlRootElement(name="response")
@XmlAccessorType(XmlAccessType.FIELD)
public class VectorIntegerResponse 
{

	@XmlJavaTypeAdapter(VectorIntegerAdaptor.class)
	private Vector<Integer> value;

	public VectorIntegerResponse()
	{
		// vide;
	}
	
	public VectorIntegerResponse(final Vector<Integer> v)
	{
		value = v;
	}
	
	public Vector<Integer> getValue() {
		return value;
	}

	public void setValue(final Vector<Integer> value) {
		this.value = value;
	}
}

Pour être complet voici le fragment XML résultant de l'invocation :
invocation getListe()

Il est possible de limiter le nombre de classes en utilisant la généricité de façon plus poussée, mais cela ne change pas vraiment la situation : pour un type Collection retourné par une méthode, il faut fournir 3 classes de support à la liaison avec JAXB.
Partant de ce constat, les arguments des tenants d'une approche "schéma en premier" plutôt que "code en premier" commencent à prendre du sens. Dans une approche "schéma en premier", les classes d'adaptateurs sont générées à partir du schéma et par conséquent la complexité d'écriture est masquée. Dans l'approche "code en premier" précédente on utilise JAXB à l'envers. En pratique, dans un contexte de code existant et avec la motivation de ne pas "trop" faire évoluer les contrats d'interfaces, il n'est pas facile de faire autrement.

Mise en oeuvre d'un autre sérialiseur XML.

Pour diminuer la complexité induite par JAXB, le mieux est d'éviter de l'utiliser dans ce contexte. Une solution de remplacement est de mettre en oeuvre XStream pour la sérialisation/désérialisation des fragments XML. C'est assez facile à mettre en oeuvre car Spring fournit tout ce qu'il faut. On commence par rétablir les méthodes du service dans leurs versions originales :

@Controller
@RequestMapping(value="/monitoring")
public class MonitorServiceImpl
implements MonitorService
{

    final private Logger log = LoggerFactory.getLogger(MonitorServiceImpl.class);

    @ResponseBody
    @RequestMapping(value = "/echo/{valeur}", method = RequestMethod.GET)
    public String echo(@PathVariable final String valeur) {
    	log.info("echo("+valeur+")");
    	if (valeur == null) {
    		return "NULL";
    	}
    	return valeur.toUpperCase();
    }

    @ResponseBody
    @RequestMapping(value = "/value/{v}", method = RequestMethod.GET)
    public int getValue(@PathVariable final int v) {
    	log.info("value("+v+")");
    	return v * 2;
    }
    
    @ResponseBody
    @RequestMapping(value = "/status/{status}", method = RequestMethod.GET)
    public boolean getStatus(@PathVariable final boolean status) {
    	return !status;
    }
    
    @ResponseBody
    @RequestMapping(value = "/list/{base}", method = RequestMethod.GET)
    public Vector<Integer> getList(@PathVariable int base) {
    	log.info("list(" + base + ")");
    	Vector<Integer> result = new Vector<Integer>();
    	result.add(base);
    	result.add(base * 2);
    	result.add(base * 4);
    	result.add(base * 8);
    	return result;
    }
}

Ensuite, on déclare le bean XStream et on modifie la configuration de la servlet monitor pour qu'elle utilise le marshaller XStream :

<bean id="xml.xstream.marshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"/>

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
  <property name="messageConverters">
    <list>
      <bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
         <property name="marshaller" ref="xml.xstream.marshaller"/>
         <property name="unmarshaller" ref="xml.xstream.marshaller"/>
      </bean>
    </list>
  </property>
</bean>

<bean id="monitor.service.http"  class="org.netapsys.monitor.impl.MonitorServiceImpl"/>

Côté client, on modifie le RestTemplate de manière analogue :

<bean id="monitor.client" class="org.netapsys.monitor.client.MonitoringClientImpl">
  <property name="restTemplate" ref="rstemp.rest.template"/>
  <property name="urlServeur" value="${rstemp.url}"/>
</bean>

<bean id="rstemp.rest.template" class="org.springframework.web.client.RestTemplate">
  <property name="messageConverters">
    <list>
      <bean id="messageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
         <property name="marshaller" ref="xml.xstream.marshaller" />
         <property name="unmarshaller" ref="xml.xstream.marshaller" />
      </bean>
    </list>
  </property>
</bean>

Le code du client devient :

public class MonitoringClientImpl 
implements MonitorService
{
   private RestTemplate restTemplate  = null;
    
   private String urlServeur = StringUtils.EMPTY;

   public int getValue(int v) {
     return getRestTemplate().getForObject(
        getUrlServeur() + "/do/monitoring/value/{v}",
        Integer.class,
        v);
   }

   public boolean getStatus(final boolean status) {
     return getRestTemplate().getForObject(
        getUrlServeur() + "/do/monitoring/status/{status}",
        Boolean.class,
        status);
   }

   public String echo(final String reference) {
     return getRestTemplate().getForObject(
        getUrlServeur() + "/do/monitoring/echo/{reference}",
        String.class,
        reference);
   }

   public Vector<Integer> getList(int base) {
     return getRestTemplate().getForObject(
        getUrlServeur() + "/do/monitoring/list/{base}",
        Vector.class,
        base);
   }

}

Enfin les résultats d'invocation :

http://localhost:8080/rstempx/do/monitoring/echo/Hello
résultat =>    <string>HELLO</string>
http://localhost:8080/rstempx/do/monitoring/value/21	  
résultat =>    <int>42</int>
http://localhost:8080/rstempx/do/monitoring/status/false   
résultat =>     <boolean>true</boolean>
http://localhost:8080/rstempx/do/monitoring/list/21
résultat =>
<vector>
  <int>21</int>
  <int>42</int>
  <int>84</int>
  <int>168</int>
</vector>

La solution demande moins de code et permet de respecter les contrats existants, comparativement à la solution utilisant JAXB, elle est beaucoup plus simple.

Conclusion

Si l'utilisation RestTemplate est une bonne solution pour écrire un web-service REST, JAXB ne rime pas toujours avec simplicité, surtout dans une approche "code en premier" avec du code hérité. Il me semble préférable dans ce cas d'utiliser un sérialiseur XStream pour le marshalling/unmarshalling des données. Il faut noter que la solution RestTemplate présentée n'a pas que des avantages par rapport à une solution JAXRS. Par exemple, elle ne permet pas de fournir un descriptif WADL des opérations utilisables.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha *

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