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 6 réduit considérablement toute la complexité en s'occupant automatiquement de toute l'ossature de bas niveau.

Choix du chapitre Présentation des services 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é.

Pour résumer, les services web sont une sorte de logique métier offerte à une application cliente (c'est-à-dire un consomateur de service) via une interface de service. A la différence des objets ou des EJBs, les services web fournissent une interface faiblement couplée en se servant de XML.

Les standards précisent que l'interface à laquelle nous envoyons un message doit définir le format du message de requête et de réponse, ainsi que les mécanismes pour publier et pour découvrir les interfaces du service (une base de registres).

Nous découvrons ici l'interaction d'un service web de façon très abstraite. Le service peut éventuellement enregistrer son interface dans une base de registres (UDDI) afin qu'un consomateur puisse le trouver. Une fois que le consommateur connaît l'interface du service et le format du message, il peut envoyer une requête et recevoir une réponse.

Technologie et protocoles

Les services web nécessitent plusieurs technologies et protocoles pour transporter et transformer les données d'un client vers un service de façon standard. Les plus courants sont les suivants :

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 :

UDDI

Les programmes qui interagissent avec un autre via le Web doivent pouvoir trouver les informations leur permettant de s'interconnecter. UDDI fournit pour cela une approche standardisée permettant de trouver les informations sur un service web et sur la façon de l'invoquer.

UDDI est une base de registres de services web en XML, un peu comme les professionnels peuvent enregistrer leurs services dans les pages jaunes. Cet enregistrement inclut le type du métier, sa localisation géographique, le site web, le numéro de téléphone, etc.

Les autres métiers peuvent ensuite parcourir cette base et retrouver les informations sur un servcie web spécifique, qui contiennent des métadonnées supplémentaires décrivant son comportement et son emplacement. Ces informations sont stockées sous la forme de document WSDL : les clients peuvent lire ce document afin d'obtenir l'information et invoquer le service.

WSDL

La base de registre UDDI pointe vers un fichier WSDL sur Internet, qui peut être téléchargé par les consomateurs potentiels. WSDL est un langage de définition d'interfaces permettant de définir les interactions entre les consomateurs et les services.

C'est donc le composant central d'un service web puiqu'il décrit le type de message, le port, le protocole de communication, les opérations possibles, son emplacement et ce que le client peut en attendre. Vous pouvez considérer WSDL comme une interface Java, mais écrite en XML.

Pour garantir l'interopérabilité, l'interface standard du service web doit être standardisée, afin qu'un consomateur et un producteur puissent partager et comprendre un message. C'est le rôle de WSDL. SOAP, de son côté, définit la façon dont le message sera envoyé d'un ordinateur à l'autre.

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ée (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.

SOAP

SOAP est le protocole standard des services web. Il fournit le mécanisme de communication permettant de connecter les services qui échangent des données au format XML au moyen du protocole réseau - HTTP le plus souvent. Comme WSDL, SOAP repose fortement sur XML : un message SOAP est un document XML contenant plusieurs éléments (une enveloppe, un corps, etc. ).

SOAP permet ainsi 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 :

Voici deux captures de trames qui visualisent l'appel d'une méthode stocker() avec sa réponse.

JAXB

Les services web envoient des requêtes et des réponses en échangeant des messages XML. En Java, il existe plusieurs API de bas niveau pour traiter les documents XML et les schémas XML. La spécification JAXB fournit un ensemble d'API et d'annotations pour représenter les documents XML comme des artéfacts Java représentant des documents XML.

JAXB facilite la désérialisation des documents XML en objets et leur sérialisation en documents XML. Même si cette spécification peut être utilisée pour n'importe quel traitement XML, elle est fortement intégrée aux services web.

Pour en savoir plus sur JAXB.

JAX-WS

JAX-WS définit un ensemble d'API et d'annotations permettant de construire et de consommer des services web en Java. Elle fournit les outils pour envoyer et recevoir des requêtes de services web via SOAP en masquant la complexité du protocole. Ni le consommateur ni le service n'ont donc besoin de produire ou d'analyser des messages SOAP car JAX-WS s'occupe de traitement de bas niveau. JAX-WS dépend lui-même d'autres spécifications comme JAXB que nous venons de découvrir.

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) {
     ...
  }
 ...
}

 

Choix du chapitre Appel d'un service Web

Malgré tous ces concepts, spécifications, standards et organisations, l'écriture et la consommation d'un service web sont très très simples.

Le code suivant, par exemple, présente le code d'un service web qui s'occupe de la gestion du personnel :
session.GestionPersonnel.java
package session;

import entité.Personne;
import java.util.List;
import javax.ejb.*;
import javax.jws.WebService;
import javax.persistence.*;

@WebService
@Stateless
@LocalBean
public class GestionPersonnel {
   @PersistenceContext
   private EntityManager bd;

   public void nouveau(Personne personne) {
     bd.persist(personne);
   }

   public Personne rechercher(Personne personne) {
      return bd.find(Personne.class, personne.getId());
   }

   public void modifier(Personne personne) {
     bd.merge(personne);
   }

   public void supprimer(Personne personne) {
     bd.remove(rechercher(personne));
   }

   public List<Personne> listePersonnels() {
      Query requête = bd.createNamedQuery("toutLePersonnel");
      return requête.getResultList();
   }
}

Comme les entités ou les EJB, un service web utilise le modèle de classe annoté avec une politique de configuration par exception. Si tous les choix par défaut vous conviennent, ceci signifie qu'un service web peut se réduire à une simple classe Java annotée @javax.ws.WebService.

Le service GestionPersonnel propose plusieurs méthodes pour gérer l'ensemble du personnel, savoir nouveau(), rechercher(), modifier(), supprimer() et listePersonnels().

Un objet Personne est échangé entre le consommateur et le service web. Lorsque nous avons décrit l'architecture d'un service web, nous avons vu que les données échangées devaient être des documents XML : nous avons donc besoin d'une méthode pour transformer un objet Java en XML et c'est là que JAXB entre en jeu avec ses annotations et son API. L'objet Personne doit simplement être annoté par @javax.xml.bind.annotation.XmlRootElement pour que JAXB le transforme en XML et réciproquement.

Grâce aux annotations JAXB, il n'est pas nécessaire d'écrire de code de bas niveau pour effectuer l'analyse XML car elle se produit en coulisse - le service web et le consomateur manipulent un objet Java. Le consommateur peut être une classe Java qui crée une instance de Personne puis invoque le service web, comme dans le code suivant :

personnel.Client.java
package personnel;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import ws.*;

public class Client extends JFrame {
    private JToolBar outils = new JToolBar();
    private JTextField nom = new JTextField();
    private JTextField prénom = new JTextField();
    private JFormattedTextField âge = new JFormattedTextField(0);
    private JComboBox téléphones = new JComboBox();
    private JPanel panneau = new JPanel();
    private JComboBox liste = new JComboBox();
    private static GestionPersonnel gestion;
    private Personne personne = new Personne();
    private boolean effacer = true;

    public Client() {
        super("Personne");
        add(outils, BorderLayout.NORTH);
        outils.add(new AbstractAction("Enregistrer") {
            public void actionPerformed(ActionEvent e) {
                personne = new Personne(nom.getText(), prénom.getText(), (Integer)âge.getValue());
                for (int i=0; i<téléphones.getItemCount(); i++)
                    personne.getTelephones().add((String) téléphones.getItemAt(i));
                gestion.nouveau(personne);
                listingPersonnes();
            }
        });
        outils.add(new AbstractAction("Ajout téléphone") {
            public void actionPerformed(ActionEvent e) {
                String téléphone = (String) téléphones.getSelectedItem();
                téléphones.addItem(téléphone);
            }
        });
        outils.add(new AbstractAction("Modifier") {
            public void actionPerformed(ActionEvent e) {
                personne.modifier(nom.getText(), prénom.getText(), (Integer)âge.getValue());
                gestion.modifier(personne);
                listingPersonnes();
            }
        });
        outils.add(new AbstractAction("Supprimer") {
            public void actionPerformed(ActionEvent e) {
                gestion.supprimer(personne);
                listingPersonnes();
            }
        });
        outils.add(new AbstractAction("Effacer") {
            public void actionPerformed(ActionEvent e) {
                personne = new Personne();
                rafraîchir();
            }
        });
        panneau.setLayout(new GridLayout(4, 2));
        panneau.add(new JLabel("Nom :"));
        panneau.add(nom);
        panneau.add(new JLabel("Prénom :"));
        panneau.add(prénom);
        panneau.add(new JLabel("Âge :"));
        panneau.add(âge);
        panneau.add(new JLabel("Téléphones :"));
        panneau.add(téléphones);
        téléphones.setEditable(true);
        add(panneau);
        add(liste, BorderLayout.SOUTH);
        liste.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (!effacer) {
                    personne = (Personne) liste.getSelectedItem();
                    rafraîchir();
                }
            }
        });
        listingPersonnes();
        pack();
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setVisible(true);
    }

    private void listingPersonnes() {
        effacer = true;
        liste.removeAllItems();
        effacer = false;
        for (Personne personne : gestion.listePersonnels())
            liste.addItem(personne);
    }

    private void rafraîchir() {
        nom.setText(personne.getNom());
        prénom.setText(personne.getPrenom());
        âge.setValue(personne.getAge());
        téléphones.removeAllItems();
        for (String téléphone : personne.getTelephones()) téléphones.addItem(téléphone);
    }

    public static void main(String[] args) {
        gestion = new GestionPersonnelService().getGestionPersonnelPort();
        new Client();
    }
}

Bien que ce code soit très simple à comprendre, beaucoup de choses se passent en arrière-plan. Pour que tout ceci fonctionne, plusieurs artéfacts ont été générés automatiquement : un fichier WSDL et des stubs clients qui contiennent toutes les informations pour se connecter à l'URL de service web, la sérialisation de l'objet Personne en XML, l'appel du service web et la récupération des différents résultats.

La partie visible des services web en Java ne manipule pas directement XML, SOAP ou WSDL. Elle est donc très simple à comprendre. Cependant, certaines parties invisibles sont très importantes pour l'interopérabilité.

 

Choix du chapitre JAXB : (Java Architecture for XML Binding)

Comme vous l'avez compris, XML est utilisé pour échanger les données et définir les services web via WSDL et les enveloppes SOAP. Pourtant, dans le code précédent, un consommateur invoquait un service web sans qu'il n'y ait aucune trace de XML car ce consommateur ne manipulait que des interfaces et des objets Java distants qui, à leur tour, gèrent toute la plomberie XML et les connexions réseau.

Nous manipulons des classes Java à un endroit de la chaîne et des documents XML à un autre - le rôle de JAXB est de faciliter cette correspondance bidirectionnelle.

J'ai déjà consacré toute une étude sur JAXB. Vous pouvez vous y reporter. Toutefois, lors de ce chapitre, nous allons nous attacher uniquement à certain aspects de cette technologie que je préfère vous repréciser ici.

JAXB définit un standard permettant de lier les représentations Java à XML et réciproquement. Il gère les documents XML et les définitions des schémas XML (XSD) de façon transparente et orientée objet qui masque la complexité du langage XSD.

Mise à part l'annotation @XmlRootElement, le code suivant est celui d'une classe (ou d'une entité) normale.

Grâce à cette simple annotation et à un mécanisme de sérialisation, JAXB est capable de créer une représentation XML d'une instance de Personne.

Document XML représentant les données d'un personnel de l'entreprise
<?xml version="1.0"  encoding="UTF-8" standalone="yes"?>  
<personne>  
     <id>51</id>  
     <nom>REMY</nom>  
     <prenom>Emmanuel</prenom>  
     <age>71</age>     
     <telephones>05-78-96-45-45</telephones>
     <telephones>06-45-87-85-21</telephones>
     <telephones>04-89-77-11-42</telephones>
</personne>

La sérialisation, ici, consiste à transformer un objet en XML, mais JAXB permet également de faire l'inverse : la désérialisation qui prend ce document XML en entrée et crée un objet Personne à partir des valeurs de ce document.

JAXB peut produire automatiquement le schéma qui valide automatiquement la structure XML du personnel afin de garantir qu'elle est correcte et que les types des données conviennent.

Schéma XML validant le document XML précédent
<?xml version='1.0' encoding='UTF-8'?><!-- Published by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is JAX-WS RI 2.2.1-hudson-28-. -->
<xs:schema xmlns:tns="http://session/" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0" targetNamespace="http://session/">
  <xs:element name="personne" type="tns:personne" />
  <xs:complexType name="personne">
    <xs:sequence>
       <xs:element name="age" type="xs:int" />
       <xs:element name="id" type="xs:long" />
       <xs:element name="nom" type="xs:string" minOccurs="0" />
       <xs:element name="prenom" type="xs:string" minOccurs="0" />
       <xs:element name="telephones" type="xs:string" nillable="true" minOccurs="0" maxOccurs="unbounded" />
    </xs:sequence>
  </xs:complexType>
</xs:schema>

Le document ci-dessus montre le schéma XML (XSD) de la classe Personne. Ce schéma est constitué uniquement d'éléments simples. Vous remarquez que tous les marqueurs sont préfixés par xs (xs:int, xs:long, xs:string, etc.) : ce préfixe est un espace de noms et est défini dans l'élément xmlns (XML namespace) du marqueur d'en-tête du document.

Les espaces de noms créent des préfixes uniques pour les éléments des documents ou des applications qui sont utilisés ensemble. Leur but principal consiste à éviter les conflits qui pourraient survenir lorsqu'un même nom d'élément apparaît dans plusieurs documents. Ceci peut poser un problème non négligeable pour les servcies web car ils manipulent plusieurs documents en même temps (l'enveloppe SOAP, le document WSDL, etc.) : les espaces de noms sont donc très importants pour les services web.

Liaison

L'API JAXB, définie dans le paquetage javax.xml.bind, fournit un ensemble d'interfaces et de classes permettant de produire des documents XML et des classes Java - en d'autres termes, elle relie les deux modèles. Le framework d'exécution de JAXB implémente les opérations de sérialisation et de désérialisation.

  1. La sérialisation (ou marshalling) consiste à convertir les instances des classes annotées par JAXB en représentations XML.
  2. Inversement, la désérialisation (unmarshalling) consiste à convertir une représentation XML en arborescence d'objets.

Les données XML sérialisées peuvent être validées par un schéma XML - JAXB peut produire automatiquement ce schéma à partir d'un ensemble de classes et vice versa.

JAXB fournit également un compilateur de schémas (xjc) et un générateur de schéma (schemagen) - alors que la sérialisation/désérialisation manipule des objets et des documents XML, ce compilateur et ce générateur de schémas manipulent des classes et des schémas XML.

Annotation

Par bien des aspects, JAXB ressemble à JPA. Cependant, au lieu de faire correspondre les objets à une base de données, JAXB les lie à un document XML. Comme JPA, JAXB définit un certain nombre d'annotations (dans la paquetage javax.xml.bind.annotation) afin de configurer cette association et s'appuie sur la configuration par exception pour alléger le travail du développeur.

Comme le montre le code suivant, l'équivalent de @Entity des objets persistants est l'annotation @XmlRootElement de JAXB :
Une classe Personne personnalisée
@XmlRootElement
public class Personne {
   private long id;
   private String nom;
   private String prenom;
   private int age;
...
}

L'annotation @XmlRootElement prévient JAXB que la classe Personne est l'élément racine du document XML. Si cette annotation est absente, JAXB lancera une exception lorsqu'il tentera de sérialiser la classe. Cette dernière est ensuite associée au schéma ci-dessous en utilisant les associations par défaut (chaque attribut est traduit en un élément de même nom)

Schéma XML correspondant à la classe précédente
<?xml version='1.0' encoding='UTF-8'?><!-- Published by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is JAX-WS RI 2.2.1-hudson-28-. -->
<xs:schema xmlns:tns="http://session/" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0" targetNamespace="http://session/">
  <xs:element name="personne" type="tns:personne" />
  <xs:complexType name="personne">
     <xs:sequence>
        <xs:element name="age" type="xs:int" />
        <xs:element name="id" type="xs:long" />
        <xs:element name="nom" type="xs:string" minOccurs="0" />
        <xs:element name="prenom" type="xs:string" minOccurs="0" />
     </xs:sequence>
  </xs:complexType>  
</xs:schema>

A l'aide du marshalling, nous obtenons aisément une représentation XML d'un objet de type Personne. L'élément racine <personne> représente l'objet Personne et inclut la valeur de chaque attribut de la classe.

Document XML représentant les données d'un personnel de l'entreprise
<?xml version="1.0"  encoding="UTF-8" standalone="yes"?>  
<personne>  
     <id>51</id>  
     <nom>REMY</nom>  
     <prenom>Emmanuel</prenom>  
     <age>31</age>     
</personne>

Le code ci-dessous utilise ces annotations spécifiques pour transformer le numéro d'identifiant en attribut (au lieu d'un élément par défaut) et pour renommer l'ensemble des balises du document XML.

Une classe Personne personnalisée
@XmlRootElement
public class Personne {
   @XmlAttribut(required = true)
   private long id;
	  @XmlElement(name = "Nom")
   private String nom;
   @XmlElement(name = "Prénom")
   private String prenom;
   @XmlElement(name = "Âge", defaultValue = "21")
   private int age;
...
}

Cette classe sera donc liée à un schéma différent dans lequel le numéro d'identifiant est un <xs:attribute> obligatoire et l'âge renommé possède une valeur par défaut de 21.

Schéma XML correspondant à la classe précédente
<?xml version='1.0' encoding='UTF-8'?><!-- Published by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is JAX-WS RI 2.2.1-hudson-28-. -->
<xs:schema xmlns:tns="http://session/" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="1.0" targetNamespace="http://session/">
  <xs:element name="personne" type="tns:personne" />
  <xs:complexType name="personne">
     <xs:sequence>
        <xs:element name="Âge" type="xs:int" default="21" />
        <xs:element name="Nom" type="xs:string" minOccurs="0" />
        <xs:element name="Prénom" type="xs:string" minOccurs="0" />
     </xs:sequence>
     <xs:attribute name="id" type="xs:long" use="required" />
  </xs:complexType>  
</xs:schema>
Document XML représentant les données d'un personnel de l'entreprise
<?xml version="1.0"  encoding="UTF-8" standalone="yes"?>  
<personne id="51">  
     <Nom>REMY</Nom>  
     <Prénom>Emmanuel</Prénom>  
     <Âge>21</Âge>     
</personne>

Le tableau ci-dessous énumère les principales annotations de JAXB ; la plupart avec les éléments auxquels elles s'appliquent ; certaines peuvent annoter des attributs, d'autres des classes et certaines peuvent s'appliquer à tout un paquetage (@XmlSchema, par exemple).

Annotation Description
XmlAccessorOrder Contrôler l'ordre des attributs et des propriétés dans la classe.
XmlAccessorType Contrôler si l'attribut ou la propriété de la classe est sérialisé par défaut.
XmlAnyAttribute Convertir une propriété de la classe en un ensemble d'attributs de type jocker dans le document XML.
XmlAnyElement Convertir une propriété de la classe en une description representant l'élément JAXB dans le document XML.
XmlAttachmentRef Marquer un attribut ou une propriété de la classe comme une URI que fait référence à une type MIME particulier.
XmlAttribute Convertir une propriété de la classe en un attribut dans le document XML.
XmlElement Convertir une propriété de la classe en un élément dans le document XML.
XmlElementDecl Associer une fabrique à un element XML.
XmlElementRef Convertir une propriété de la classe en un élément dans le document XML qui est associé à un nom de propriété particulier.
XmlElementRefs Marquer une propriété de la classe qui fait référence aux classes qui possèdente XmlElement ou JAXBElement.
XmlElements Créer un conteneur pour de multiples annotations XmlElement, qui précise ainsi le choix possible parmi celles qui sont proposées.
XmlElementWrapper Créer un élément père dans le document XML pour des collections d'éléments.
XmlEnum Convertir une énumération en une représentation équivalente dans le document XML.
XmlEnumValue Convertir un énumérateur en une représentation équivalente dans le document XML.
XmlID Convertir une propriété de la classe en un ID XML.
XmlIDREF Convertir une propriété de la classe en un IDREF XML.
XmlInlineBinaryData Encoder en base64 dans le document XML.
XmlList Utilisé pour convertir une propriété de la classe vers une liste de type simple.
XmlMimeType Associer un type MIME qui contrôle la représentation XML en rapport avec la propriété de la classe.
XmlMixed Annoter une propriété de la classe qui peut supporter plusieurs types de valeur avec un contenu mixte.
XmlNs Associer un prefix d'un espace de nommage à une URI.
XmlRegistry Marquer une classe comme possédant une ou des méthodes annotées avec XmlElementDecl.
XmlRootElement Associer une classe ou une énumération à un element XML. Très souvent utilisé pour spécifier la racine du document XML.
XmlSchema Associer un espace de nommage à un paquetage.
XmlSchemaType Associer un type Java ou une énumeration à un type défini dans un schéma.
XmlSchemaTypes Conteneur pour plusieurs propriétés annotées par XmlSchemaType.
XmlTransient Marquer une entité pour qu'elle ne soit pas mappée dans le document XML.
XmlType Convertir une classe ou une énumération vers le type spécifié dans Shéma XML correspondant.
XmlValue Mapper une classe vers le type complexe dans le Schéma XML ou vers le type simple suivant le cas.

 

Choix du chapitre La partie immergée de l'iceberg

Même si nous ne manipulons pas explicitement des documents SOAP et WSDL lorsque nous développons avec JAX-WS, il est important de comprendre un peu leur structure. Les services web fournissent deux ingrédients essentiels :

Lorsqu'un consommateur invoque le service web GestionPersonnel, il récupère son WSDL pour connaître son interface et il demande à connaître la liste de tous les agents (le message listePersonnels de SOAP) et reçoit une réponse (le message listePersonnelsResponse de SOAP).

WSDL

Les documents WSDL sont hébergés dans le conteneur de service web et utilisent XML pour décrire ce que fait un service, comment appeler ses opérations et où le trouver. Ce document XML respecte une structure bien établie, formée de plusieurs parties. Le sercice GestionPersonnel, par exemple, utilise les éléments suivants :

Fichier WSDL partiel pour le service web GestionPersonnel
<?xml version='1.0' encoding='UTF-8'?>
                       xmlns:wsp="http://www.w3.org/ns/ws-policy" 
                       xmlns:wsp1_2="http://schemas.xmlsoap.org/ws/2004/09/policy" 
                       xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata" 
                       xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
                       xmlns:tns="http://session/" 
                       xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
                       xmlns="http://schemas.xmlsoap.org/wsdl/" 
                       targetNamespace="http://session/" 
                       name="GestionPersonnelService">

   <types>
      <xsd:schema>
         <xsd:import namespace="http://session/" 
                             schemaLocation="http://214-0:8080/GestionPersonnelService/GestionPersonnel?xsd=1" />
      </xsd:schema>
   </types>

   <message name="modifier">
      <part name="parameters" element="tns:modifier" />
   </message>

   <message name="modifierResponse">
      <part name="parameters" element="tns:modifierResponse" />
   </message>

   <message name="nouveau">
      <part name="parameters" element="tns:nouveau" />
   </message>

   <message name="nouveauResponse">
      <part name="parameters" element="tns:nouveauResponse" />
   </message>

   <portType name="GestionPersonnel">
      <operation name="modifier">
         <input wsam:Action="http://session/GestionPersonnel/modifierRequest" message="tns:modifier" />
         <output wsam:Action="http://session/GestionPersonnel/modifierResponse" message="tns:modifierResponse" />
      </operation>

      <operation name="nouveau">
         <input wsam:Action="http://session/GestionPersonnel/nouveauRequest" message="tns:nouveau" />
         <output wsam:Action="http://session/GestionPersonnel/nouveauResponse" message="tns:nouveauResponse" />
      </operation>
   </portType>

    <binding name="GestionPersonnelPortBinding" type="tns:GestionPersonnel">
         <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document" />
         <operation name="modifier">
             <wsp:PolicyReference URI="#GestionPersonnelPortBinding_modifier_WSAT_Policy" />
             <soap:operation soapAction="" />
             <input>
                 <wsp:PolicyReference URI="#GestionPersonnelPortBinding_modifier_WSAT_Policy" />
                 <soap:body use="literal" />
             </input>
             <output>
                  <wsp:PolicyReference URI="#GestionPersonnelPortBinding_modifier_WSAT_Policy" />
                  <soap:body use="literal" />
             </output>
          </operation>

          <operation name="nouveau">
               <wsp:PolicyReference URI="#GestionPersonnelPortBinding_nouveau_WSAT_Policy" />
               <soap:operation soapAction="" />
               <input>
                     <wsp:PolicyReference URI="#GestionPersonnelPortBinding_nouveau_WSAT_Policy" />
                     <soap:body use="literal" />
               </input>
               <output>
                     <wsp:PolicyReference URI="#GestionPersonnelPortBinding_nouveau_WSAT_Policy" />
                     <soap:body use="literal" />
               </output>
         </operation>
    </binding>
    
    <service name="GestionPersonnelService">
        <port name="GestionPersonnelPort" binding="tns:GestionPersonnelPortBinding">
            <soap:address location="http://214-0:8080/GestionPersonnelService/GestionPersonnel" />
        </port>
    </service>
</definitions>

L'élément <xsd:import namespace> fait référence à un schéma XML qui doit être disponible sur le réseau pour les clients du WSDL.
.

SOAP

Alors que WSDL décrit une interface abstraite du service web, SOAP fournit une implémentation concrète en définissant la structure XML des messages échangés. Dans le cadre du Web, SOAP est une structure de messages pouvant être délivrés par HTTP (ou d'autres protocoles de communication) - la liaison HTTP de SOAP contient quelques en-têtes d'extension HTTP standard.

Cette structure de message est décrite en XML. Au lieu d'utiliser HTTP pour demander une page web à partir d'un navigateur, SOAP envoie un message XML via une requête HTTP et reçoit une réponse HTTP. Un message SOAP est un document XML contenant les éléments suivants :

Seuls l'enveloppe et le corps sont obligatoires. Dans notre exemple, une application cliente appelle le service web pour connaître l'ensemble du personnel de l'entreprise (une enveloppe SOAP pour la requête) et reçoit la liste des agents (une autre enveloppe SOAP pour la réponse).


 

Choix du chapitre JAX-WS : Java API for XML-Based Web Services

Nous venons de découvrir un document WSDL, ainsi qu'une requête et une réponse SOAP. Vous avez pu remarquer que lorsque les services web proposent plusieurs opérations avec des pararamètres complexes, ces documents XML deviennent un véritable cauchemar pour le développeur ; heureusement, JAX-WS leur facilite la vie en masquant cette complexité.

Le document WSDL étant le contrat qui lie le consommateur et le service, il peut servir à écrire le code Java pour ces deux parties : c'est ce que nous appelons la méthode descendante, également méthode par contrat car elle part du contrat (le WSDL) en définissant les opérations, les messages, etc. Lorsque le consommateur et le fournisseur sont d'accord sur le contrat, nous pouvons implémenter les classses en fonction de celui-ci. Metro fournit quelques outils permettant de produire des classes à partir d'un document WSDL.

Dans l'autre approche, la méthode ascendante, la classe de l'implémentation existe déjà et il suffit de créer le WSDL. Là encore, Metro dispose d'outils pour effectuer cette opération. Dans les deux cas, le code doit parfois être ajusté pour correspondre au WSDL ou vice versa, et c'est là que JAX-WS vient à votre secours : grâce à un modèle de développement simple et à quelques annotations, vous pouvez ajuster l'association Java-WSDL.

L'approche ascendante peut produire des applications très inefficaces car les méthodes et les classes Java n'ont aucune idée de la granularité idéale des messages qui circulent sur le réseau. Si la latence est élevée et/ou la bande passante, faible, il est préférable d'utiliser moins de messages, qui seront plus gros : seule l'approche par contrat permet de faire ce choix.

Toute cette étude nous a présenté les concepts et les spécifications des services web en général puis a introduit JAXB et a effleuré la surface des documents WSDL et SOAP. Cependant, les services web suivent le paradigme de facilité de développement de Java EE 6 et n'obligent pas à écrire le moindre code WSDL ou SOAP. Un service web est une simple classe annotée qui doit être déployée dans un conteneur de service web. Toutefois, ce modèle de programmation mérite notre attention.

Le modèle JAX-WS

Comme la plupart des composants de Java EE 6, les services web s'appuient sur le paradigme de la configuration par exception. Seule l'annotation @WebService est nécessaire pour transformer une simple classe en service web, mais cette classe doit respecter les règles suivantes :

Au niveau du service, les systèmes sont définis en terme de messages XML, d'opérations WSDL et de messages SOAP. Cependant, au niveau Java, les applications sont décrites en termes d'objets, d'interfaces et de méthodes. Il est donc nécessaire d'effectuer une traduction des objets Java vers les opérations WSDL. L'environnement d'exécution de JAXB utilise les annotations pour savoir comment sérialiser/désérialiser une classe vers/à partir de XML.

Généralement ces annotations sont cachées au développeur du service web. De même, JAX-WS se sert d'annotations pour déterminer comment sérialiser un appel de méthode vers un message de requête SOAP et comment désérialiser une réponse SOAP vers une instance du type du résultat de la méthode. Il existe deux sortes d'annotations :
@WebService

L'annotation @javax.jws.WebService marque une classe ou une interface Java comme étant un service web. Cette annotation possède un certain nombre d'attributs permettant de personnaliser le nom du services web dans le fichier WSDL (éléments <wsdl:portType> ou <wsdl:service>) et son espace de noms ainsi que l'emplacement du fichier WSDL lui-même (attribut wsdlLocation).

API de l'annotation @WebService
@Target(TYPE)
@Retention(RUNTIME)
public @interface WebService {
    public String name() default "";
    public String targetNamespace() default "";
    public String serviceName() default "";
    public String portName() default "";
    public String wsdlLocation() default "";
    public String endpointInterface() default "";
} 

Lorsque nous utilisons @WebService, toutes les méthodes publiques du service web qui n'utilisent pas l'annotation @WebMethod sont exposées.

@WebMethod

Par défaut, toutes les méthodes publiques d'un service web sont exposées dans le WSDL et utilisent toutes les règles d'association par défaut. L'annotation @javax.jws.WebMethod permet de personnaliser certaines de ces associations de méthodes. Son API est assez simple et permet de renommer une méthode ou de l'exclure du WSDL.

session.GestionPersonnel.java
@WebService
@Stateless
public class GestionPersonnel {
   @PersistenceContext
   private EntityManager bd;

   public void nouveau(Personne personne) {
     bd.persist(personne);
   }

   @WebMethod(exclude = true)
   public Personne rechercher(Personne personne) {
      return bd.find(Personne.class, personne.getId());
   }

   public void modifier(Personne personne) {
     bd.merge(personne);
   }

   public void supprimer(Personne personne) {
     bd.remove(rechercher(personne));
   }

   @WebMethod(operationName = "listeDuPersonnel")
   public List<Personne> listePersonnels() {
      Query requête = bd.createNamedQuery("toutLePersonnel");
      return requête.getResultList();
   }
}

Le service web ci-dessus exclut la méthode rechercher() et demande à renommer la méthode listePersonnels().
.

@WebResult

L'annotation @javax.jws.WebResult fonctionne en relation avec @WebMethod pour contrôler le nom de la valeur renvoyée par le message dans le WSDL.

session.GestionPersonnel.java
@WebService
@Stateless
public class GestionPersonnel {
...
   @WebMethod(operationName = "listeDuPersonnel")
   @WebResult(name = "personnels")
   public List<Personne> listePersonnels() {
      Query requête = bd.createNamedQuery("toutLePersonnel");
      return requête.getResultList();
   }
}

Dans le code ci-dessus, le résultat de la méthode listePersonnels() est renommée personnels.
.

@WebParam

L'annotation @javax.jws.WebParam, dont l'API est présentée ci-dessous, ressemble à @WebResult car elle personnalise les paramètres des méthodes du service web. Cette API permet de modifier le nom du paramètre dans le WSDL, l'espace de nom et le mode de passage des paramètres - IN, OUT, INOUT.

API de l'annotation @WebParam
@Target(TYPE)
@Retention(RUNTIME)
public @interface WebParam {
    public String name() default "";
    public String targetNamespace() default "";
    public enum Mode {IN, OUT, INOUT};
    public boolean header() default false;
    public String partName() default "";
} 
session.GestionPersonnel.java
@WebService
@Stateless
public class GestionPersonnel {
...
   @WebMethod
   public void nouveau(@WebParam(name = "agent") Personne personne) {
     bd.persist(personne);
   }
...
} 
@OneWay

L'annotation @OneWay peut être utilisée avec les méthodes qui ne renvoient aucun résultat, comme celles de type void. Cette annotation ne possède aucun élément et peut être considérée comme une interface de marquage informant le conteneur que l'appel de cette méthode peut être optimisé (en utilisant un appel asynchrone, par exemple) puisqu'il n'y a pas de valeur de retour.

session.GestionPersonnel.java
@WebService
@Stateless
public class GestionPersonnel {
...
   @WebMethod
   @OneWay
   @Asynchronous
   public void nouveau(@WebParam(name = "agent") Personne personne) {
     bd.persist(personne);
   }
...
} 

Cycle de vie et méthode de rappel

Comme vous pouvez le constater avec la figure ci-dessous, le cycle de vie des services web ressemble à celui des beans sans état et des MDB. Comme avec tous les composants qui ne mémorisent pas l'état, soit ils n'existent pas, soit ils sont prêts à traiter une requête. Ce cycle de vie est géré par le conteneur.

Comme elles s'exécutent dans un conteneur, les EJB autorisent l'injection de dépendances et les méthodes de rappel du cycle de vie : si elle existe, le conteneur appelera la méthode de rappel @PostConstruct lorsqu'il crée une instance d'un service web et la méthode de rappel @PreDestroy lorsqu'il la détruit.

Contexte d'un service web

Un service web possède un contexte d'environnement auquel il peut accéder en injectant une référence à javax.xml.ws.WebServiceContext au moyen de l'annotation @Resource. Dans ce contexte, le service peut obtenir des informations d'exécution comme la classe qui implémente l'extrémité, le contexte du message et des informations concernant la sécurité relative à la requête qui est traitée.

session.GestionPersonnel.java
@WebService
@Stateless
public class GestionPersonnel {
   @Resource
   private WebServiceContext contexte;
...
}
Méthode Description
getMessageContext() Renvoie le MessageContext de la requête en cours de traitement au moment de l'appel. Permet d'accéder aux en-têtes du message SOAP, etc.
getUserPrincipal() Renvoie le principal qui identifie l'émetteur de la requête en cours de traitement.
isUserInRole() Teste si l'auteur authentifié appartient au rôle logique indiqué.
getEndPointReference() Renvoie l'EndPointReference associé à cette extrémité.

Appel d'un service web

Vous pouvez invoquer un service web en utilisant le fichier WSDL et certains outils de génération de classes Java relais (stubs). L'appel d'un service web ressemble à l'appel d'un objet distribué avec RMI : comme ce dernier, JAX-WS permet au programmeur d'utiliser un appel de méthode local pour invoquer un service se trouvant sur un autre hôte. La différence est que le service web sur l'hôte distant peut être écrit dans un autre langage de programmation.

Le fichier WSDL établit le contrat entre le consommateur et le service, et Metro fournit un outil de conversion WSDL vers Java (wsimport) qui produit des classes et des interfaces à partir du code WSDL - ces interfaces sont appelées SEI (service endpoint interfaces) car ce sont des représentations Java d'une extrémité de service web (servlet ou EJB). Cette SEI agit comme un proxy qui route l'appel Java local vers le service web distant via HTTP ou d'autres protocoles de transport.

Lorsqu'une méthode de ce proxy est appelé, elle convertit ses paramètres en message SOAP (la requête) et l'envoie à l'extrémité du web service. Pour obtenir le résultat, la réponse SOAP est reconvertie en une instance du type renvoyé. Pour l'utiliser, il n'est pas nécessaire de connaître le fonctionnement interne du proxy ni d'étudier son code. Avant de compiler le consommateur client, il est nécessaire de produire la SEI afin d'obtenir la classe proxy pour l'appeler dans le code.

Le consommateur peut obtenir une instance du proxy par injection ou en la créant par programme. Nous injectons un client de service web à l'aide de l'annotation @javax.xml.ws.WebServiceRef, qui ressemble aux annotations @Resource ou @EJB présentés dans les études précédentes. Lorsqu'elle est appliquée à un attribut, le conteneur injecte une instance du service web au moment où l'application est initialisée.
personnel.Client.java
package personnel;
...
public class Client extends JFrame {
    @WebServiceRef
    private static GestionPersonnelService service;
    private static GestionPersonnel gestion;
...
    public static void main(String[] args) {
        gestion = service.getGestionPersonnelPort();
        new Client();
    }
...
    gestion.nouveau(personne);
    gestion.modifier(personne);
    gestion.supprimer(personne);
    gestion.listePersonnels())
...
}

La classe GestionPersonnelService est la SEI, pas le service web lui-même. Vous devez ensuite obtenir la classe proxy GestionPersonnel afin de permettre localement l'invocation des méthodes métiers. Nous appelons localement les méthodes nouveau(), modifier(), etc. du proxy, qui, à leurs tours, invoquerons le service web distant, créeront les requêtes SOAP, sérialiserons les messages, etc.

Pour que cette injection fonctionne, ce code doit s'exécuter dans un conteneur (de servlet, d'EJB ou de client d'application) : dans le cas contraire, nous ne pouvons pas utiliser l'annotation @WebServiceRef et nous devons alors passer impérativement par la programmation.

Les classes produites par l'outil wsimport peuvent être directement utilisées. Il suffit alors tout simplement de créer une instance de GestionPersonnelService à l'aide de l'opérateur new ; le reste est entièrement identique. Du coup, l'injection peut ne pas paraître très intéressante si ce n'est le fait d'avoir l'application cliente automatiquement installée au travers de Java Web Start.

Les outils wsimport et wsgen sont founis avec le JDK 1.6. Vous pouvez accéder directement à ces outils ou passer par l'interface en ligne de commande de Glassfish.

personnel.Client.java
package personnel;
...
public class Client extends JFrame {
    private static GestionPersonnel gestion;
...
    public static void main(String[] args) {
        gestion = new GestionPersonnelService().getGestionPersonnelPort();
        new Client();
    }
...
    gestion.nouveau(personne);
    gestion.modifier(personne);
    gestion.supprimer(personne);
    gestion.listePersonnels())
...
}

Conclusion

Le développement et l'utilisation d'un service web comporte quatre phases :

 

Choix du chapitre Projet Archivage de photos

Nous allons maintenant valider toutes ces notions apprises au cours de cette étude au travers de deux projets. Le premier concerne l'archivage de photos sur un serveur distant. Le deuxième, qui sera traité dans un chapitre à part entière permettra de gérer l'identité du personnel d'une entreprise. Ce premier projet d'archivage englobe en réalité deux parties :

Finalement l'ensemble de ce projet global va être constitué de deux sous projets correspondant à deux postes informatiques différents, c'est-à-dire à deux machines virtuelles également différentes.
Diagramme de déploiement

Diagramme de cas d'utilisation

Module EJB - Création du service web

Procédons par ordre et commençons par créer le service web qui va être capable d'archiver les photos désirées sur le disque dur du serveur. Le point d'extrémité est un bean session. Du coup, il est nécessaire de prévoir un projet de type "Module EJB". Le nom du projet s'intitule ArchiverPhotos qui possède un bean session Archiver, de type stateless, dont voici le code :

session.Archiver.java
package session;

import java.io.*;
import javax.ejb.*;
import javax.jws.*;

@WebService
@Stateless
public class Archiver  {
    private final String répertoire = "C:/Archivage/";

    @Asynchronous
    public void stocker(String nom, byte[] octets) throws IOException  {
        File fichier = new File(répertoire+nom);
        if (fichier.exists()) return;
        FileOutputStream photo = new FileOutputStream(fichier);
        photo.write(octets);
        photo.close();
    }

    public byte[] restituer(String nom) throws IOException {
        File fichier = new File(répertoire+nom);
        if (!fichier.exists()) return null;
        FileInputStream photo = new FileInputStream(fichier);
        byte[] octets = new byte[(int)fichier.length()];
        photo.read(octets);
        photo.close();
        return octets;
    }

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

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

Ce code est finalement extrêmement simple puisque toute la problématique réseau est totalement occultée par le fait que nous utilisons un bean session (objet distant). Par ailleurs, la mise en oeuvre du service web est encore plus simple puisqu'il suffit juste de proposer la seule annotation @WebService directement sur la classe représentant le bean session.

Remarquez au passage que la méthode stocker() est asynchrone, ce qui permet de libérer l'application cliente dès que l'envoi des octets est réalisé. Chacun reprend la main de son côté, et le serveur peut ainsi, pendant ce temps là, créer le fichier image correspondant dans le répertoire de stockage dédié.

Une fois que ces quelques lignes sont introduites, dans netbeans pas exemple, vous pouvez demander l'exécution de ce service (un simple clic sur le bouton Run) qui dans l'ordre propose : la compilation et l'archivage suivie du déploiement sur le serveur d'applications Glassfish. Ce dernier génère alors effectivement le service web avec les différents artefacts nécessaires (à l'aide de l'utilitaire interne wsgen) ainsi que le contrat du service sous forme de document WSDL.


Je rappelle que les artefacts sont des classes spécialisées qui s'occupent uniquement, soit de générer des documents XML en relation avec la réponse du service souhaité, soit d'être capable de remettre en forme une requête sous forme d'appel de méthode (avec les arguments nécessaires) à partir d'un document XML envoyé par le client.

Vous pouvez consulter à tout moment notre service web directement sur le serveur Glassfish à l'aide de l'application web proposée par défaut, et donc à l'aide d'un simple navigateur. Nous retrouvons ainsi notre module EJB ArchiverPhotos à l'intérieur duquel nous découvrons notre bean session Archiver. Il est alors possible de consulter par la suite le document WSDL définissant le service web.



Réaliser un service web avec Netbeans et Glassfish est vraiment très simple et très rapide. Tout se fait en coulisse. Après avoir mis l'annotation @WebService sur un bean session sans état (seul possibilité), il suffit ensuite de demander l'exécution pour que le déploiement et la mise en service totale se réalise automatiquement, et ceci dans les plus brefs délais.

Application cliente - Utilisation du service web à distance

Nous pouvons dès lors nous consacrer à l'application cliente qui va utiliser ce service web. Il s'agit d'une simple application fenêtrée. L'avantage, c'est que cette application peut très bien être utilisée depuis Internet puisque le protocole utilisé est le protocole HTTP sur le port 80 (ici 8080) qui est généralement non filtré par le parefeu.

Nous devons donc élaborer un nouveau projet pour une application Java classique. Avant d'écrire le code relatif à la mise en oeuvre de l'IHM, la toute première chose à s'occuper est de générer les artefacts côté client (à l'aide de wsimport) afin que nous disposions de toutes les classes nécessaires au dialogue avec le service web au travers du protocole SOAP/XML. Ces classes, je le rappelle, vont s'occuper de traduire ou de générer des documents XML.

Ceci dit, encore une fois, il ne sera pas nécessaire de manipuler directement wsimport. Netbeans le fait à votre place. Voici la procédure à suivre. Il suffit de faire appel au menu WebServiceClient et de spécifier alors la localisation du document WSDL afin que les artefacts soient en adéquation avec le service web utilisé.

Dès que vous validez votre choix, wsimport est automatiquement exécuté et génère donc à votre place l'ensemble des classes nécessaires à la communication avec le web service au travers du protocole SOAP / XML.

Maintenant tout est prêt, nous pouvons donc écrire notre codage Java correspondant à toute la partie graphique afin de permettre la sélection des photos présentes sur le disque dur local et de les envoyer par la suite sur le disque dur du serveur par le service web que nous venons de décrire.

photos.Client.java
package photos;

import java.awt.BorderLayout;
import java.awt.event.*;
import java.io.*;
import javax.imageio.ImageIO;
import javax.swing.*;
import ws.*;

public class Client extends JFrame {
    private JTabbedPane onglets = new JTabbedPane();
    private JPanel panneauLocal = new JPanel(new BorderLayout());
    private JPanel panneauServeur = new JPanel(new BorderLayout());
    private JToolBar outilsLocal = new JToolBar();
    private JToolBar outilsDistant = new JToolBar();
    private JLabel photoLocale = new JLabel();
    private JLabel photoDistante = new JLabel();
    private JLabel description = new JLabel();
    private JComboBox listePhotos = new JComboBox();
    private JFileChooser sélecteur = new JFileChooser();
    private File fichier;
    private byte[] octets;
    private boolean effacer = true;

    private static Archiver archivage;  // <----------------------------------------------------------------------------------------------------------------- accès au service web

    public Client() {
        super("Envoyer des photos");
        add(onglets);
        onglets.add("Photos en local", panneauLocal);
        onglets.add("Photos distantes", panneauServeur);

        panneauLocal.add(outilsLocal, BorderLayout.NORTH);
        panneauLocal.add(new JScrollPane(photoLocale));

        outilsLocal.add(new AbstractAction("Sélectionner") {
            public void actionPerformed(ActionEvent e) {
                sélecteur.setFileSelectionMode(JFileChooser.FILES_ONLY);
                if (sélecteur.showOpenDialog(Client.this)==JFileChooser.APPROVE_OPTION) {
                    fichier = sélecteur.getSelectedFile();
                    photoLocale.setIcon(new ImageIcon(fichier.getPath()));
                }
            }
        });

        outilsLocal.add(new AbstractAction("Envoyer") {
            public void actionPerformed(ActionEvent e) {
                if (fichier!=null)
                    try {
                        byte[] octets = new byte[(int) fichier.length()];
                        FileInputStream lecture = new FileInputStream(fichier);
                        lecture.read(octets);
                        lecture.close();
                        archivage.stocker(fichier.getName(), octets); // <------------------------------------------ appel de la méthode stocker() du service web
                        listingPhotos();
                    }
                    catch (Exception ex) {
                        setTitle("Impossible d'envoyer le fichier");
                    }
              }
        });

        panneauServeur.add(outilsDistant, BorderLayout.NORTH);
        panneauServeur.add(new JScrollPane(photoDistante));
        panneauServeur.add(description, BorderLayout.SOUTH);

        outilsDistant.add(new AbstractAction("Restituer") {
            public void actionPerformed(ActionEvent e) {
                sélecteur.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
                if (sélecteur.showSaveDialog(Client.this)==JFileChooser.APPROVE_OPTION) {
                    try {
                        fichier = new File(sélecteur.getSelectedFile() + "/" +(String)listePhotos.getSelectedItem());
                        FileOutputStream fluxImage = new FileOutputStream(fichier);
                        fluxImage.write(octets);
                        fluxImage.close();
                    } 
                    catch (Exception ex) {  setTitle("Problème pour restituer la photo en local"); }                  
                }
            }
        });

        outilsDistant.add(new AbstractAction("Supprimer") {
            public void actionPerformed(ActionEvent e) {
                archivage.supprimer((String)listePhotos.getSelectedItem()); // <------------------------ appel de la méthode supprimer() du service web
                listingPhotos();
            }
        });

        outilsDistant.add(listePhotos);
        listePhotos.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (!effacer)
                    try {
                        octets = archivage.restituer((String) listePhotos.getSelectedItem());  // <------- appel de la méthode restituer() du service web
                        ByteArrayInputStream fluxImage = new ByteArrayInputStream(octets);
                        photoDistante.setIcon(new ImageIcon(ImageIO.read(fluxImage)));
                    }
                    catch (Exception ex) {  setTitle("Problème pour récupérer l'image du serveur");  }
                }
        });
        listingPhotos();
        setSize(500, 400);
        setLocationByPlatform(true);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setVisible(true);
    }

    private void listingPhotos() {
        effacer = true;
        listePhotos.removeAllItems();
        for (String photo : archivage.liste()) listePhotos.addItem(photo); // <----------------------------------- appel de la méthode liste() du service web
        effacer = false;
        if (listePhotos.getItemCount()>0) listePhotos.setSelectedIndex(0);
    }
    
    public static void main(String[] args) {
        archivage = new ArchiverService().getArchiverPort();
        new Client();
    }
}  

Principe des artefacts

Nous venons de le dire, l'utilisation d'un service web est vraiment très simple et très intuitive. Il suffit d'appeler la méthode qui vous intéresse de l'objet distant (service web) en spécifiant les bonnes valeurs dans les arguments requis. Par contre, la communication entre le client et le service web se fait par l'intermédiaire de documents XML formatés suivant le protocole SOAP.

Les artefacts sont donc là pour se préoccuper de la formation et de l'analyse de documents XML. Nous allons détailler plus précisément l'architecture de ces artefacts.

Syntèse de la communication entre l'application cliente et le web service

Nous allons maintenant conclure et bien visualiser au travers d'un cas d'utilisation, l'enchaînement des communications entre le client et le service web au travers du tube HTTP en utilisant le protocole SOAP / XML.

Nous allons prendre le temps de bien démontrer la communication entre les deux machines, à l'aide d'un analyseur de trame, en faisant l'étude d'une méthode particulière du service web.
Diagramme de cas d'utilisation

Diagramme de séquence de Stocker fichier

A titre d'exemple, je vous propose de voir également les captures correspondant à la demande de suppression d'une photo sur le serveur :


Capture de trames correspondant à la demande de liste des fichiers images présents actuellement sur le serveur :

 

Choix du chapitre Projet gestion du personnel

Je vous propose de réaliser un deuxième projet qui permet de gérer le personnel d'une entreprise, ce qui va nous permettre de voir comment transiter une entité au travers du réseau, tout en respectant le protocole SOAP / XML. Par contre, nous fairons très peu de description, uniquement ce qui s'avèrera nécessaire.

Constitution du projet dans son ensemble

Ce deuxième projet de gestion de personnel, comme le projet précédent, englobe également deux parties :

Module EJB - Création du service web et de la persistance

Procédons par ordre et commençons par nous occuper du serveur. Il est nécessaire de prévoir un projet de type "Module EJB". Le nom du projet s'intitule tout simplement Personnels, et possède deux éléments importants, d'une part, le bean session sans état GestionPersonnel qui intègre le service web et qui gère la persistance, et l'entité Personne, qui se préoccupe de rendre persistant chaque personnel de l'entreprise. Commençons justement par cette dernière :

entité.Personne.java
package entité;

import java.io.Serializable;
import java.util.*;
import javax.persistence.*;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement
@Entity
@NamedQuery(name="toutLePersonnel", query="SELECT p FROM Personne p ORDER BY p.nom")
public class Personne implements Serializable {
   @Id
   @GeneratedValue(strategy = GenerationType.AUTO)
   private long id;
   private String nom;
   private String prenom;
   private int age;
   @ElementCollection(fetch=FetchType.EAGER)
   private List<String> telephones = new ArrayList<String>();

   public long getId() { return id; }
   public void setId(long id) { this.id = id; }

   public int getAge() { return age; }
   public void setAge(int age) { this.age = age; }

   public String getNom() { return nom; }

   public void setNom(String nom) {
       this.nom = nom.toUpperCase();
   }

   public String getPrenom() { return prenom;  }

   public void setPrenom(String prenom) {
       StringBuilder chaine = new StringBuilder(prenom.toLowerCase());
       chaine.setCharAt(0, Character.toUpperCase(chaine.charAt(0)));
       this.prenom = chaine.toString();
   }

   public List<String> getTelephones() { return telephones; }
   public void setTelephones(List<String> telephones) { this.telephones = telephones; }
}
session.GestionPersonnel.java
package session;

import entité.Personne;
import java.util.List;
import javax.ejb.*;
import javax.jws.WebService;
import javax.persistence.*;

@WebService
@Stateless
@LocalBean
public class GestionPersonnel {
   @PersistenceContext
   private EntityManager bd;

   @Asynchronous
   public void nouveau(Personne personne) {
     bd.persist(personne);
   }

   private Personne rechercher(Personne personne) {
      return bd.find(Personne.class, personne.getId());
   }

   @Asynchronous
   public void modifier(Personne personne) {
     bd.merge(personne);
   }

   @Asynchronous
   public void supprimer(Personne personne) {
     bd.remove(rechercher(personne));
   }

   public List<Personne> listePersonnels() {
      Query requête = bd.createNamedQuery("toutLePersonnel");
      return requête.getResultList();
   }
} 
Application cliente - Utilisation du service web de gestion du personnel

Le service est maintenant opérationnel, du moins si vous avez pensé à compiler votre module et à le déployer sur le serveur Glassfish, tout ceci en l'exécutant au travers de Netbeans. Nous pouvons maintenant travailler sur l'application cliente.

Je le rappelle, nous devons élaborer un nouveau projet au travers d'une application Java classique. Avant d'écrire le code relatif à la mise en oeuvre de l'IHM, la toute première chose à s'occuper est de générer les artefacts côté client (à l'aide de wsimport) afin que nous disposions de toutes les classes nécessaires au dialogue avec le service web au travers du protocole SOAP/XML. Ces classes vont s'occuper de traduire ou de générer des documents XML.

Ceci dit, encore une fois, il ne sera pas nécessaire de manipuler directement wsimport. Netbeans le fait à votre place. Vous connaissez maintenant la procédure à suivre. Il suffit de faire appel au menu WebServiceClient et de spécifier alors la localisation du document WSDL afin que les artefacts soient en adéquation avec le service web utilisé.

Dès que vous validez votre choix, wsimport est automatiquement exécuté et génère donc à votre place l'ensemble des classes nécessaires à la communication avec le web service au travers du protocole SOAP / XML.

ws.Personne.java
package ws;

import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "personne", propOrder = {
    "age",
    "id",
    "nom",
    "prenom",
    "telephones"
})
public class Personne {

    protected int age;
    protected long id;
    protected String nom;
    protected String prenom;
    @XmlElement(nillable = true)
    protected List<String> telephones;

    public int getAge() { return age;  }
    public void setAge(int value) {  this.age = value;   }

    public long getId() {  return id;   }
    public void setId(long value) {  this.id = value;   }

    public String getNom() {  return nom;   }
    public void setNom(String value) { this.nom = value;   }

    public String getPrenom() { return prenom;  }
    public void setPrenom(String value) { this.prenom = value;   }

    public List<String> getTelephones() {
        if (telephones == null) {
            telephones = new ArrayList<String>();
        }
        return this.telephones;
    }
}

Il est possible de prévoir des méthodes supplémentaires, coté client, sur l'artefact Personne, afin que cela soit plus facile et plus intuitif à utiliser par l'IHM. Je vous porpose donc de faire les rajouts nécessaires comme montré ci-dessous.

ws.Personne.java
package ws;

import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "personne", propOrder = {
    "age",
    "id",
    "nom",
    "prenom",
    "telephones"
})
public class Personne {

    protected int age;
    protected long id;
    protected String nom;
    protected String prenom;
    @XmlElement(nillable = true)
    protected List<String> telephones;

    public int getAge() { return age;  }
    public void setAge(int value) {  this.age = value;   }

    public long getId() {  return id;   }
    public void setId(long value) {  this.id = value;   }

    public String getNom() {  return nom;   }
    public void setNom(String value) { this.nom = value;   }

    public String getPrenom() { return prenom;  }
    public void setPrenom(String value) { this.prenom = value;   }

    public List<String> getTelephones() {
        if (telephones == null) {
            telephones = new ArrayList<String>();
        }
        return this.telephones;
    }

    public Personne() {  }

    public Personne(String nom, String prenom, int age) {
        this.age = age;
        this.nom = nom;
        this.prenom = prenom;
    }

    public void modifier(String nom, String prenom, int age) {
        this.age = age;
        this.nom = nom;
        this.prenom = prenom;
    }

    @Override
    public String toString() {
        return nom+' '+prenom;
    } 
}
  1. Ainsi, à l'artefact original est proposé un constructeur où nous spécifions le nom, le prénom et l'âge du personnel. Attention, dès que vous créez un nouveau constructeur, vous êtes obligé de prévoir le constructeur par défaut.
  2. J'en profite également pour rajouter une méthode modifier() et pour redéfinir la méthode toString() qui, dans ce dernier cas, me permet de visualiser automatiquement le nom suivi du prénom lorsque nous intégrons un personnel dans la liste déroulante de l'IHM cliente.

Maintenant tout est prêt, nous pouvons donc écrire notre codage Java correspondant à toute la partie graphique afin de permettre la gestion complète du personnel de l'entreprise à distance (même sur Internet) et de mémoriser ainsi toutes les actions proposées sur la base de données du serveur et ceci, au travers du service web.

personnel.Client.java
package personnel;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import ws.*;

public class Client extends JFrame {
    private JToolBar outils = new JToolBar();
    private JTextField nom = new JTextField();
    private JTextField prénom = new JTextField();
    private JFormattedTextField âge = new JFormattedTextField(0);
    private JComboBox téléphones = new JComboBox();
    private JPanel panneau = new JPanel();
    private JComboBox liste = new JComboBox();
    private static GestionPersonnel gestion;
    private Personne personne = new Personne();
    private boolean effacer = true;

    public Client() {
        super("Personne");
        add(outils, BorderLayout.NORTH);
        outils.add(new AbstractAction("Enregistrer") {
            public void actionPerformed(ActionEvent e) {
                personne = new Personne(nom.getText(), prénom.getText(), (Integer)âge.getValue());
                for (int i=0; i<téléphones.getItemCount(); i++)
                    personne.getTelephones().add((String) téléphones.getItemAt(i));
                gestion.nouveau(personne);
                listingPersonnes();
            }
        });
        outils.add(new AbstractAction("Ajout téléphone") {
            public void actionPerformed(ActionEvent e) {
                String téléphone = (String) téléphones.getSelectedItem();
                téléphones.addItem(téléphone);
            }
        });
        outils.add(new AbstractAction("Modifier") {
            public void actionPerformed(ActionEvent e) {
                personne.modifier(nom.getText(), prénom.getText(), (Integer)âge.getValue());
                gestion.modifier(personne);
                listingPersonnes();
            }
        });
        outils.add(new AbstractAction("Supprimer") {
            public void actionPerformed(ActionEvent e) {
                gestion.supprimer(personne);
                listingPersonnes();
            }
        });
        outils.add(new AbstractAction("Effacer") {
            public void actionPerformed(ActionEvent e) {
                personne = new Personne();
                rafraîchir();
            }
        });
        panneau.setLayout(new GridLayout(4, 2));
        panneau.add(new JLabel("Nom :"));
        panneau.add(nom);
        panneau.add(new JLabel("Prénom :"));
        panneau.add(prénom);
        panneau.add(new JLabel("Âge :"));
        panneau.add(âge);
        panneau.add(new JLabel("Téléphones :"));
        panneau.add(téléphones);
        téléphones.setEditable(true);
        add(panneau);
        add(liste, BorderLayout.SOUTH);
        liste.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (!effacer) {
                    personne = (Personne) liste.getSelectedItem();
                    rafraîchir();
                }
            }
        });
        listingPersonnes();
        pack();
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setVisible(true);
    }

    private void listingPersonnes() {
        effacer = true;
        liste.removeAllItems();
        effacer = false;
        for (Personne personne : gestion.listePersonnels())
            liste.addItem(personne);
    }

    private void rafraîchir() {
        nom.setText(personne.getNom());
        prénom.setText(personne.getPrenom());
        âge.setValue(personne.getAge());
        téléphones.removeAllItems();
        for (String téléphone : personne.getTelephones()) téléphones.addItem(téléphone);
    }

    public static void main(String[] args) {
        gestion = new GestionPersonnelService().getGestionPersonnelPort();
        new Client();
    }
}
Diagramme de séquences de Enregistrer nouveau personnel