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;
}

05 janvier 2011

GWT round trips

Pour bien entamer l'année je vous propose un petit point sur la phase de chargement d'une application GWT.

La page HTML hôte est chargée par le navigateur (potentiellement mise en cache) et inclut un lien vers le script de sélection, qui lui est configuré pour ne jamais être en cache. Ce script s'exécute et sélectionne la permutation qui convient à votre environnement : navigateur, langue, etc (potentiellement mise en cache) L'exécution de ce script construit l'UI de l'application web et enchaine généralement avec un (voir plusieurs si vous n'avez pas trop bien pensé votre appli) appels RPC.

Au final, on a donc 4 requêtes séquentielles avant que l'application soit disponible pour l'utilisateur.

Pour améliorer les performances du chargement d'une appli GWT, autrement dit la première impression qu'auront vos utilisateurs, il existe plusieurs techniques.

La première, consiste à utiliser une page hôte dynamique [1], une JSP qui va directement inclure le contenu du script de sélection, ainsi que le résultat du premier appel RPC (encodé au format JSON ou GWT-RPC) [2].
La page hôte dynamique nous évite 3 requêtes, contre une petite surcharge de travail côté serveur.

Une autre piste pour aller encore plus loin est proposé dans les derniers builds du compilo GWT, via l'issue 941802. Il s'agit de remplacer la sélection du script côté client par du code côté serveur. La page hôte peut alors contenir directement la permutation GWT adaptée au navigateur client. Ne vous emballez pas, cette option est totalement expérimentale et même pas complète dans le SDK : il vous faudra surcharger le Linker et développer le code d'identification de la permutation, autrement dit retrousser vos manches.


[1] http://code.google.com/intl/fr-FR/webtoolkit/articles/dynamic_host_page.html
[2] http://www.techhui.com/profiles/blogs/simpler-and-speedier-gwt-with