06 janvier 2011

RPC/Encoded avec JAX-WS

A l'époque ou SUN a voulu faire prendre à Java le train des services web, le JCP a accouché de JAX-RPC. Nous avons été nombreux à choisir l'implémentation Apache Axis pour exposer ou consommer de tels services.

Mauvaise pioche, le "marché" a par la suite adopté l'approche document, alors que JAX-RPC était un RMI pour les services web, basé sur l'approche RPC. Pour la version 2.0 de la norme, le JCP a changé son fusil d'épaule et créé JAX-WS, norme qui exclue le mode RPC/Encoded et repose sur JAXB.

Voilà pour l'historique. Aujourd'hui, que vous choisissiez Metro, JbossWSAxis2 ou Apache CXF (mon préféré), vous  partez sur une stack web-services éprouvée et interopérable ... jusqu'à ce qu'un partenaire vous expose un vieux WSDL bien naze en RPC/Encoded.

Un réflexe que j'ai constaté plusieurs fois dans ces circonstances consiste à déterrer Axis pour communiquer avec ce service web façon grand-mère. Autrement dit, partir sur un outil qui n'a plus évolué depuis 5 ans, abandonné par ses développeurs au profit d'Axis2 et ... techniquement discutable (pour avoir du déboguer un problème de namespace dedans, je vous déconseille d'essayer)

Si la norme JAX-WS exclue le support du mode RPC/Encoded, cela ne signifie pas qu'il est impossible d'invoquer un service de ce type. On peut toujours exploiter notre pile web-services, incluant ses mécanismes de sécurité, de gestion de ressources, de log et monitoring, etc. Par contre on ne bénéficiera pas des mécanisme de binding entre le flux XML et du code Java généré.

Le moyen le plus simple que j'ai trouvé (je suis ouvert à toutes suggestion) est de construire à la main la trame de requête (utiliser SoapUi pour avoir une base valide), à l'aide d'un mécanisme de template, puis d'aller piocher les éléments du flux de réponse via son arbre DOM. Ca donne ça :

// Construction de la trame requête, 
// utiliser un mécanisme de template style freemarker si besoin
String request = "<soapenv:Envelope "
  + " xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' "
  + " xmlns:xsd='http://www.w3.org/2001/XMLSchema' "
  + " xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/' "
  + " xmlns:urn='urn:Naze'>"
  + "<soapenv:Header/>"
  + "<soapenv:Body>"
  + "  <urn:foo soapenv:encodingStyle='http://schemas.xmlsoap.org/soap/encoding/'>"
  + "    <id xsi:type='xsd:string'>42</id>"
  + "    <query xsi:type='xsd:string'>ndeloof</query>"
  + "  </urn:foo>"
  + "</soapenv:Body>"
  + "</soapenv:Envelope>";

// Un peu de manipulation des API XML, désolé
QName serviceName = new QName( "urn:Naze", "NazeService" );
QName portName = new QName( "urn:Naze", "NazePort" );
Service service = Service.create( getWSDL(), serviceName );

// Utilisation de l'API de niveau "message" de JAX-WS pour transférer le message
Dispatch dispatch = 
   service.createDispatch( portName, Source.class, Service.Mode.MESSAGE );
dispatch.getRequestContext().put( 
   BindingProvider.USERNAME_PROPERTY, "login" );
dispatch.getRequestContext().put( 
   BindingProvider.PASSWORD_PROPERTY, "password" );
Source response = dispatch.invoke( 
   new StreamSource( new StringReader( request ) ) );

MessageFactory msgFactory = MessageFactory.newInstance();
SOAPMessage msg = msgFactory.createMessage();
SOAPPart env = msg.getSOAPPart();
env.setContent( request );

if ( msg.getSOAPBody().hasFault() )
  throw new RemoteAccessException( "oups..." );

// récupération des éléments XML qui nous intéressent
NodeList arrayOfString = 
  msg.getSOAPBody().getElementsByTagName( "result" );
Node node = arrayOfString.item( 0 );
return node.getTextContent();

Je vous accorde que c'est un peu laborieux ;)

Une autre solution, plus légère mais moins portable, consiste à appeler Spring-WS à notre secours. Les templates de Spring permettent d'invoquer un service web en manipulant directement les messages, comme nous venons de le faire, mais avec une couche de Spring pour simplifier le code. Spring-WS n'utilise pas notre  pile JAX-WS mais des appels HTTP directs avec Commons-httpclient et XPath pour extraire les données du message de réponse, avec des classes très "Spring" pour mapper les noeuds XML sur nos classes Java.

Le résultat est une très grande souplesse pour s'adapter à des services web pas trop standards (et il y en a, vous pouvez me croire, qui lisent les normes avec les pieds) :

private final WebServiceTemplate webServiceTemplate;

private final XPathOperations xpath;

public NazeClient( String url ) throws Exception {
   webServiceTemplate = new WebServiceTemplate();
   webServiceTemplate.setDefaultUri( url );
   CommonsHttpMessageSender sender = configureSender();
   webServiceTemplate.setMessageSender( sender );
   xpath = new Jaxp13XPathTemplate();
}

private CommonsHttpMessageSender configureSender()  throws Exception {
   CommonsHttpMessageSender sender = new CommonsHttpMessageSender();
   sender.setCredentials( 
       new UsernamePasswordCredentials( "login", "password" ) );
   sender.afterPropertiesSet();
   return sender;
}

public List sendAndReceive() {
   String message = ""; // idem exemple précédent
   StreamSource source = new StreamSource( new StringReader( message ) );
   Result result = new StringResult();
   webServiceTemplate.sendSourceAndReceiveToResult( source, result );

   List values = xpath.evaluate( "//result", 
       new StringSource( result.toString() ),
       new NodeMapper() {
          public String mapNode( Node node, int nodeNum ) {
              return node.getTextContent();
          }
      } );
   return values;
}

2 commentaires:

johnnyJohn a dit…

N'importe quoi setContent ne prend pas de String

Nicolas De Loof a dit…

Quand j'ai redige ce billet il y a plus d'un an ca marchait (ce code tourne en prod...)