Les services Web avec Java

Note

Pour ce chapitre, vous aurez besoin de l’outil SoapUI. Vous devez le télécharger à l’adresse https://www.soapui.org/downloads/soapui/source-forge.html

La quête de l’interopérabilité

Une problématique courante dans la mise en place de systèmes d’information et de savoir comment échanger des informations entre plusieurs processus. Cette problématique est souvent résumée par la notion d’interopérabilité. D’un point de vue technique, l’interopérabilité est rendue plus complexe par un ou plusieurs facteurs :

  • les programmes sont écrits dans des langages différents

  • les processus s’exécutent sur des machines différentes

  • les machines appartiennent à des réseaux différents

Avant l’an 2000, on trouve des solutions telles que Sun RPC, CORBA, DCOM, RMI. À partir des années 2000, le succès des réseaux IP, de HTTP et de XML apporte une nouvelle perspective : définir un protocole d’échange de messages XML via HTTP entre deux processus.

Ce protocole originellement défini par Microsoft puis maintenu par le W3 s’appelle SOAP. Depuis SOAP a été enrichi de nombreuses spécifications et l’ensemble de ces technologies sont désignées sous le terme WS-*.

WS-* est un ensemble de technologies, il ne s’agit ni d’un modèle de conception ni d’un modèle d’architecture.

SOAP : structure des messages

SOAP est une recommandation pour l’échange de messages au format XML entre un expéditeur (SOAP sender) et un destinataire (SOAP receiver). La structure des messages SOAP est identique qu’elle provienne de l’expéditeur (requête) ou du destinataire (réponse). Un message est constitué par une enveloppe (SOAP envelope) qui peut contenir un en-tête (SOAP header) et ensuite une charge utile (SOAP body).

Pour une utilisation avec le protocole HTTP, le message doit être envoyé avec la méthode POST à l’URL du service (appelée aussi endpoint)

Exemple de message SOAP
<soapenv:Envelope
   xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
   xmlns:m="http://www.mymeteo.com/meteo">
   <soapenv:Header>
   </soapenv:Header>
   <soapenv:Body>
      <m:releve>
         <m:temperature>
            <m:valeur>6.0</m:valeur>
            <m:unite>celsius</m:unite>
         </m:temperature>
         <m:lieu>
            <m:ville>Bordeaux</m:ville>
         </m:lieu>
      </m:releve>
   </soapenv:Body>
</soapenv:Envelope>

Dans l’exemple ci-dessus le message contient de toute évidence un relevé météorologique pour la ville de bordeaux. Ce message peut aussi bien être une requête d’un expéditeur qu’une réponse d’un destinataire (la structure des messages est la même).

Note

Du fait de son utilisation exclusive du XML pour la composition des messages, une bonne compréhension de SOAP nécessite une connaissance approfondie de XML (Cf. le complément au cours sur XML).

Un message SOAP est défini pour le namespace XML http://schemas.xmlsoap.org/soap/envelope/

Structure d’un message SOAP
<!-- l'enveloppe du message -->
<soapenv:Envelope
   xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">

  <!-- l'en-tête du message -->
  <soapenv:Header>
    <!-- les éventuelles méta-informations -->
  </soapenv:Header>

  <!-- le corps de la requête -->
  <soapenv:Body>
    <!-- le message (payload) -->
  </soapenv:Body>

</soapenv:Envelope>

SOAP définit un format pour un type de réponse particulière : une SOAP fault. Une SOAP fault signale un problème lors du traitement du service.

Une réponse contenant une SOAP fault
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
   <soap:Body>
      <soap:Fault>
         <faultcode>soap:Server</faultcode>
         <faultstring>Internal server error</faultstring>
      </soap:Fault>
   </soap:Body>
</soap:Envelope>

Pour l’élément faultcode, on peut utiliser les valeurs :

Web Service Description Language (WSDL)

La condition pour utiliser un service Web SOAP est de connaître les opérations disponibles et le format des messages autorisés en entrée et/ou en sortie de ses opérations. WSDL (Web Service Description Language) est un langage XML permettant la description complète d’un service Web. Ainsi un fichier WSDL est analysable par programme et on trouve des outils dans différents langages de programmation pour générer du code (les stubs) facilitant le développement des programmes (client ou serveur).

Note

La plupart des stacks SOAP donne accès au WSDL en ligne (en le générant dynamiquement si nécessaire).

Par exemple, pour les serveurs d’application Java EE, le WSDL est disponible à la même URL que le service en ajoutant wsdl comme paramètre.

Exemple d’URL pour accéder au WSDL
http://localhost:8080/MeteoService?wsdl
Exemple de WSDL
<?xml version='1.0' encoding='UTF-8'?>
<definitions
  xmlns="http://schemas.xmlsoap.org/wsdl/"
  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
  xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata"
  xmlns:tns="http://www.mymeteo.com/webservices/meteo"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns:ns1="http://www.mymeteo.com/meteo"
  targetNamespace="http://www.mymeteo.com/webservices/meteo"
  name="MeteoService"
>

  <!-- Les types de données au format XML schema -->
  <types>
    <xs:schema version="1.0" targetNamespace="http://www.mymeteo.com/meteo"
               elementFormDefault="qualified">
      <xs:complexType name="temperature">
        <xs:sequence>
          <xs:element name="valeur" type="xs:double"/>
          <xs:element name="unite" type="xs:string"/>
        </xs:sequence>
      </xs:complexType>

      <xs:complexType name="lieu">
        <xs:sequence>
          <xs:element name="ville" type="xs:string"/>
        </xs:sequence>
      </xs:complexType>

      <xs:complexType name="releve">
        <xs:sequence>
          <xs:element name="temperature" type="ns1:temperature"/>
          <xs:element name="lieu" type="ns1:lieu"/>
        </xs:sequence>
      </xs:complexType>

      <xs:element name="lieu" type="ns1:lieu"/>
      <xs:element name="releve" type="ns1:releve"/>
    </xs:schema>
  </types>

  <!--
    La description de tous les messages possibles.
    Un message est défini par un ensemble de parties (une partie pour le payload
    et une partie par en-tête)
  -->
  <message name="releveMeteo">
    <part name="partLieu" element="ns1:lieu"/>
  </message>

  <message name="releveMeteoResponse">
    <part name="partReleve" element="ns1:releve"/>
  </message>

  <!--
    Description des interfaces (indépendantes de SOAP)
    Une interface est composée d'un ensemble d'opérations.
    Chaque opération est définie par les messages en entrée et en sortie.
  -->
  <portType name="MeteoService">
    <operation name="releveMeteo">
      <input
        wsam:Action="http://www.mymeteo.com/webservices/meteo/MeteoService/releveMeteoRequest"
        message="tns:releveMeteo"/>
      <output
        wsam:Action="http://www.mymeteo.com/webservices/meteo/MeteoService/releveMeteoResponse"
        message="tns:releveMeteoResponse"/>
    </operation>
  </portType>

  <!-- Liaison des interfaces avec le protocole SOAP -->
  <binding name="MeteoServicePortBinding" type="tns:MeteoService">
    <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"/>
    <operation name="releveMeteo">
      <soap:operation soapAction=""/>
      <input>
        <soap:body use="literal"/>
      </input>
      <output>
        <soap:body use="literal"/>
      </output>
    </operation>
  </binding>

  <!--
    Localisation du service
    Dans le cas d'un binding SOAP avec un transport HTTP, on trouve ici l'URL du service.
  -->
  <service name="MeteoService">
    <port name="MeteoServicePort" binding="tns:MeteoServicePortBinding">
      <soap:address location="http://www.mymeteo.com/ws/meteo"/>
    </port>
  </service>
</definitions>

Développer un service Web SOAP

Pour le langage Java, on dispose de JAX-WS qui est l’API permettant d’implémenter des services Web et JAXB (Java API for XML Binding) permettant d’associer une classe Java à une représentation XML.

On distingue deux approches pour développer un service Web WS-* :

Contract first

On définit le contrat du service en spécifiant le WSDL et on utilise des outils pour générer le code des stubs.

Contract last

On développe le service et on utilise des outils pour générer le WSDL.

Exercice

Accéder à un service Web

Testez le service Web SOAP disponible à cette adresse : http://ws-meteo.herokuapp.com/

Pour tester ce service Web, vous pouvez utiliser le client SoapUI.

Les outils Java de génération

Il est possible d’utiliser le plugin maven JAX-WS pour générer les classes Java à partir d’un fichier WSDL. Il est possible de développer une application cliente et/ou une application serveur à partir des classes générées.

WSDL → Java (Contract first)

Si on dispose du fichier WSDL, on peut générer le code Java. On dit que l’on part du contrat de service : c’est l’approche contract first. On utilise pour cela l’outil wsimport.

Extrait d’un fichier pom.xml pour la déclaration du plugin JAX-WS
<build>
  <plugins>
    <plugin>
      <groupId>com.sun.xml.ws</groupId>
      <artifactId>jaxws-maven-plugin</artifactId>
      <version>2.3.2</version>
      <executions>
        <execution>
          <id>wsimport</id>
          <goals>
            <goal>wsimport</goal>
          </goals>
          <phase>generate-sources</phase>
          <configuration>
            <wsdlDirectory>src/main/wsdl</wsdlDirectory>
            <sourceDestDir>src/main/java</sourceDestDir>
            <xnocompile>true</xnocompile>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

La configuration ci-dessus déclare pour Maven de sélectionner tous les fichiers *.wsdl qui se trouvent dans le répertoire src/main/wsdl et de générer le code Java correspondant dans le répertoire src/main/java.

Astuce

Pour générer les fichiers depuis Eclipse, faites un clic droit sur votre projet et sélectionnez Run As > Maven generate-sources.

Consommer un service Web avec JAX-WS

Une application cliente d’un service Web SOAP peut utiliser un stub. Un stub est une classe générée qui masque la complexité des échanges de messages avec le serveur.

Il est possible de créer un client à partir du code généré par wsimport.

Exemple d’accès à la SEI pour un client
// L'URL du WSDL (peut-être un fichier sur disque ou un lien HTTP)
java.net.URL wsdlDocumentLocation = new java.net.URL("file:HelloService.wsdl");

// Le qualified name du service spécifié par les attributs
// targetNamespace et name de la balise racine dans le WSDL
javax.xml.namespace.QName serviceName =
             new javax.xml.namespace.QName("http://spoonless.github.io/ws/hello","HelloService");

// Creation d'une stub vers le service Web
javax.xml.ws.Service service = javax.xml.ws.Service.create(wsdlDocumentLocation, serviceName);

// La SEI offrant les méthodes du service Web sous la forme d'un interface Java
// L'interface est celle générée par wsimport
HelloService helloService = service.getPort(HelloService.class);

// Exemple d'appel du service
System.out.println(helloService.sayHello("world"));

Le code précédent pose un problème : le client utilise l’URL du service spécifiée dans la section wsdl:service du WSDL. Il est souvent utile de pouvoir changer dynamiquement l’URL du endpoint.

Modification de l’URL du endpoint du service
// ...

// La SEI offrant les méthodes du service Web sous la forme d'un interface Java
// L'interface est celle générée par wsimport
HelloService helloService = service.getPort(HelloService.class);

// Un service implémente l'interface javax.xml.ws.BindingProvider
javax.xml.ws.BindingProvider bp = (javax.xml.ws.BindingProvider) helloService;

// ENDPOINT_ADDRESS_PROPERTY est une propriété donnant l'URL du endpoint
bp.getRequestContext().put(javax.xml.ws.BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
                           "http://monendpoint.com/monservice");

// Exemple d'appel du service
System.out.println(helloService.sayHello("world"));

Exercice

Écrire un programme client d’un service Web

Écrivez une client pour le service Web SOAP disponible à cette adresse : http://ws-meteo.herokuapp.com/.

Par exemple, vous pouvez écrire un programme qui accepte le nom d’une ville en paramètre et qui affiche la température obtenue pour cette ville après un appel au service Web.

Modèle de projet

Vous pouvez télécharger un modèle de projet Maven. Ajoutez un fichier WSDL dans le répertoire src/main/wsdl et faites générer les classes Java à partir de ce contrat. Pour cela, vous devez lancer la cible Maven generate-sources. Dans Eclipse, vous pouvez appeler Maven en faisant un clic droit sur le nom de votre projet et en choisissant Run As > Maven generate-sources.

Implémenter des services Web avec JAX-WS

L’implémentation d’un service Web passe par l’écriture et l’implémentation d’une SEI (Service Endpoint Implementation). Cette interface et son implémentation peuvent être fournies par le développeur ou peuvent être générées à partir d’un ficher WSDL (Cf. ci-dessous).

Une SEI avec JAX-WS SOAP
package dev.gayerie;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService(targetNamespace="http://spoonless.github.io/ws/hello")
@SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)
public interface HelloService {

  @WebMethod
  @WebResult(name = "helloMessage", targetNamespace = "http://spoonless.github.io/ws/hello")
  public String sayHello(
      @WebParam(name = "who", targetNamespace="http://spoonless.github.io/ws/hello") String name);

}
La classe d’implémentation de la SEI
package dev.gayerie;

import javax.jws.WebService;

@WebService(endpointInterface="dev.gayerie.HelloService",
    targetNamespace="http://spoonless.github.io/ws/hello", serviceName="HelloService")
public class HelloServiceImpl implements HelloService {

  @Override
  public String sayHello(String name) {
    return "Hello " + name;
  }

}

Note

Il est également possible de définir une SEI directement avec une classe Java sans passer par une interface :

package dev.gayerie;

import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;

@WebService(targetNamespace="http://spoonless.github.io/ws/hello", serviceName="HelloService")
@SOAPBinding(parameterStyle = SOAPBinding.ParameterStyle.BARE)
public class HelloService {
  @WebMethod
  @WebResult(name = "helloMessage", targetNamespace = "http://spoonless.github.io/ws/hello")
  public String sayHello(
      @WebParam(name = "who", targetNamespace="http://spoonless.github.io/ws/hello") String name) {
    return "Hello " + name;
  }
}

Cette SEI permet de gérer un échange de messages comme celui-ci :

Message de l’expéditeur
<soapenv:Envelope
  xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
  xmlns:hel="http://spoonless.github.io/ws/hello">
   <soapenv:Header/>
   <soapenv:Body>
      <hel:who>world</hel:who>
   </soapenv:Body>
</soapenv:Envelope>
Message du destinataire
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
   <soapenv:Body>
      <helloMessage xmlns="http://spoonless.github.io/ws/hello">Hello world</helloMessage>
   </soapenv:Body>
</soapenv:Envelope>

Nous allons maintenant décrire les annotations Java utilisées pour transformer la classe HelloService en service Web :

@WebService

Désigne une classe comme étant une SEI ou une implémentation d’une SEI. Elle possède entre-autres les attributs suivants :

serviceName

Le nom du service tel qu’exposé dans le WSDL

targetNamespace

Le namespace XML du service tel qu’exposé dans le WSDL

endpointInterface

Désigne l’interface Java correspondante au SEI. À spécifier sur la classe d’implémentation du SEI.

@SOAPBinding

Indique le support SOAP pour le service. Les informations données par les attributs de cette annotation correspondent pour la plupart à celles que l’on trouve dans la section binding du WSDL. Cette annotation possède entre-autres les attributs suivants :

style

Définit s’il s’agit d’un service RPC ou Document.

parameterStyle

Détermine si un paramètre de la méthode représente la charge utile du message SOAP (le body) ou si ce paramètre doit être encapsulé (wrapped) dans un élément racine XML nommé d’après le nom de l’opération SOAP.

@WebMethod

Indique si la méthode correspond à une opération du service Web

@WebParam

Cette annotation permet d’associer le paramètre de la méthode sur laquelle elle porte à une partie d’un message (l’élément part de message dans le WSDL). Elle possède les attributs suivants :

name

le nom de l’élément XML

targetNamespace

l’espace de nom de l’élément XML

header

un booléen indiquant si l’élément XML correspond à un en-tête ou s’il s’agit du corps du message.

mode

Il s’agit d’une énumération pouvant prendre les valeurs IN, OUT ou INOUT. Pour les valeurs OUT et INOUT, le type du paramètre est obligatoirement de type javax.xml.ws.Holder<T>. IN signifie que l’élément est extrait du message de l’expéditeur tandis que OUT indique que l’élément fait partie de la réponse.

public String sayHello(
     @WebParam String name,
     @WebParam(header=true) UserCredentials userCredentials,
     @WebParam(header=true, mode=Mode.OUT) Holder<ServerInfo> serverInfo);
@WebResult

Cette annotation a la même sémantique que l’annotation @WebParam sauf qu’elle est ajoutée à la méthode et qu’elle porte sur la valeur de retour de la méthode. Elle possède les attributs suivants :

name

le nom de l’élément XML

targetNamespace

l’espace de nom de l’élément XML

header

un booléan indiquant si l’élément XML correspond à un en-tête ou s’il s’agit du corps du message.

Avec JAX-WS, les exceptions lancées par une méthode d’une SEI sont automatiquement transformées en SOAP fault.

Déployer un service Web dans un serveur Tomcat

Tomcat ne gère pas directement le déploiement de service Web JAX-WS. Vous pouvez ajouter jaxws-rt dans votre projet. Cette bibliothèque fournit les classes nécessaires pour développer une application compatible JAX-WS.

Dépendance avec jaxws-rt dans le fichier pom.xml
<dependency>
  <groupId>com.sun.xml.ws</groupId>
  <artifactId>jaxws-rt</artifactId>
  <version>2.3.2</version>
</dependency>

Dans le fichier web.xml de déploiement, il faut déclarer un listener fourni par jaxws-rt :

Déclaration du listener dans le fichier web.xml
<listener>
  <listener-class>com.sun.xml.ws.transport.http.servlet.WSServletContextListener</listener-class>
</listener>

Il faut ensuite déclarer les services dans le fichier sun-jaxws.xml. Ce fichier devra être présent dans le répertoire WEB-INF de l’application. Pour un projet Maven, il faut donc ajouter le fichier :file`src/main/webapp/WEB-INF/sun-jaxws.xml`.

Exemple de contenu du fichier sun-jaxws.xml
<?xml version="1.0" encoding="UTF-8"?>
<endpoints xmlns="http://java.sun.com/xml/ns/jax-ws/ri/runtime" version="2.0">
        <endpoint name="meteoService"
                  implementation="dev.gayerie.HelloServiceImpl"
                  url-pattern="/hello" />
</endpoints>

Ce fichier permet de déclarer chaque endpoint en fournissant son nom, sa classe d’implémentation Java et le motif d’URL pour lequel le service est disponible.

Note

Il n’est pas nécessaire de fournir de document WSDL, le serveur le générera automatiquement lorsque l’on ajoute ?wsdl à la fin de l’URL du endpoint.

Exercice

Écrire un service Web

Reprenez l’implémentation de la SEI donnée en exemple plus haut et créez une application Web incorporant ce service. Déployez ensuite cette application dans un serveur Tomcat. Vérifiez que le service fonctionne en utilisant par exemple SoapUI.

Vous pouvez ensuite écrire un programme Java client de votre service.

Modèle de projet

Vous pouvez télécharger un modèle de projet Maven.