Les services web

Chapitres traités   

Avec la plateforme Java EE, nous avons découvert de nombreux services différents, qui sont très faciles à développer (codage extrêmement simplifié), grâce à la technique des objets distants.

Dans un premier temps, nous avons utiliser les beans sessions qui s'intéressent à la logique métier. Ce sont des objets distants facile à manipuler. Effectivement, avec une application fenêtrée classique, il suffit d'appeler les méthodes de ces objets comme s'ils étaient présent sur le poste local. Par contre, pour que ce fonctionnement simple puisse s'établir, vous devez rester dans le réseau local de l'entreprise (le numéro de port de ces services va être bloqué par le pare-feu).

Si vous désirez utiliser ces services depuis Internet, nous avons découvert que nous devions passer par une application Web qui elle-même communique, en local, aux différents beans sessions. Cela fonctionne très bien, mais nous devons passer systématiquement par un navigateur Web, ce qui limite beaucoup la présentation et l'ergonomie (il s'agit d' un client léger). Effectivement, nous travaillons uniquement à l'aide de page Web.

Le top du top serait de pouvoir faire comme en réseau local, c'est-à-dire de pouvoir utiliser une application fenêtrée, qui fait appel aux différents services, tout en étant sur Internet, et donc sans passer par un navigateur. Il existe une solution pour cela, il s'agit des services web. En réalité, ces services Web communiquent comme une application Web, c'est-à-dire au travers du protocole HTTP (ce qui permet la communication par Internet).

Au travers de ce protocole HTTP, l'appel des différentes méthodes de l'objet distant se fait à l'aide d'un document XML, qui est envoyé et interprété. Ensuite, le service est rendu par l'envoi également d'un autre document XML. Ces documents XML respectent un standard (SOAP) propre au service Web. Le gros avantage de cette démarche, c'est que vous pouvez développer votre service Web avec le langage que vous voulez, de même que pour l'application cliente. Ainsi par exemple, vous pouvez faire votre service web à l'aide de la plate-forme Java EE et développer votre application cliente en .NET. C'est ce que nous appelons : Interopérabilité.

Nous imaginons aisément que toute cette technique est compliquée. Effectivement elle l'est. Toutefois, grâce aux outils de développement comme NetBeans, la fabrication d'un service Web est très facile à mettre en oeuvre. Toute la logique de bas niveau est automatiquement gérée. Dans cette étude, nous aborderons ce sujet au travers de cet environnement afin d'éviter cette complexité. Il faut dire aussi que Java EE 5 réduit considérablement toute la complexité en s'occupant automatiquement de toute l'ossature de bas niveau.

 

Choix du chapitre Qu'est-ce qu'un service web ?

Avant d'expliquer ce qu'est un service web (Web Service), nous allons voir pourquoi ils existent. Auparavant, pour mettre en place des applications distribuées, les développeurs devaient mettre en place des architectures type Corba dans le cas d'applications hétérogènes, RMI en environnement Java ou encore DCOM chez Microsoft. Pour faciliter la coordination de différents systèmes hétérogènes, les grands éditeurs comme SUN, IBM, Oracle... ont décidé d'établir un standard de communication : les services web. Les services web permettent ainsi, ce que nous avons nommé, l'interopérabilité.

Interopérabilité

L'interopérabilité s'appuie sur l'utilisation de standards techniques définis par le W3C :

  1. SOAP : permet la transmission de messages entre objets distants, ce qui veut dire qu'il autorise un objet à invoquer des méthodes d'objets physiquement situés sur une autre machine. Le transfert se fait le plus souvent à l'aide du protocole HTTP, mais peut également se faire sur d'autres protocoles, comme SMTP.
  2. WSDL : langage XML utilisé pour décrire :
    1. Le contrat de services : l'ensemble des opérations disponibles, ainsi que la structure des messages XML échangés (exprimée en XML Schema).
    2. Comment, concrètement, transporter les messages XML sur un ou plusieurs protocole (habituellement SOAP).
    3. La localisation des services.

L'intérêt des services web réside dans la robustesse du protocole HTTP et donc sa capacité à passer plus facilement les pare-feu qu'un autre protocole.

Offrir un service web

Pour offrir un service web avec SOAP il nous faut :

  1. Un fichier décrivant le service ( Les services distants sont décrits dans un fichier WSDL).
  2. Un programme réalisant le service (Objets distants EJB ou Servlet sur le serveur d'applications).

Révolution des services web

SOA

Le terme SOA - Service Oriented Architecture définit une architecture logicielle à base de services. Un service désigne une action exécutée par un composant fournisseur à l'attention d'un composant consommateur.

B2A, B2B, B2C, C2C

Ces sigles distinguent le commerce inter-entreprise (Business to Business ou B2B) du commerce avec les particuliers (Business to Consumer ou B2C). Il y a aussi les échanges entreprises-administrations (Business to Administration ou B2A) et ceux entre particuliers comme les enchères, petites annonces, etc. (Consumer to Consumer ou C2C).

URL, URI, URN, URC

URL (Uniform Resource Locator) est une chaîne de caractères servant à localiser des resources consultable à l'aide d'un navigateur Les URL sont un sous-ensemble d'URI (Unified Resource Identifier), qui est un mode d'adressage élaboré de ressources (utilisé notamment pour le Web. Plus précisément, l'URI est un élément générique qui se décline en trois sous-ensembles :

- L'URN (Uniform Resource Name) qui permet un nommage unique et permanent (même si la ressource devient inaccessible).

- L'URC (Uniform Resource Characteristic) qui décrit les caractéristiques de la ressource

- L'URL qui donne sa localisation.

XML-RPC

XML-RPC (Remote Procedure Call) est un protocole qui a été conçu pour permettre à des structures de données complexes d'être transmises, exécutées et renvoyées très facilement sur des plate-formes hétérogènes. XML-RPC est l'ancêtre de SOAP.

Annuaire UDDI

Un annuaire UDDI est constitué de pages blanches (nom de l'entreprise, adresse, contacts), jaunes (secteurs d'affaires relatifs au service web) et vertes (informations techniques des services web proposés).

W3C

Le World Wide Web Consortium (W3C) est un consortium promouvant la compatibilité des technologies web telles que HTML, XHTML, XML, RDF, CSS, SOAP, WSDL, UDDI, etc.

Design pattern Proxy

Le proxy, très utilisé pour la gestion d'objet distribués, ajoute un niveau de redirection vers une méthode d'un objet. L'idée est de construire un Proxy capable de communiquer avec un objet distant sans que l'appelant fasse de différence entre un accès local ou un accès distant.

JAX-RPC

JAX-RPC (Java API for XML-based Remote Procedure Call) est une API permettant de créer des services clients web basés sur XML et RPC.

 

Souvent décrit comme le nouveau modèle de référence pour les systèmes d'information, les services web permettent à des applications de dialoguer à distance, et ceci indépendamment des plate-formes et des langages sur lesquels elles reposent. Pour communiquer, les services web s'appuient sur les standards que nous venons d'évoquer. Cette communication est basée sur le principe de demandes et de réponses (en fait, des messages XML) transportés par le protocole HTTP.

Les services web sont aujourd'hui incontournables et se présentent comme le nouveau paradigme des architectures logicielles ou architectures orientées services (SOA). Cette technologie tend à s'imposer comme le nouveau standard en termes d'intégration et d'échanges B2B. Grâce aux services web, les applications peuvent être vues comme un ensemble de services métier, structurés et dialoguant selon un standard, plutôt qu'un ensemble d'objets et de méthodes.

 

Choix du chapitre Les standards autour des services web

Les services web s'appuient sur un ensemble d'API et de protocoles standardisant l'invocation de composants applicatifs. Ils sont décrit en XML par des documents WSDL qui précisent les méthodes pouvant être invoquées, leurs signatures et les points d'accès du service (URL, port). UDDI est le standard de recherche de ces services et SOAP le protocole utilisé pour échanger ces informations.

Java EE 5 a considérablement simplifié le développement des services web. Nous n'aurons donc pas à manipuler directement tous ces standards. Il est cependant important d'en connaître la définition et leur rôle dans l'architecture globale des services web.

SOAP

Inspiré de XML-RPC, SOAP (Simple Object Access Protocol) est le protocole utilisé par les services web. Fondé sur XML, ce protocole autorise l'interopérabilité avec différents environnements logiciels quelle que soit leur plate-forme d'exécution (comme DCOM ou Corba IIOP). En effet, SOAP permet la transmission de messages entre objets distants, en invoquant des méthodes sur des objets physiquement situés sur une autre machine. Le transfert se fait le plus souvent à l'aide du protocole HTTP.

Le protocole SOAP se décompose en deux parties :

  1. Une enveloppe, contenant des informations sur le message lui-même afin de permettre son acheminement et son traitement ;
  2. Un modèle de données, définissant le format du message, c'est-à-dire les informations à transmettre.

Après avoir présenté le protocole SOAP, décrivons plus précisément sa manipulation. En premier lieu, il est important de noter qu'en pratique SOAP ne se manipule pas directement. En effet, en Java EE 5, l'API utilisée pour échanger des messages est JAX-WS (Java API for XML-based Web Services). Cela nous permet donc de nous concentrer sur le développement et l'invocation de services web, sans se soucier du protocole SOAP.

Voici juste un petit exemple simple pour comprendre le fonctionnement. Soit l'interface suivante :

public interface AgenceDeVoyage {   
   public void faireRéservation(int idBâteau, int numéroCabine, double prix);
}

Voici le document SOAP qui est envoyé par le protocole HTTP pour une réservation particulière :

<?xml version='1.0' encoding='UFT-8'>   
<env:Enveloppe 
    xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:agence ="http://portable:8080/AgenceDeVoyage/" />
    <env:Body>
         <agence:faireRéservation>
                <idBâteau>23</idBâteau>
                <numéroCabine>9395</numéroCabine>
                <prix>5677.56</prix>
         </agence:faireRéservation>
    </env:Body>
</env:Enveloppe>

UDDI

UDDI (Universal Description, Discovery and Integration) est la spécification définissant la manière de publier et de retrouver des services web. C'est un annuaire qui offre des mécanismes d'enregistrement et de recherche de services web développés et publiés par des entreprises. UDDI fournit des informations sur l'auteur de services web (adresse, contact...), sur la classification (société informatique, hôpital, ...) et sur les moyens techniques permettant de les invoquer. Chaque entreprise peut avoir son propre annuaire UDDI pour publier ses services, amis elle peut aussi utiliser des annuaires publics comme ceux de Microsoft, IMB ou SAP.

UDDI n'est pas fondamentale aux services web comme l'est XML, SOAP ou WSDL. Nous n'aborderons donc pas la publication de services dans cette étude.

WSDL

WSDL (Web Service Description Language) est le format XML spécifié par le W3C permettant de définir un service web qui utilise le protocole SOAP. Nous exposons ainsi au format XML la signature d'un service web accessible sur Internet. Cette signature inclut les opérations exposées, le type de ses paramètres d'entrées-sorties, et l'adresse réseau à laquelle nous pourrons l'invoquer.

UDDI permet de retrouver un service web, et WSDL de décrire ses méthodes.
.

En réalité, WSDL est scindé en deux parties que nous appelons abstraite et concrète. La signature du service, ses méthodes et ses paramètres sont décrits de manière abstraite. Cette partie est ensuite liée à un protocole de communication et à un format de messages concrets. Ainsi, la partie abstraite est totalement découplée de la manière concrète permettant d'appeler le service.

Extrait d'un fichier WSDL du service Web que nous allons mettre en oeuvre :

<?xml version="1.0" encoding="UTF-8"?>
<definitions  targetNamespace="http://photos/" name="StockerPhotosService">

   <types>
      <xsd:schema>
          <xsd:import 
             namespace="http://photos/" 
             schemaLocation="http://portable:8080/StockerPhotosService/StockerPhotos?xsd=1" />

      </xsd:schema>
   </types>

   <message name="stocker">
      <part name="parameters" element="tns:stocker"></part>
   </message>
...
   <service name="StockerPhotosService">
      <port name="StockerPhotosPort" binding="tns:StockerPhotosPortBinding">
         <soap:address location="http://portable:8080/StockerPhotosService/StockerPhotos"></soap:address>
      </port>
   </service>
</definitions>

Cet extrait de document WSDL commence par l'en-tête <definitions>. Cet élément peut prendre plusieurs attributs facultatifs qui définissent des noms de domaines dans la suite du document. Dans notre exemple, la définition reçoit le nom StockerPhotosService. Le service Web portant ce même nom (<service>) peut être invoqué à partir de l'URL spécifié (http://portable:8080/StockerPhotosService/StockerPhotos)

Nous ne nous attarderons pas sur WSDL car, comme vous le découvrirez plus loin, ce document est généré automatiquement et n'a pas à être développé manuellement.

JAX-WS 2.0

JAX-WS est la nouvelle appellation de JAX-RPC (Java API for XML Based RPC) qui permet de développer très simplement des services web. JAX-WS fournit un ensemble d'annotations pour mapper la correspondance Java-WSDL. Il suffit pour cela d'annoter directement les classes Java qui vont représenter le service web. En ce qui concerne le client, JAX-WS permet d'utiliser une classe proxy pour appeler un service distant et masquer la complexité du protocole. Ainsi, ni le client ni le serveur n'ont besoin de générer ou de parser les messages SOAP. JAX-WS s'occupe de ces traitements de bas niveau.

Dans l'exemple ci-dessous, une classe Java utilise des annotations JAX-WS qui vont permettre par la suite de générer le document WSDL. Le document WSDL est auto-générer par le serveur d'application au moment du déploiement :

@WebService()
public class StockerPhotos {

  @WebMethod
  public void stocker(String nomFichier, Byte [] octets) {
     ...
  }
 ...
}  

JAXB 2.0

JAX-WS s'appuie sur l'API JAXB 2.0 pour tout ce qui concerne la correspondance entre document XML et objets Java. Un graphe d'objets est constitué suivant les éléments constituant le document XML. JAXB (Java Architecture for XML Binding) facilite la correspondance bidirectionnelle en fournissant un niveau d'abstraction plus élevé que SAX ou DOM et en s'appuyant sur les annotations.

Par exemple, si nous désirons obtenir une représentation XML de la classe Client, il suffit de l'annoter avec @javax.xml.bind.annotation.XmlRootElement. D'autres annotations permettent de spécifier qu'un attribut est un identifiant avec @XmlID ou de renommer un attribut (e-mail au lieu de email, par exemple) avec @XmlAttribute :

@XmlRootElement
public class Client {
  @XmlID
  private String id;
  private String nom;
  private String prénom;
  @XmlAttribute(name="e-mail")
  private String email; 
}  

Ces annotations permettent alors de générer le document XML suivant à partir de la classe, et inversement :

<?xml version="1.0" encoding="UTF-8"?>
<client>
   <id>3456</id>
   <nom>REMY</nom>
   <prénom>Emmanuel</prénom>
   <e-mail>emmanuel.remy@orange.fr</e-mail>
</client>

Mapping entre WSDL et JAX-WS/JAXB

Le document WSDL qui est généré par le serveur d'applications respecte le standard XML Schema. Ce standard permet de définir précisément les types de chaque élément constituant le document XML. Ces types sont important puiqu'ils définissent les paramètres de chaque méthode de la classe qui constitue le service web. Ainsi le mapping entre une classe Java est son correspond XML peut se faire correctement grâce à JAXB en respectant les types proposés.

Vous avez justement ci-dessous la correspondance entre les types XML Schema et les types Java :

XML Schema Types Java
xsd:byte Byte
xsd:Boolean Boolean
xsd:short Short
xsd:int Integer
xsd:long Long
xsd:float Float
xsd:double Double
xsd:string java.lang.String
xsd:dateTime java.util.Calendar
xsd:integer java.math.BigInteger
xsd:decimal java.math.BigDecimal
xsd:QName java.xml.namespace.QName
xsd:base64Binary Byte [ ]
xsd:hexBinary Byte [ ]

Vous remarquez que le mapping proposé par le couple JAX-WS/JAXB utilise systématiquement les classes enveloppes plutôt que les types primitifs. Vous pouvez malgré tout utiliser les types primitifs pour les paramètres de vos méthodes puisque l'auto-boxing entre la classe enveloppe et son type primitif se réalise automatiquement, sauf pour les tableaux d'octets. En effet, un Byte[] est totalement différent d'un byte[].

Généralement, la définition de tous les types utilisés par le service web, notamment par les paramètres des méthodes accessibles sont placés dans un fichier à part et qui correspond justement au Schema prévu. Ainsi, nous aurons donc deux fichiers, le fichier WSDL qui porte l'extension <*.wsdl> qui fera lui même référence au fichier XML Schema dont l'extension est généralement <*.xsd>.

 

Choix du chapitre Comment développer un service Web

En réalité, le développement d'un service web est relativement simple bien que plusieurs technologies soient mises en oeuvre. Telle est la force de Java EE. Pour cela, il existe plusieurs phases de génération de code qui entre en jeu et qui mettent en oeuvre toute la tuyauterie technique. Le développement et l'utilisation d'un service web comporte quatre phases :

  1. Développement du service web proprement dit.
  2. Génération des artefacts côté serveur.
  3. Génération des artefacts côté client.
  4. Appel du service web chez le client.

Un artefact est composé de l'ensemble des documents nécessaires à un service web. Nous pouvons citer par exemple le document WSDL ou encore les classes Java qui formeront les messages SOAP d'échanges XML.

Développer la classe du service web

La première chose à faire consiste à créer notre service web. Il s'agit tout simplement d'une classe qui sera déployée dans le serveur d'applications. Il existe deux moyen d'implémenter un service web. Le premier repose sur les servlets où une simple classe annotée @WebService est déployée dans un conteneur Web (dans un <.war>). L'autre moyen repose sur les EJB sans état qui sont annotées à la fois @Stateless et @WebService, puis déployés dans un conteneur EJB (dans un <.jar> ou <.ear>).

Afin d'illustrer mes propos, nous allons reprendre le projet de l'étude précédente. Nous avions effectivement mis en oeuvre un système qui permet d'archiver des photos numériques. Nous sommes passé par un bean session qui offre toutes méthodes nécessaires et dont le code est présenté ci-dessous. Nous allons reprendre ce bean session pour qu'il devienne un service web.

photos.ArchivagePhotosBean.java
package photos;
...
@Stateless
public class ArchivagePhotosBean implements ArchivagePhotosRemote {  
 public void stocker(String intitulé, byte[] octets) throws IOException {
   File fichier = new File(répertoire + intitulé);
   FileOutputStream stockage = new FileOutputStream(fichier);
   stockage.write(octets);
   stockage.close();
 }

 public String[] liste() {
   return new File(répertoire).list();
 }

 public void supprimer(String nom) {
   File fichier = new File(répertoire+nom);
   fichier.delete();       
 }
    
 public byte[]  getPhoto(String nom) throws IOException {
   File fichier = new File(répertoire+nom);
   byte[] octets = new byte[(int)fichier.length()];
   FileInputStream photo = new FileInputStream(fichier);
   photo.read(octets);
   photo.close();
   return octets;
 }
}

Annotations JAX-WS

Comme nous l'avons découvert dans le chapitre précédent, les annotations JAX-WS sont spécifiques aux services web. Elles permettent d'agir sur la structure d'un document WSDL en modifiant certains paramètres du service ou des méthodes qui le compose. Dans cette rubrique, nous allons décrire plus précisément le comportement de ces annotations.

Le service

L'annotation principale pour définir un service web est @javax.jws.WebService. Cette annotation s'applique à une classe et utilise éventuellement plusieurs attributs qui permettent, par exemple, de spécifier la localisation d'un service web.

package photos;

import javax.jws.*;
import javax.ejb.Stateless;

@WebService()
@Stateless
public class StockerPhotos {
...
}  

Voici La liste des attributs qu'il est possible de mettre en oeuvre :

  1. name : définit le nom du service web. Ce nom se trouve alors dans le fichier WSDL dans l'attribut name de la balise <portType>. La valeur par défaut est le nom de la classe d'imlémentation, ici StockerPhotos.
  2. serviceName : définit le nom du service. Ce nom se trouve dans le fichier WSDL dans l'attribut name de la balise <service>. La valeur par défaut est le nom de la classe d'implémentation suivie du suffixe "Service", ici donc StockerPhotosService.
  3. wsdlLocation : définit l'URL, qu'elle soit relative ou absolue, d'un fichier WSDL existant. Par défaut, ce fichier est auto-généré lors du déploiement.
  4. endpointInterface : définit le nom complet de l'interface "endpoint" définissant les méthodes du service web. Cela permet au développeur de séparer le "contrat" (l'interface) de l'implémentation. L'implémentation de cette interface par le service n'est pas requise. L'intérêt de cette solution est également d'affecter des annotations de mapping Java vers le WSDL dans l'interface et non dans la classe d'implémentation.

    package photos;
    
    import javax.jws.*;
    import javax.ejb.Stateless;
    
    @WebService
    public interface StockerPhotosEndPoint {
    ...
    } 
    
    @WebService(endpointInterface = "StockerPhotosEndPoint")
    @Stateless
    public class StockerPhotos implements StockerPhotosEndPoint {
    ...
    }

Cette annotation peut être utilisée sur une classe (comme dans notre exemple) ou sur l'interface métier dite "endpoint". Dans le deuxième cas, il faut impérativement indiquer l'interface utilisée via l'attribut endpointInterface dans la classe d'implémentation.

La méthode

Si une classe est annotée par @WebService, alors par défaut, toutes ces méthodes publiques peuvent être appelées. Si vous souhaitez restreindre cette règle et ne publier que certaines méthodes, nous pouvons alors utiliser l'annotation @javax.jws.WebMethod. Celle-ci permet aussi de modifier certains attributs par défaut.

package photos;

import javax.jws.*;
import javax.ejb.Stateless;

@WebService()
@Stateless()
public class StockerPhotos {

  @WebMethod(operationName = "stocker")
  @Oneway
  public void stocker(String nomFichier, Byte [] octets) {
   ...
  }
...
}  

Cette annotation @WebMethod est utilisée pour déclarer les méthodes accessibles par le service web (nous parlons d'opération dans le protocole SOAP). Voici les détails des attributs qu'elle contient :

  1. operationName : définit la valeur de l'attribut name de la balise <operation> dans le fichier WSDL. Celui-ci représente le nom de l'opération.
  2. exclude : marque une méthode comme étant non disponible par le service web. Ce paramètre est utilisé lorsque nous souhaitons masquer une méthode héritée, par exemple.

Une méthode qui n'a pas de paramètre de retour peut être annotée par @Oneway.
.

Les paramètres de la méthode

Les paramètres de la méthode ainsi que la valeur de retour peuvent aussi être changées. L'annotation @javax.jws.WebParam permet de contrôler la génération du WSDL qui concerne les paramètres de la méthode.

package photos;

import javax.jws.*;
import javax.ejb.Stateless;

@WebService()
@Stateless()
public class StockerPhotos {

  @WebMethod(operationName = "stocker")
  @Oneway
  public void stocker(@WebParam(name = "nomFichier") String nomFichier, 
                                   @WebParam(name = "octets") Byte [] octets) {
    ...
  }
...
}  

Ainsi, l'annotation @WebParam permet de préciser des informations concernant les arguments des opérations définies dans le fichier WSDL. Voici le détail des attributs disponibles :

  1. name : définit le nom du paramètre qui sera utilisé dans le fichier WSDL. Par défaut, le nom de l'argument (du code Java) est utilisé.
  2. header : indique si la définition du paramètre se trouve dans l'en-tête (header) plutôt que dans le corps (body). La valeur par défaut étant false, la définition se trouve donc dans le corps.
  3. mode : indique si le paramètre est utilisé en entrée, en sortie ou les deux. Pour spécifier ce type, il faut utiliser l'énumération WebParam.MODE (Valeurs : IN, OUT, BOTH).
  4. targetNamespace : définit le namespace à utiliser. Par défaut, nous utilisons un namespace vide. Cet attribut est à utiliser si le paramètre est mappé dans l'en-tête.

L'annotation @WebResult est presque identique, mais elle annote la valeur de retour de la méthode.
.

Conclusion sur la mise en oeuvre de notre service web

Voici le résultat obtenu en tenant compte de ces différentes annotations :

photos.StockerPhotos
package photos;

import java.io.*;
import javax.jws.Oneway;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.ejb.Stateless;

@WebService()
@Stateless()
public class StockerPhotos {
   
  private final String répertoire = "J:/Photos/";
  @WebMethod(operationName = "stocker")
  @Oneway
  public void stocker(@WebParam(name = "nomFichier") String nomFichier, 
                      @WebParam(name = "octets") Byte [] octets) {
    try {
      File fichier = new File(répertoire + nomFichier);
      FileOutputStream stockage = new FileOutputStream(fichier);
      byte[] bytes = new byte[octets.length];
      for (int i = 0; i < bytes.length; i++) bytes[i] = octets[i];
      stockage.write(bytes);
      stockage.close();
    } 
    catch (IOException ex) {
      System.err.println("Problème de stockage");
    } 
  }

  @WebMethod(operationName = "liste")
  public java.lang.String [] liste() {
    return new File(répertoire).list();
  }

  @WebMethod(operationName = "supprimer")
  @Oneway
  public void supprimer(@WebParam(name = "nomFichier") String nomFichier) {
    File fichier = new File(répertoire+nomFichier);
    fichier.delete();
  }

  @WebMethod(operationName = "getPhoto")
  public Byte [] getPhoto(@WebParam(name = "nomFichier") String nomFichier) {
    try {
      File fichier = new File(répertoire + nomFichier);
      byte[] octets = new byte[(int) fichier.length()];
      FileInputStream photo = new java.io.FileInputStream(fichier);
      photo.read(octets);
      photo.close();
      java.lang.Byte[] bytes = new java.lang.Byte[octets.length];
      for (int i = 0; i < bytes.length; i++)  bytes[i] = octets[i];   
      return bytes;    
    } 
    catch (IOException ex) {
      System.err.println("Problème de lecture");
      return null;
    }      
  }
}  

Voilà, tout est dit. N'est-ce pas magique ? Si les valeurs par défaut vous conviennent, vous pouvez même limiter les annotations à la seule @WebService. Contrairement aux EJB classiques, un service web n'a pas besoin d'implémenter une interface.

Voici donc le même web service en prenant les valeurs par défaut :

photos.StockerPhotos
package photos;

import java.io.*;
import javax.jws.Oneway;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebService;
import javax.ejb.Stateless;

@WebService()
@Stateless()
public class StockerPhotos {
   
  private final String répertoire = "J:/Photos/";
  public void stocker(String nomFichier, Byte [] octets) {
    try {
      File fichier = new File(répertoire + nomFichier);
      FileOutputStream stockage = new FileOutputStream(fichier);
      byte[] bytes = new byte[octets.length];
      for (int i = 0; i < bytes.length; i++) bytes[i] = octets[i];
      stockage.write(bytes);
      stockage.close();
    } 
    catch (IOException ex) {
      System.err.println("Problème de stockage");
    } 
  }

  public java.lang.String [] liste() {
    return new File(répertoire).list();
  }

  public void supprimer(String nomFichier) {
    File fichier = new File(répertoire+nomFichier);
    fichier.delete();
  }

  public Byte [] getPhoto(String nomFichier) {
    try {
      File fichier = new File(répertoire + nomFichier);
      byte[] octets = new byte[(int) fichier.length()];
      FileInputStream photo = new java.io.FileInputStream(fichier);
      photo.read(octets);
      photo.close();
      java.lang.Byte[] bytes = new java.lang.Byte[octets.length];
      for (int i = 0; i < bytes.length; i++)  bytes[i] = octets[i];   
      return bytes;    
    } 
    catch (IOException ex) {
      System.err.println("Problème de lecture");
      return null;
    }      
  }
}  

Nous avons passé beaucoup de temps à expliquer pas mal de chose mais en réalité vous remarquez que nous n'avons pas grand chose à faire. Dans l'absolu, il suffit juste de placer l'annotation @WebService sur la classe bu bean désirée et ensuite de la déployer sur le serveur d'applications et tout s'exécute automatiquement. Effectivement, c'est assez magique !

Générer les artefacts serveurs

Cette classe développée, StockerPhotos doit générer les artefacts de son service, c'est-à-dire le document WSDL et les classes Java qui formeront les messages d'échanges XML suivant le protocole SOAP. Tout est auto-généré par le serveur d'applications.

En réalité, le serveur d'applications utilise un utilitaire pour cela qui dans le cas de GlassFish se nomme wsgen. Nous verrons comment faire à travers la plate-forme de développement NetBeans dans le chapitre suivant.

A partir de la classe StockerPhotos, l'utilitaire wsgen génère les éléments suivants :

Le document WSDL décrivant le service web et son schéma XSD

StockerPhotos.wsdl
<?xml version="1.0" encoding="UTF-8"?>
<definitions 
   xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" 
   xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" 
   xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
   xmlns:tns="http://photos/" 
   xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
   xmlns="http://schemas.xmlsoap.org/wsdl/" 
   targetNamespace="http://photos/" 
   name="StockerPhotosService">
   <types>
      <xsd:schema>
         <xsd:import 
           namespace="http://photos/" 
           schemaLocation="http://portable:8080/StockerPhotosService/StockerPhotos?xsd=1">
         </xsd:import>
      </xsd:schema>
   </types>
   <message name="stocker">
      <part name="parameters" element="tns:stocker"></part>
   </message>
   <message name="liste">
      <part name="parameters" element="tns:liste"></part>
   </message>
   <message name="listeResponse">
      <part name="parameters" element="tns:listeResponse"></part>
   </message>
   <message name="supprimer">
      <part name="parameters" element="tns:supprimer"></part>
   </message>
   <message name="getPhoto">
      <part name="parameters" element="tns:getPhoto"></part>
   </message>
   <message name="getPhotoResponse">
      <part name="parameters" element="tns:getPhotoResponse"></part>
   </message>
   <portType name="StockerPhotos">
      <operation name="stocker">
         <input message="tns:stocker"></input>
      </operation>
      <operation name="liste">
         <input message="tns:liste"></input>
         <output message="tns:listeResponse"></output>
      </operation>
      <operation name="supprimer">
         <input message="tns:supprimer"></input>
      </operation>
      <operation name="getPhoto">
         <input message="tns:getPhoto"></input>
         <output message="tns:getPhotoResponse"></output>
      </operation>
   </portType>
   <binding name="StockerPhotosPortBinding" type="tns:StockerPhotos">
      <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document"></soap:binding>
      <operation name="stocker">
         <soap:operation soapAction=""></soap:operation>
         <input><soap:body use="literal"></soap:body></input>
      </operation>
      <operation name="liste">
         <soap:operation soapAction=""></soap:operation>
         <input><soap:body use="literal"></soap:body></input>
         <output><soap:body use="literal"></soap:body></output>
      </operation>
      <operation name="supprimer">
         <soap:operation soapAction=""></soap:operation>
         <input><soap:body use="literal"></soap:body></input>
      </operation>
      <operation name="getPhoto">
         <soap:operation soapAction=""></soap:operation>
         <input><soap:body use="literal"></soap:body></input>
         <output><soap:body use="literal"></soap:body></output>
      </operation>
   </binding>
   <service name="StockerPhotosService">
      <port name="StockerPhotosPort" binding="tns:StockerPhotosPortBinding">
         <soap:address location="http://portable:8080/StockerPhotosService/StockerPhotos"></soap:address>
      </port>
   </service>
</definitions>

Ce fichier fait ensuite référence à la définition des types qui sont décrit dans le fichier correspondant au XML Schema et qui porte l'extension <.xsd>. Ce fichier est également auto-généré par le serveur d'applications.

StockerPhotos.xsd
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:tns="http://photos/" 
           xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0" targetNamespace="http://photos/">

<xs:element name="getPhoto" type="tns:getPhoto"></xs:element>

<xs:element name="getPhotoResponse" type="tns:getPhotoResponse"></xs:element>

<xs:element name="liste" type="tns:liste"></xs:element>

<xs:element name="listeResponse" type="tns:listeResponse"></xs:element>

<xs:element name="stocker" type="tns:stocker"></xs:element>

<xs:element name="supprimer" type="tns:supprimer"></xs:element>

<xs:complexType name="supprimer">
<xs:sequence>
<xs:element name="nomFichier" type="xs:string" minOccurs="0"></xs:element>
</xs:sequence>
</xs:complexType>

<xs:complexType name="getPhoto">
<xs:sequence>
<xs:element name="nomFichier" type="xs:string" minOccurs="0"></xs:element>
</xs:sequence>
</xs:complexType>

<xs:complexType name="getPhotoResponse">
<xs:sequence>
<xs:element name="return" type="xs:byte" nillable="true" minOccurs="0" maxOccurs="unbounded"></xs:element>
</xs:sequence>
</xs:complexType>

<xs:complexType name="liste">
<xs:sequence></xs:sequence>
</xs:complexType>

<xs:complexType name="listeResponse">
<xs:sequence>
<xs:element name="return" type="xs:string" nillable="true" minOccurs="0" maxOccurs="unbounded"></xs:element>
</xs:sequence>
</xs:complexType>

<xs:complexType name="stocker">
<xs:sequence>
<xs:element name="nomFichier" type="xs:string" minOccurs="0"></xs:element>
<xs:element name="octets" type="xs:byte" nillable="true" minOccurs="0" maxOccurs="unbounded"></xs:element>
</xs:sequence>
</xs:complexType>
</xs:schema>

Les différentes classes qui vont permettre les échanges de messages XML suivant le protocole SOAP
  1. La classe Stocker (du nom de la première méthode du service) correspondant aux paramètres passés au service. Cette classe ne contient que les deux attributs (qui sont une image des paramètres de la méthode) ainsi que les accesseurs.



    Comme elle est responsable du transport des paramètres au format XML, cette classe utilise les annotations JAXB.

    @XmlRootElement(name="Stocker")
    public class Stocker {
      @XmlElement(name="nomFichier")
      private String nomFichier;
      @XmlElement(name="octets")
      private List<Byte> octets; 
      ...
    } 
    Remarquez au passage que le paramètre Byte[ ] est devenu List<Byte>, ce qui n'est pas illogique puisqu'à priori le système ne connaît pas la taille de la collection d'éléments de type Byte. Il faut donc que ce soit une collection variable de type List et non pas un tableau. Ceci dit, rien n'empèche comme nous l'avons fait de déclarer tout de même un tableau. Toutefois, il faudra en tenir compte du changement effectué lorsque nous allons mettre en oeuvre l'application cliente.
  2. La classe Liste (du nom de la deuxième méthode du service).

  3. La classe ListeResponse (du nom de la deuxième méthode suffixée par Response) correspond à la valeur de retour.


    Cette classe est elle aussi annotée par JAXB.

    @XmlRootElement
    public class ListResponse {
      @XmlElement 
      private List<String> _return;
      ...
    }
  4. La classe Supprimer (du nom de la troisième méthode du service).


  5. La classe GetPhoto (du nom de la quatrième méthode du service).


  6. La classe GetPhotoResponse (du nom de la quatrième méthode suffixée par Response).

Générer les artefacts clients

StockerPhotos a développé son service web, généré ses artefacts serveurs et a déployé le tout sur son serveur à une URL donnée :

http://portable:8080/StockerPhotosService/StockerPhotos

Cette URL est structurée de la façon suivante :

  1. portable:8080 : D'une part la localisation du serveur d'applications avec le numéro du service correspondant au serveur web (puisque nous utilisons le protocole HTTP comme support de documents XML). Dans le cas de GlassFish, le numéro de port est 8080.
  2. StockerPhotosService : le nom du service web. Vous remarquez, comme nous l'avons déjà évoqué plus haut, que ce nom possède le suffixe Service associé au nom de la classe qui rend ce service.
  3. StockerPhotos : le nom de la classe représentant le service web.

Maintenant, pour que l'application fenêtrée cliente puisse accéder à ce service, il lui faut générer les artefacts côté client. Cette opération se fait, avec GlassFish, via l'utilitaire wsimport. Celui-ci prend en paramètres l'URL du WSDL du service web :

wsimport http://portable:8080/StockerPhotosService/StockerPhotos?WSDL

Nous verrons comment faire à travers la plate-forme de développement NetBeans dans le chapitre suivant.
.

A partir du WSDL, wsimport génère les éléments suivant :

  1. StockerPhotosService : est la classe principale qui est utilisée dans le code de l'application cliente.


    Celle-ci retourne l'interface StockerPhotos, qui possède la même signature que le service web, au moyen de la méthode getStockerPhotosPort() - la signature de cette méthode est toujours de la forme suivante getNomDeLaClasseDuServiceWebPort() :



    Remarquez, encore une fois, le changement opéré sur les paramètres possédant une collection (List<Byte> au lieu de Byte [ ]).
    .
  2. Une fabrique ObjectFactory pour créer les six mêmes classes (Stocker, Liste, ListeResponse, Supprimer, GetPhoto et GetPhotoResponse). Grâce aux annotations JAXB, ces classes générent les messages XML.

Appeler un service web

Une fois le service web déployé et les artefacts clients générés, il est temps de les utiliser pour invoquer le service web depuis l'application cliente fenêtrée photos.EnvoyerPhotos.

Nous reprenons la même interface graphique avec donc le même code que lors de l'étude précédente, en prenant en compte cette fois-ci les modifications nécessaires pour implémenter l'appel au service web.

photos.EnvoyerPhotos
package photos;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import javax.imageio.*;
import javax.naming.*;
import java.io.*;
import java.util.*;

public class EnvoyerPhotos extends JFrame implements ActionListener {
  private String répertoire = "J:/Stockage/";
  private static StockerPhotos archivage;
   
  private String[] listeLocal;
  private String[] listeServeur;
  private Panneau panneauPhoto = new Panneau();
  private JComboBox choixLocal;
  private JComboBox choixServeur;
  private JButton envoyer = new JButton("Stocker");
  private JButton supprimer = new JButton("Supprimer");
  private JPanel panneauNord = new JPanel();
  private JPanel panneauSud = new JPanel();

  public EnvoyerPhotos() throws Exception {
    listeLocal = new File(répertoire).list();
    choixLocal = new JComboBox(listeLocal);
    java.util.List<String> listeFichiers = archivage.liste();
    listeServeur = new String[listeFichiers.size()];
    listeFichiers.toArray(listeServeur);
    choixServeur = new JComboBox(listeServeur);
    panneauPhoto.change(ImageIO.read(new File(répertoire + choixLocal.getSelectedItem())));
    choixLocal.addActionListener(this);
    envoyer.addActionListener(this);
    choixServeur.addActionListener(this);
    supprimer.addActionListener(this);
    setSize(500, 500);
    setTitle("Stockage de photos");
    panneauNord.add(new JLabel("Photos en local : "));
    panneauNord.add(choixLocal);
    panneauNord.add(envoyer);
    add(panneauNord, BorderLayout.NORTH);
    panneauSud.add(new JLabel("Sur le serveur : "));
    panneauSud.add(choixServeur);
    panneauSud.add(supprimer);
    add(panneauSud, BorderLayout.SOUTH);       
    add(panneauPhoto);
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setVisible(true);
  }

  public static void main(String[] args) throws Exception {
    archivage = new StockerPhotosService().getStockerPhotosPort();
    new EnvoyerPhotos();
  }

  public void actionPerformed(ActionEvent e) {
    if (e.getSource()==choixLocal) {
      try {
        panneauPhoto.change(ImageIO.read(new File(répertoire + choixLocal.getSelectedItem())));
      } 
      catch (Exception ex) {
        setTitle("Problème de localisation des photos");
      }
    }
    else if (e.getSource()==envoyer) {       
      try {
        File fichier = new File(répertoire+choixLocal.getSelectedItem());
        byte[] octets = new byte[(int)fichier.length()];
        FileInputStream photo = new FileInputStream(fichier);
        photo.read(octets);
        ArrayList<Byte> bytes = new ArrayList<Byte>();
        for (byte octet : octets) bytes.add(octet);   
        archivage.stocker((String)choixLocal.getSelectedItem(), bytes);
        choixServeur.addItem(choixLocal.getSelectedItem());
      }
      catch (Exception ex) {
        setTitle("Problème avec le fichier");
      }
    }
    else if (e.getSource()==choixServeur) {
      try {
        java.util.List<Byte> photo = archivage.getPhoto((String) choixServeur.getSelectedItem());
        Byte[] bytes = new Byte[photo.size()];
        photo.toArray(bytes);
        byte[] octets = new byte[bytes.length];
        for (int i=0; i<octets.length; i++) octets[i] = bytes[i];
        ByteArrayInputStream fluxImage = new ByteArrayInputStream(octets);
        panneauPhoto.change(ImageIO.read(fluxImage));
      } 
      catch (IOException ex) {
        setTitle("Problème avec le serveur");
      }
    }
    else if (e.getSource()==supprimer) {
      archivage.supprimer((String) choixServeur.getSelectedItem());
      choixServeur.removeItem(choixServeur.getSelectedItem());
    }   
  }
}

class Panneau extends JComponent {
  private BufferedImage image;
  private double ratio;
  
  public void change(BufferedImage image) {
    if (image!=null) {
      this.image = image;
      ratio = (double)image.getWidth()/image.getHeight();  
      repaint();
    }
  }
  
  @Override
  protected void paintComponent(Graphics surface) {
    if (image!=null)
      surface.drawImage(image, 0, 0, this.getWidth(), (int)(this.getWidth()/ratio), null);   
  }   
}

Cette fois-ci, c'est beaucoup plus simple à implémenter. En effet, vous n'avez plus besoin de contexte d'initialisation (donc pas besoin de fichier de configuration jndi.properties), ni d'archives propres au serveur d'applications. Vous avez juste besoin de créer un objet de type StockerPhotos qui est l'interface, créée par l'utilitaire wsimport, représentant l'objet distant qui implémente le service web. Cela se fait, nous l'avons vu, par l'intermédiaire de la méthode getStockerPhotosPort() de l'objet StockerPhotosService qui représente réellement le service web. Ensuite, c'est comme pour un bean session, vous avez juste à faire appel aux méthodes de l'interface StockerPhotos pour que le service demandé soit rendu.

Attention, nous devons tenir compte des nouveaux types de valeur et modifier ainsi le codage interne de chacune des méthodes concernées.
.

L'inconvénient d'un service web est que le temps de réponse est, semble-t-il, plus long que lors d'un appel à un bean session directement. Par contre, le gros avantage, c'est que nous pouvons appeler ce service n'importe où, soit en réseau local, soit depuis Internet puisque le protocole de transport est le HTTP.

Au niveau codage, que ce soit sur le serveur, ou que ce soit chez le client, le codage d'un service web est extrêmement simple. Toute l'architecture de bas niveau est automatiquement construite par les différents utilitaires proposés par le serveur d'applications. D'ailleurs, avec une plate-forme telle que NetBeans, vous ne vous rendez même pas compte de l'utilisation de ces utilitaires, tout se fait en tâche de fond.

Exemple de scénario entre le client et le service web qui permet de récupérer une photo stockée dans le serveur

Nous voyons, au travers de cette illustration, l'acheminement des informations lorsque vous désirez visualisée une photo présente sur le serveur :

  1. Vous devez la choisir d'abord au moyen de la ComboBox située au dessous de la fenêtre.
  2. Ceci a pour effet d'affecter les données correspondantes dans la classe GetPhoto.
  3. A partir de ce moment là, et grâce aux annotations JAXB, cette classe génère un document XML qui peut alors transiter par HTTP dans une enveloppe SOAP.
  4. A la réception de ce message XML, le service web utilise l'annotation JAXB pour reconstruire un objet GetPhoto.
  5. Le service web récupère alors la photo désirée suivant les données envoyées et la renvoie au travers de la classe GetPhotoResponse.
  6. En utilisant le même mécanisme, cette classe est transformée en flux XML pour transiter à travers le réseau.
  7. Le résultat arrive enfin chez le client qui le transforme en objet GetPhotoResponse et peut ainsi récupérer la valeur de retour, c'est-à-dire la photo elle-même que l'application cliente peut alors afficher.

 

Choix du chapitre Mise en oeuvre du service web à l'aide de NetBeans

Nous allons maintenant construire le service web que nous avons étudié dans le chapitre précédent, ainsi que son application cliente, à l'aide de l'environnement intégré NetBeans. Nous allons suivre les deux phases de développement qui va correspondre à la mise en oeuvre de deux projets. Dans un premier temps, nous allons constituer notre service web sur un module EJB. Ensuite, nous allons mettre en place une application classique Java qui utilisera les compétences de ce service web.

Le service web

La première chose à faire est de construire un nouveau projet de type Entreprise en choisissant la mise en oeuvre d'un module EJB, qui dans notre exemple, se nommera EJBStockerPhotos. Une fois que l'ossature du projet est en place, vous pouvez demander tout de suite la création du service web en faisant un clic bouton droit sur le nom du projet. Un menu contextuel apparaît alors, et vous pouvez donc choisir la rubrique "Web Service...". Il suffit de choisir un nom à votre service web (StockerPhotos) et d'effectuer les réglages nécessaires dans la boîte de dialogue correspondante.

Dans Netbeans, vous trouvez alors deux zones d'éditions différentes :

  1. La première, qui apparaît automatiquement, correspond au fichier WSDL. A l'aide du mode design, vous avez la possibilité de déclarer les méthodes qui vont constituées le service web avec également tous les paramètres nécessaires. Ce mode permet en réalité de placer les différentes annotations JAX-WS.
  2. Attention pour écrire le code interne de chacune de vos méthode, afin que celles-ci puissent rendre tous les services désirés, vous devez ouvrir la deuxième zone d'édition qui correspond réellement au code source du service web. Il faut donc double-cliquer sur le fichier StockerPhotos.java tel que cela vous est présenté dans l'illustration ci-dessus.

Lorsque votre code source est correctement complété, vous avez déjà terminé. Vous n'avez effectivement plus rien à faire si ce n'est de lancer le mode "Run", ce qui va permettre de tout mettre en place.

C'est-à-dire :

  1. Compilation.
  2. Appel de l'utilitaire wsgen pour fabriquer les artefacts côté serveur.
  3. Déploiement du service web sur le serveur d'applications.

Voici d'ailleurs se qui s'affiche dans le mode "Output" :

L'application cliente fenêtrée classique qui fait appel au service web

Pour l'application cliente, il suffit de faire un projet Java classique. Une fois que l'ossature du projet est en place, vous pouvez faire appel à un service web au moyen de la rubrique Web Service Client... que vous pouvez atteindre à l'aide du menu contextuel associé au nom du projet. Il suffit alors, grâce à la boîte de dialogue qui apparaît en conséquence, de localiser le fichier WSDL qui définit le service web demandé.

Dès que vous cliquez sur le bouton "Finish" de cette boîte de dialogue, le système recherche effectivement le fichier WSDL correspondant :

L'utilitaire wsimport entre alors en action et fabrique automatiquement toutes les classes nécessaires :

Dès lors, vous n'avez plus qu'à écrire votre code client. C'est vraiment très simple à mettre en oeuvre.
.

Si vous désirez déployer votre application cliente, voici ci-contre l'archive que vous devez construire.

Rremarquez bien la présence de toutes les classes correspondantes aux artefacts côté client.

Cette fois-ci, nous n'avons plus besoin du fichier de propriétés jndi.properties. Par ailleurs, vous n'avez également plus besoin des archives correspondantes au serveur d'applications utilisé.

Pour terminer, il faut dire que le poids de ce fichier archive ClientWebService.jar est vraiment très léger, ce qui favorise le déploiement, à la fois sur le réseau local, mais surtout sur Internet.