IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Penser en Java

2nde édition


précédentsommairesuivant

XVII. Informatique distribuée

De tout temps, la programmation des machines interconnectées s'est avérée difficile, complexe, et sujette à erreurs.

Le programmeur devait connaître le réseau de façon détaillée, et parfois même son hardware. Il était généralement nécessaire d'avoir une bonne compréhension des différentes couches du protocole réseau, et il existait dans chacune des bibliothèques de réseau un tas de fonctions, bien entendu différentes, concernant la connexion, l'empaquetage et le dépaquetage des blocs d'informations, l'émission et la réception de ces blocs, la correction des erreurs et le dialogue. C'était décourageant.

Toutefois, l'idée de base de l'informatique distribuée n'est pas vraiment complexe, et elle est encapsulée de manière très agréable dans les bibliothèques Java. Il faut pouvoir :

  • obtenir quelques informations de cette machine-là et les amener sur cette machine-ci, ou vice versa. Ceci est réalisé au moyen de la programmation réseau de base ;
  • se connecter à une base de données, qui peut résider quelque part sur le réseau. Pour cela, on utilise la Connectivité Bases de Données Java : Java DataBase Connectivity (JDBC), qui est une encapsulation des détails confus, et spécifiques à la plate-forme, du SQL (Structured Query Language - langage structuré d'interrogation de bases de données - utilisé pour de nombreux échanges avec des bases de données) ;
  • fournir des services via un serveur Web. C'est le rôle des servlets Java et des Pages Serveur Java : Java Server Pages (JSP) ;
  • exécuter de manière transparente des méthodes appartenant à des objets Java résidant sur des machines distantes, exactement comme si ces objets résidaient sur la machine locale. Pour cela, on utilise l'Invocation de Méthode Distante de Java : Remote Method Invocation (RMI) ;
  • utiliser du code écrit dans d'autres langages, tournant sous d'autres architectures. C'est l'objet de Common Object Request Broker Architecture (CORBA), qui est directement mis en œuvre par Java ;
  • séparer les questions relatives à la connectivité de la logique concernant le résultat cherché, et en particulier les connexions aux bases de données incluant la gestion des transactions et la sécurité. C'est le domaine des Enterprise JavaBeans (EJB). Les EJB ne représentent pas réellement une architecture distribuée, mais les applications qui en découlent sont couramment utilisées dans un système client-serveur en réseau ;
  • ajouter et enlever, facilement et dynamiquement, des fonctionnalités provenant d'un réseau considéré comme un système local. C'est ce que propose la fonctionnalité Jini de Java.

Ce chapitre a pour but de présenter succinctement toutes ces fonctionnalités. Il faut noter que chacune représente un vaste sujet, et pourrait faire l'objet d'un livre à elle toute seule, aussi ce chapitre n'a d'autre but que de vous rendre ces concepts familiers, et en aucun cas de faire de vous un expert (toutefois, vous aurez largement de quoi faire avec l'information que vous trouverez ici à propos de la programmation réseau, des servlets et des JSP).

XVII-A. La programmation réseau

Une des grandes forces de Java est de pouvoir travailler en réseau sans douleur. Les concepteurs de la bibliothèque réseau de Java en ont fait quelque chose d'équivalent à la lecture et l'écriture de fichiers, avec la différence que les « fichiers » résident sur une machine distante et que c'est elle qui décide de ce qu'elle doit faire au sujet des informations que vous demandez ou de celles que vous envoyez. Autant que possible, les menus détails du travail en réseau ont été cachés et sont pris en charge par la JVM et l'installation locale de Java. Le modèle de programmation utilisé est celui d'un fichier ; en réalité, la connexion réseau (Une » socket « - littéralement douille, ou prise, NdT) est encapsulée dans des objets stream, ce qui permet d'utiliser les mêmes appels de méthode que pour les autres objets stream. De plus, les fonctionnalités de multithreading de Java viennent à point lorsqu'on doit traiter plusieurs connexions simultanées.

Cette section introduit le support réseau de Java en utilisant des exemples triviaux.

XVII-A-1. Identifier une machine

Il semble évident que, pour pouvoir appeler une machine depuis une autre, en ayant la certitude d'être connecté à une machine en particulier, il doit exister quelque chose comme un identifiant unique sur le réseau. Les anciens réseaux se contentaient de fournir des noms de machines uniques sur le réseau local. Mais Java travaille sur l'Internet, et cela nécessite un moyen d'identifier chaque machine de manière unique par rapport à toutes les autres dans le monde entier. C'est la raison d'être de l'adresse IP (Internet Protocol - protocole internet, NdT) qui existe sous deux formes :

  1. La forme familière la forme DNS (Domain Name System, Système de Nommage des Domaines). Mon nom de domaine est bruceeckel.com, et si j'avais dans mon domaine un ordinateur nommé Opus, son nom de domaine serait Opus.bruceeckel.com. C'est exactement le type de nom que vous utilisez lorsque vous envoyez du courrier à quelqu'un, et il est souvent associé à une adresse World Wide Web ;
  2. Sinon, on peut utiliser la forme du quadruplet pointé, c'est-à-dire quatre nombres séparés par des points, par exemple 123.255.28.120.

Dans les deux cas, l'adresse IP est représentée en interne comme un nombre sur 32 bits (57) (et donc chaque nombre du quadruplet ne peut excéder 255), et il existe un objet spécial Java pour représenter ce nombre dans l'une des formes décrites ci-dessus en utilisant la méthode de la bibliothèque java.net : static InetAddress.getByName( ). Le résultat est un objet du type InetAddress qu'on peut utiliser pour construire une socket, « comme on le verra plus loin.

Pour montrer un exemple simple d'utilisation de InetAddress.getByName( ), considérons ce qui se passe lorsque vous êtes en communication avec un Fournisseur d'Accès Internet (FAI) - Internet Service Provider (ISP). Chaque fois que vous vous connectez, il vous assigne une adresse IP temporaire. Tant que vous êtes connecté, votre adresse IP est aussi valide que n'importe quelle autre adresse IP sur Internet. Si quelqu'un se connecte à votre machine au moyen de votre adresse IP, alors il peut se connecter à un serveur Web ou FTP qui tournerait sur votre machine. Bien entendu, il faudrait qu'il connaisse votre adresse IP, mais puisqu'une nouvelle vous est assignée à chaque connexion, comment pourrait-il faire ?

Le programme qui suit utilise InetAddress.getByName( ) pour récupérer votre adresse IP. Pour qu'il fonctionne, vous devez connaître le nom de votre ordinateur. Sous Windows 95/98, aller à Paramètres, « Panneau de Contrôle, « Réseau, « et sélectionnez l'onglet » Identification. » Le Nom d'ordinateur est le nom à utiliser sur la ligne de commande.

 
Sélectionnez
//: c15:WhoAmI.java
// Affiche votre adresse de réseau lorsque
// vous êtes connectés à Internet.
import java.net.*;

public class WhoAmI {
  public static void main(String[ « args) 
      throws Exception {
    if(args.length != 1) {
      System.err.println(
        "Usage: WhoAmI MachineName");
      System.exit(1);
    }
    InetAddress a = 
      InetAddress.getByName(args[0]);
    System.out.println(a);
  }
} ///:~

Supposons que ma machine ait pour nom » peppy. « Une fois connecté au FAI, je lance le programme :

 
Sélectionnez
java WhoAmI peppy

En retour, j'obtiens un message tel que celui-ci (bien entendu, l'adresse est différente à chaque connexion) :

 
Sélectionnez
peppy/199.190.87.75

Si je donne cette adresse à un ami et qu'il existe un serveur Web tournant sur mon ordinateur, il peut s'y connecter en allant à l'URL http://199.190.87.75 (du moins tant que je reste connecté). Ceci est parfois une manière commode de distribuer de l'information à d'autres personnes, ou encore de tester la configuration d'un site avant de l'installer sur un « vrai » serveur.

XVII-A-1-a. Serveurs et clients

La finalité d'un réseau est de permettre à deux machines de se connecter et ainsi de se « parler ». Une fois que les deux machines se sont trouvées l'une l'autre, elles peuvent entamer une agréable conversation bidirectionnelle. Mais qu'est-ce que chacune peut donc rechercher chez l'autre ? Et d'abord, comment trouvent-elles l'autre ? Tout se passe à peu près comme lorsqu'on est perdu dans un parc d'attractions : une des machines doit attendre l'appel de l'autre sans bouger : « Hé, où êtes-vous ? »]

La machine qui attend est appelée serveur, celle qui cherche client. Cette distinction n'est importante que tant que le client cherche à se connecter au serveur. Une fois les machines connectées, la communication se traduit par un processus bidirectionnel et on n'a plus à se préoccuper de savoir qui est le serveur et qui est le client.

Le rôle du serveur est donc d'être en attente d'une demande de connexion, ceci est réalisé par l'objet serveur qu'on crée dans ce but. Le rôle du client est de tenter d'établir une connexion avec un serveur, ceci est réalisé avec un objet client qu'on crée pour cela. Une fois la connexion établie, aussi bien du côté serveur que du côté client, elle se transforme magiquement en un objet flux d'E/S, et dès lors on peut traiter cette connexion comme une lecture ou une écriture dans un fichier. Ainsi, une fois la connexion établie, il ne reste qu'à utiliser les commandes d'E/S familières vues au titre XIII. C'est un des aspects agréables des fonctions de Java en réseau.

XVII-A-1-b. Tester les programmes hors réseau

Pour toutes sortes de raisons, il se peut qu'on ne dispose pas de machines client et serveur, pas plus que d'un réseau pour tester nos programmes. Par exemple lorsqu'on réalise des exercices dans une situation d'apprentissage, ou bien qu'on écrive des programmes qui ne sont pas encore suffisamment stables pour être mis sur le réseau. Les créateurs du protocole internet (IP) ont bien appréhendé cette question, et ont créé une adresse spéciale appelée localhost qui représente une adresse IP en « boucle locale » permettant d'effectuer des tests en se passant de la présence d'un réseau. Voyez ci-dessous la manière générique de réaliser cette adresse en Java :

 
Sélectionnez
InetAddress addr = InetAddress.getByName(null);

En utilisant getByName( ) avec un argument null, cette fonction utilise par défaut localhost. InetAddress est utilisée pour désigner une machine particulière, et on doit l'initialiser avant d'aller plus loin. On ne peut pas manipuler le contenu d'une InetAddress (mais il est possible de l'imprimer, comme on va le voir dans l'exemple suivant). L'unique manière de créer une InetAddress passe par l'une des méthodes membres static surchargées de cette classe : getByName( ) (habituellement utilisée), getAllByName( ) ou getLocalHost( ).

Une autre manière de réaliser l'adresse de la boucle locale est d'utiliser la chaîne localhost :

 
Sélectionnez
InetAddress.getByName("localhost");

(en supposant que localhost « est décrit dans la table des hôtes de la machine), ou encore en se servant de la forme « quadruplet pointé » pour désigner l'adresse IP réservée à la boucle locale :

 
Sélectionnez
InetAddress.getByName("127.0.0.1");

Les trois formes aboutissent au même résultat.

XVII-A-1-c. Les Ports  : un emplacement unique dans la machine

Une adresse IP n'est pas suffisante pour identifier un serveur, en effet plusieurs serveurs peuvent coexister sur une même machine. Chaque machine IP contient aussi des ports, et lorsqu'on installe un client ou un serveur il est nécessaire de choisir un port convenant aussi bien à la connexion du client qu'à celle du serveur ; si vous donnez rendez-vous à quelqu'un, l'adresse IP représentera le quartier et le port sera le nom du bar.

Le port n'est pas un emplacement physique dans la machine, mais une abstraction de programmation (surtout pour des raisons de compatibilité). Le programme client sait comment se connecter à la machine via son adresse IP, mais comment se connectera-t-il au service désiré (potentiellement, l'un des nombreux services de cette machine) ? C'est pourquoi les numéros de port représentent un deuxième niveau d'adressage. L'idée sous-jacente est que si on s'adresse à un port particulier, en fait on effectue une demande pour le service associé à ce numéro de port. Un exemple simple de service est l'heure du jour. Typiquement, chaque service est associé à un numéro de port unique sur une machine serveur donnée. Il est de la responsabilité du client de connaître à l'avance quel est le numéro de port associé à un service donné.

Les services système réservent les numéros de ports 1 à 1024, il ne faut donc pas les utiliser, pas davantage que d'autres numéros de ports dont on saurait qu'ils sont utilisés. Le premier nombre choisi pour les exemples de ce livre est le port 8080 (en souvenir de la vénérable vieille puce 8 bits Intel 8080 de mon premier ordinateur, une machine CP/M).

XVII-A-2. Les sockets

Une socket est une abstraction de programmation représentant les extrémités d'une connexion entre deux machines. Pour chaque connexion donnée, il existe une socket sur chaque machine, on peut imaginer un câble virtuel reliant les deux machines, chaque extrémité enfichée dans une socket. Bien entendu, le hardware sous-jacent ainsi que la manière dont les machines sont connectées ne nous intéressent pas. L'essentiel de l'abstraction est qu'on n'a pas à connaître plus que ce qu'il est nécessaire.

En Java, on crée un objet Socket pour établir une connexion vers une autre machine, puis on crée un InputStream et un OutputStream (ou, avec les convertisseurs appropriés, un Reader et un Writer) à partir de ce Socket, afin de traiter la connexion en tant qu'objet flux d'E/S. Il existe deux classes Socket basées sur les flux : ServerSocket utilisé par un serveur pour [écouter « les connexions entrantes et Socket utilisé par un client afin d'initialiser une connexion. Lorsqu'un client réalise une connexion socket, ServerSocket retourne (via la méthode accept( )) un Socket correspondant permettant les communications du côté serveur. À ce moment-là, on a réellement établi une connexion Socket à Socket et on peut traiter les deux extrémités de la même manière, car elles sont alors identiques. On utilise alors les méthodes getInputStream( ) et getOutputStream( ) pour réaliser les objets InputStream et OutputStream correspondants à partir de chaque Socket. À leur tour ils peuvent être encapsulés dans des classes buffers ou de formatage tout comme n'importe quel objet flux décrit au titre XIII.

La construction du mot ServerSocket est un exemple supplémentaire de la confusion du plan de nommage des bibliothèques Java. ServerSocket aurait dû s'appeler «  ServerConnector » ou bien n'importe quoi qui n'utilise pas le mot Socket. Il faut aussi penser que ServerSocket et Socket héritent tous deux de la même classe de base. Naturellement, les deux classes ont plusieurs méthodes communes, mais pas suffisamment pour qu'elles aient une même classe de base. En fait, le rôle de ServerSocket est d'attendre qu'une autre machine se connecte, puis à ce moment-là de renvoyer un Socket réel. C'est pourquoi ServerSocket semble mal nommé, puisque son rôle n'est pas d'être une socket, mais plus exactement de créer un objet Socket lorsque quelqu'un se connecte.

Cependant, un ServerSocket crée physiquement un » serveur « ou, si l'on préfère, une « prise » à l'écoute sur la machine hôte. Cette « prise » est à l'écoute des connexions entrantes et renvoie une « prise » établie (les points terminaux et distants sont définis) via la méthode accept( ). La confusion vient du fait que ces deux « prises » (celle qui écoute et celle qui représente la communication établie) sont associées à la même « prise » serveur. La « prise » qui écoute accepte uniquement les demandes de nouvelles connexions, jamais les paquets de données. Ainsi, même si ServerSocket n'a pas beaucoup de sens en programmation, il en a physiquement.

Lorsqu'on crée un ServerSocket, on ne lui assigne qu'un numéro de port. Il n'est pas nécessaire de lui assigner une adresse IP parce qu'il réside toujours sur la machine qu'il représente. En revanche, lorsqu'on crée un Socket, il faut lui fournir l'adresse IP et le numéro de port sur lequel on essaie de se connecter (toutefois, le Socket résultant de la méthode ServerSocket.accept( ) contient toujours cette information).

XVII-A-2-a. Un serveur et un client vraiment simples

Cet exemple montre l'utilisation minimale d'un serveur et d'un client utilisant des sockets. Le serveur se contente d'attendre une demande de connexion, puis se sert du Socket résultant de cette connexion pour créer un InputStream et un OutputStream. Ces derniers sont convertis en Reader et Writer, puis encapsulés dans un BufferedReader et un PrintWriter. À partir de là, tout ce que lit le BufferedReader est renvoyé en écho au PrintWriter jusqu'à ce qu'il reconnaisse la ligne » END, « et dans ce cas il clôt la connexion.

Le client établit une connexion avec le serveur, puis crée un OutputStream et réalise le même type d'encapsulation que le serveur. Les lignes de texte sont envoyées vers le PrintWriter résultant. Le client crée également un InputStream (ici aussi, avec la conversion et l'encapsulation appropriées) afin d'écouter le serveur (c'est-à-dire, dans ce cas, simplement les mots renvoyés en écho).

Le serveur ainsi que le client utilisent le même numéro de port, et le client se sert de l'adresse de boucle locale pour se connecter au serveur sur la même machine, ce qui évite de tester en grandeur réelle sur un réseau (pour certaines configurations, on peut être amené à se connecter physiquement à un réseau afin que le programme fonctionne, même si on ne communique pas sur ce réseau.)

Voici le serveur :

 
Sélectionnez
//: c15:JabberServer.java
// Serveur simplifié dont le rôle se limite à         
// renvoyer en écho tout ce que le client envoie.
import java.io.*;
import java.net.*;

public class JabberServer {  
  // Choisir un port hors de la plage 1-1024:
  public static final int PORT = 8080;
  public static void main(String[ « args) 
      throws IOException {
    ServerSocket s = new ServerSocket(PORT);
    System.out.println("Started: " + s);
    try {
      // Le programme stoppe ici et attend
      // une demande de connexion: 
      Socket socket = s.accept();
      try {
        System.out.println(
          "Connection accepted: "+ socket);
        BufferedReader in = 
          new BufferedReader(
            new InputStreamReader(
              socket.getInputStream()));
        // Le tampon de sortie est vidé
        // automatiquement par PrintWriter:
        PrintWriter out = 
          new PrintWriter(
            new BufferedWriter(
              new OutputStreamWriter(
                socket.getOutputStream())),true);
        while (true) {  
          String str = in.readLine();
          if (str.equals("END")) break;
          System.out.println("Echoing: " + str);
          out.println(str);
        }
      // Toujours fermer les deux sockets...
      } finally {
        System.out.println("closing...");
        socket.close();
      }
    } finally {
      s.close();
    }
  } 
} ///:~

On remarque que ServerSocket ne nécessite qu'un numéro de port, et non une adresse IP (puisqu'il tourne sur cette machine !). Lorsqu'on appelle accept( ) la méthode bloque jusqu'à ce qu'un client tente de se connecter. Cela signifie qu'elle est en attente d'une demande de connexion, mais elle ne bloque pas les autres processus (voir le titre XVI). Une fois la connexion établie, accept( ) renvoie un objet Socket qui représente cette connexion.

La même logique est utilisée pour le Socket renvoyé par accept( ). Si accept( ) échoue, nous devons supposer que le Socket n'existe pas et qu'il ne monopolise aucune ressource, et donc qu'il n'est pas besoin de le nettoyer. Au contraire, si accept( ) se termine avec succès, les instructions qui suivent doivent se trouver dans un bloc try-finally pour que Socket soit toujours nettoyé si l'une d'entre elles échoue. Il faut porter beaucoup d'attention à cela, car les sockets sont des ressources importantes qui ne résident pas en mémoire, et on ne doit pas oublier de les fermer (car il n'existe pas en Java de destructeur qui le ferait pour nous).

Le ServerSocket et le Socket fournis par accept( ) sont imprimés sur System.out. Leur méthode toString( ) est donc appelée automatiquement. Voici le résultat :

 
Sélectionnez
ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]
Socket[addr=127.0.0.1,PORT=1077,localport=8080]

En raccourci, on peut voir comment cela « colle » avec ce que fait le client.

Le reste du programme consiste seulement à ouvrir des fichiers pour lire et écrire, sauf que InputStream et OutputStream sont créés à partir de l'objet Socket. Les deux objets InputStream et OutputStream sont convertis en objets Reader et Writer au moyen des « classes de conversion InputStreamReader et OutputStreamWriter, respectivement. On aurait pu travailler directement avec les classes Java 1.0 InputStream et OutputStream, mais pour la sortie il y a un avantage certain à utiliser l'approche Writer. C'est évident avec PrintWriter, qui possède un constructeur surchargé prenant en compte un deuxième argument, un flag boolean indiquant que le tampon de sortie doit être automatiquement vidé après chaque println( ) (mais non après les instructions print( )). Chaque fois qu'on écrit sur out, son buffer doit être vidé afin que l'information parte sur le réseau. Le vidage du tampon est important dans cet exemple particulier, car aussi bien le serveur que le client attendent de l'autre une ligne complète avant de la traiter. Si le tampon n'est pas vidé à chaque ligne, l'information ne circule pas sur le réseau tant que le buffer n'est pas plein, ce qui occasionnerait de nombreux problèmes dans cet exemple.

Lorsqu'on écrit des programmes réseau, il faut être très attentif à l'utilisation du vidage automatique de buffer. Chaque fois que l'on vide un buffer, un paquet est créé et envoyé. Dans notre exemple, c'est exactement ce que nous recherchons, puisque si le paquet contenant la ligne n'est pas envoyé, l'échange entre serveur et client sera stoppé. Dit d'une autre manière, la fin de ligne est la fin du message. Mais dans de nombreux cas, les messages ne sont pas découpés en lignes et il sera plus efficace de ne pas utiliser le vidage automatique de buffer et à la place de laisser le gestionnaire du buffer décider du moment pour construire et envoyer un paquet. De cette manière, les paquets seront plus importants et le processus plus rapide.

Remarquons que, comme tous les flux qu'on peut ouvrir, ceux-ci sont tamponnés. À la fin de ce chapitre, vous trouverez un exercice montrant ce qui se passe lorsqu'on ne tamponne pas les flux (tout est ralenti).

La boucle while infinie lit les lignes depuis le BufferedReader in et écrit sur System.out et PrintWriter out. Remarquons que in et out peuvent être n'importe quel flux, ils sont simplement connectés au réseau.

Lorsque le client envoie une ligne contenant juste le mot END, « la boucle est interrompue et le programme ferme le Socket.

Et voici le client :

 
Sélectionnez
//: c15:JabberClient.java
// Client simplifié se contentant d'envoyer
// des lignes au serveur et de lire les lignes
// que le serveur lui envoie.
import java.net.*;
import java.io.*;

public class JabberClient {
  public static void main(String[ « args) 
      throws IOException {
// Appeler getByName() avec un argument null revient
    // à utiliser une adresse IP spéciale "Boucle Locale" 
// pour faire des tests réseau sur une seule machine.

     InetAddress addr = 
      InetAddress.getByName(null);
    // Il est également possible d'utiliser 
    // l'adresse ou le nom:
    // InetAddress addr = 
    //    InetAddress.getByName("127.0.0.1");
    // InetAddress addr = 
    //    InetAddress.getByName("localhost");
    System.out.println("addr = " + addr);
    Socket socket = 
      new Socket(addr, JabberServer.PORT);
    // Le code doit être inclus dans un bloc
    // try-finally afin de garantir 
    // que socket sera fermé:
    try {
      System.out.println("socket = " + socket);
      BufferedReader in =        new BufferedReader(
          new InputStreamReader(
            socket.getInputStream()));
      // Le tampon de sortie est automatiquement
      // vidé par PrintWriter:
      PrintWriter out =        new PrintWriter(
          new BufferedWriter(
            new OutputStreamWriter(
              socket.getOutputStream())),true);
      for(int i = 0; i < 10; i ++) {
        out.println("howdy " + i);
        String str = in.readLine();
        System.out.println(str);
      }
      out.println("END");
    } finally {
      System.out.println("closing...");
      socket.close();
    }
  }
} ///:~

La méthode main( ) montre qu'il existe trois manières de produire l'InetAddress de l'adresse IP de la boucle locale  : avec null, localhost, ou bien l'adresse réservée et explicite 127.0.0.1. Bien entendu, si l'on désire se connecter à une machine du réseau, il suffit d'y substituer son adresse. Lorsque InetAddress addr est imprimée (via l'appel automatique de sa méthode toString( )), voici le résultat :

 
Sélectionnez
localhost/127.0.0.1

Lorsque getByName( ) a été appelée avec un argument null, elle a cherché par défaut localhost, ce qui a fourni l'adresse spéciale 127.0.0.1.

Remarquons que le Socket nommé socket est créé avec InetAddress ainsi que le numéro de port. Pour comprendre ce qui se passe lorsqu'on imprime un de ces objets Socket, il faut se souvenir qu'une connexion Internet est déterminée de manière unique à partir de quatre données : clientHost, clientPortNumber, serverHost, et serverPortNumber. Lorsque le serveur démarre, il prend en compte le port qui lui est assigné (8080) sur la machine locale (127.0.0.1). Lorsque le client démarre, le premier port suivant disponible sur sa machine lui est alloué, 1077 dans ce cas, qui se trouve sur la même machine (127.0.0.1) que le serveur. Maintenant, pour que les données circulent entre le client et le serveur, chacun doit savoir où les envoyer. En conséquence, pendant la connexion au serveur connu, le client envoie une adresse de retour afin que le serveur sache où envoyer ses données. Ce qu'on peut voir dans la sortie de l'exemple côté serveur :

 
Sélectionnez
Socket[addr=127.0.0.1,port=1077,localport=8080]

Cela signifie que le serveur vient d'accepter une demande de connexion provenant de 127.0.0.1 sur le port 1077 alors qu'il est à l'écoute sur le port local (8080). Du côté client :

 
Sélectionnez
Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]

ce qui signifie que le client vient d'établir une connexion à 127.0.0.1 sur le port 8080 en utilisant le port local 1077.

Il faut remarquer que chaque fois qu'on relance le client, le numéro du port local est incrémenté. Il commence à 1025 (un après le bloc de ports réservé) et ne cesse d'augmenter jusqu'à ce qu'on reboote la machine, auquel cas il recommence à 1025 (sur les machines UNIX, lorsque la limite supérieure de la plage accordée aux sockets est atteinte, on recommence avec la plus petite valeur possible).

L'objet Socket créé, le processus consistant à en faire un BufferedReader puis un PrintWriter est le même que pour le serveur (encore une fois, dans les deux cas on commence par un Socket). Ici, le client entame la conversation en envoyant la chaîne « [howdy] » suivie d'un nombre. Remarquons que le buffer doit être vidé à nouveau (ce qui est automatique via le deuxième argument du constructeur de PrintWriter). Si le buffer n'est pas vidé, la conversation est complètement suspendue parce que le « howdy » initial ne sera jamais envoyé (le buffer n'est pas assez rempli pour que l'envoi se fasse automatiquement). Chaque ligne renvoyée par le serveur est écrite sur System.out pour vérifier que tout fonctionne correctement. Pour arrêter l'échange, le client envoie le mot « connu » END. Si le client ne se manifeste plus, le serveur lance une exception.

Remarquons qu'ici aussi le même soin est apporté pour assurer que la ressource réseau que représente le Socket est relâchée correctement, au moyen d'un bloc try-finally.

Les sockets fournissent une connexion » dédiée « qui persiste jusqu'à ce qu'elle soit explicitement déconnectée (la connexion dédiée peut encore être rompue de manière non explicite si l'un des deux côtés ou un lien intermédiaire de la connexion se plante). La conséquence est que les deux parties sont verrouillées en communication et que la connexion est constamment ouverte. Cela peut paraître une approche logique du travail en réseau, en fait on surcharge le réseau. Plus loin dans ce chapitre on verra une approche différente du travail en réseau, dans laquelle les connexions seront temporaires.

XVII-A-3. Servir des clients multiples

Le programme JabberServer fonctionne, mais ne peut traiter qu'un client à la fois. Dans un serveur typique, on désire traiter plusieurs clients en même temps. La réponse est le multithreading, et dans les langages ne supportant pas cette fonctionnalité cela entraîne toutes sortes de complications. Dans le titre XVI, nous avons vu que le multithreading de Java est aussi simple que possible, si l'on considère le multithreading comme un sujet quelque peu complexe. Parce que gérer des threads est relativement simple en Java, écrire un serveur prenant en compte de multiples clients est relativement facile.

L'idée de base est de construire dans le serveur un seul ServerSocket et d'appeler accept( ) pour attendre une nouvelle connexion. Au retour d'accept( ), on crée un nouveau thread utilisant le Socket résultant, thread dont le travail est de servir ce client particulier. Puis on appelle à nouveau accept( ) pour attendre un nouveau client.

Dans le code serveur suivant, on remarquera qu'il ressemble à l'exemple JabberServer.java sauf que toutes les opérations destinées à servir un client particulier ont été déplacées dans une classe thread séparée :

 
Sélectionnez
//: c15:MultiJabberServer.java
// Un serveur utilisant le multithreading 
// pour traiter un nombre quelconque de clients.
import java.io.*;
import java.net.*;

class ServeOneJabber extends Thread {
  private Socket socket;
  private BufferedReader in;
  private PrintWriter out;
  public ServeOneJabber(Socket s) 
      throws IOException {
    socket = s;
    in = 
      new BufferedReader(
        new InputStreamReader(
          socket.getInputStream()));
    // Autoriser l'autovidage:
    out = 
      new PrintWriter(
        new BufferedWriter(
          new OutputStreamWriter(
            socket.getOutputStream())), true);
    // Si l'un des appels ci-dessus résulte en une 
    // exception, l'appelant a la responsabilité
    // de fermer socket. Sinon le thread
    // s'en chargera.
    start(); // Appelle run()
  }
  public void run() {
    try {
      while (true) {  
        String str = in.readLine();
        if (str.equals("END")) break;
        System.out.println("Echoing: " + str);
        out.println(str);
      }
      System.out.println("closing...");
    } catch(IOException e) {
      System.err.println("IO Exception");
    } finally {
      try {
        socket.close();
      } catch(IOException e) {
        System.err.println("Socket not closed");
      }
    }
  }
}

public class MultiJabberServer {  
  static final int PORT = 8080;
  public static void main(String[ « args)
      throws IOException {
    ServerSocket s = new ServerSocket(PORT);
    System.out.println("Server Started");
    try {
      while(true) {
// On attend ici jusqu'à avoir 
// une demande de connexion:
        Socket socket = s.accept();
        try {
          new ServeOneJabber(socket);
        } catch(IOException e) {
          // En cas d'échec, fermer l'objet socket,
          // sinon le thread le fermera:
          socket.close();
        }
      }
    } finally {
      s.close();
    }
  } 
} ///:~

Chaque fois qu'un nouveau client se connecte, le thread ServeOneJabber prend l'objet Socket produit par accept( ) dans main( ). Puis, comme auparavant, il crée un BufferedReader et un objet PrintWriter (avec autovidage du buffer) à partir du Socket. Finalement, il appelle la méthode spéciale start( ) de la classe Thread qui initialise le thread puis appelle run( ). On réalise ainsi le même genre de traitement que dans l'exemple précédent : lire quelque chose sur la socket et le renvoyer en écho jusqu'à l'arrivée du signal spécial » END.

À nouveau, il nous faut penser à notre responsabilité en ce qui concerne la ressource socket. Dans ce cas, la socket est créée hors de ServeOneJabber et donc la responsabilité doit être partagée. Si le constructeur de ServeOneJabber échoue, il suffit qu'il lance une exception vers l'appelant, qui nettoiera alors le thread. Mais dans le cas contraire, l'objet ServeOneJabber a la responsabilité de nettoyer le thread, dans sa méthode run( ).

Remarquons la simplicité de MultiJabberServer. Comme auparavant, on crée un ServerSocket, et on appelle accept( ) pour autoriser une nouvelle connexion. Mais cette fois, la valeur de retour de accept( ) (un Socket) est passée au constructeur de ServeOneJabber, qui crée un nouveau thread afin de prendre en compte cette connexion. La connexion terminée, le thread se termine tout simplement.

Si la création du ServerSocket échoue, à nouveau une exception est lancée par main( ). En cas de succès, le bloc try-finally extérieur garantit le nettoyage. Le bloc try-catch intérieur protège d'une défaillance du constructeur ServeOneJabber  ; en cas de succès, le thread ServeOneJabber fermera la socket associée.

Afin de tester que le serveur prend réellement en compte plusieurs clients, le programme suivant crée beaucoup de clients (sous la forme de threads) et se connecte au même serveur. Le nombre maximum de threads autorisés est fixé par la constante final int MAX_THREADS.

 
Sélectionnez
//: c15:MultiJabberClient.java
// Client destiné à tester MultiJabberServer
// en lançant des clients multiples.
import java.net.*;
import java.io.*;

class JabberClientThread extends Thread {
  private Socket socket;
  private BufferedReader in;
  private PrintWriter out;
  private static int counter = 0;
  private int id = counter++;
  private static int threadcount = 0;
  public static int threadCount() { 
    return threadcount; 
  }
  public JabberClientThread(InetAddress addr) {
    System.out.println("Making client " + id);
    threadcount++;
    try {
      socket = 
        new Socket(addr, MultiJabberServer.PORT);
    } catch(IOException e) {
      System.err.println("Socket failed");
      // Si la création de socket échoue, 
      // il n'y a rien à nettoyer.
    }
    try {    
      in = 
        new BufferedReader(
          new InputStreamReader(
            socket.getInputStream()));
      // Autoriser l'autovidage du tampon:
      out = 
        new PrintWriter(
          new BufferedWriter(
            new OutputStreamWriter(
              socket.getOutputStream())), true);
      start();
    } catch(IOException e) {
      // socket doit être fermé sur n'importe quelle
      // erreur autre que celle de son constructeur:
      try {
        socket.close();
      } catch(IOException e2) {
        System.err.println("Socket not closed");
      }
    }
    // Sinon socket doit être fermé par
    // la méthode run() du thread.
  }
  public void run() {
    try {
      for(int i = 0; i < 25; i++) {
        out.println("Client " + id + ": " + i);
        String str = in.readLine();
        System.out.println(str);
      }
      out.println("END");
    } catch(IOException e) {
      System.err.println("IO Exception");
    } finally {
      // Toujours fermer:
      try {
        socket.close();
      } catch(IOException e) {
        System.err.println("Socket not closed");
      }
      threadcount--; // Fin de ce thread
    }
  }
}

public class MultiJabberClient {
  static final int MAX_THREADS = 40;
  public static void main(String[ « args) 
      throws IOException, InterruptedException {
    InetAddress addr = 
      InetAddress.getByName(null);
    while(true) {
      if(JabberClientThread.threadCount() 
         < MAX_THREADS)
        new JabberClientThread(addr);
      Thread.currentThread().sleep(100);
    }
  }
} ///:~

La variable threadCount garde la trace du nombre de JabberClientThread existant actuellement. Elle est incrémentée dans le constructeur et décrémentée lorsque run( ) termine (ce qui signifie que le thread est en train de se terminer). Dans MultiJabberClient.main( ) le nombre de threads est testé, on arrête d'en créer s'il y en a trop. Dans ce cas la méthode s'endort. De cette manière, certains threads peuvent se terminer et de nouveaux pourront être créés. Vous pouvez faire l'expérience, en changeant la valeur de MAX_THREADS, afin de savoir à partir de quel nombre de connexions votre système commence à avoir des problèmes.

XVII-A-4. Les Datagrammes

Les exemples que nous venons de voir utilisent le Protocole de Contrôle de Transmission Transmission Control Protocol (TCP, connu également sous le nom de stream-based sockets- sockets basés sur les flux, NdT), qui est conçu pour une sécurité maximale et qui garantit que les données ne seront pas perdues. Il permet la retransmission des données perdues, il fournit plusieurs chemins au travers des différents routeurs au cas où l'un d'eux tombe en panne, enfin les octets sont délivrés dans l'ordre où ils ont été émis. Ces contrôles et cette sécurité ont un prix : TCP a un overhead élevé.

Il existe un deuxième protocole, appelé User Datagram Protocol (UDP), qui ne garantit pas que les paquets seront acheminés ni qu'ils arriveront dans l'ordre d'émission. On l'appelle protocole peu sûr « (TCP est un protocole sûr), ce qui n'est pas très vendeur, mais il peut être très utile, car il est beaucoup plus rapide. Il existe des applications, comme la transmission d'un signal audio, pour lesquelles il n'est pas critique de perdre quelques paquets çà et là, mais où en revanche la vitesse est vitale. Autre exemple, considérons un serveur de date et heure, pour lequel on n'a pas vraiment à se préoccuper de savoir si un message a été perdu. De plus, certaines applications sont en mesure d'envoyer à un serveur un message UDP et ensuite d'estimer que le message a été perdu si elles n'ont pas de réponse dans un délai raisonnable.

En règle générale, la majorité de la programmation réseau directe est réalisée avec TCP, et seulement occasionnellement avec UDP. Vous trouverez un traitement plus complet sur UDP, avec un exemple, dans la première édition de ce livre (disponible sur le CD-ROM fourni avec ce livre, ou en téléchargement libre depuis www.BruceEckel.com).

XVII-A-5. Utiliser des URL depuis un applet

Un applet a la possibilité d'afficher n'importe quelle URL au moyen du navigateur sur lequel il tourne. Ceci est réalisé avec la ligne suivante :

 
Sélectionnez
getAppletContext().showDocument(u);

dans laquelle u est l'objet URL. Voici un exemple simple qui nous redirige vers une autre page Web. Bien que nous soyons seulement redirigés vers une page HTML, nous pourrions l'être de la même manière vers la sortie d'un programme CGI.

 
Sélectionnez
//: c15:ShowHTML.java
// <applet code=ShowHTML width=100 height=50>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.io.*;
import com.bruceeckel.swing.*;

public class ShowHTML extends JApplet {
  JButton send = new JButton("Go");
  JLabel l = new JLabel();
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    send.addActionListener(new Al());
    cp.add(send);
    cp.add(l);
  }
  class Al implements ActionListener {
    public void actionPerformed(ActionEvent ae) {
      try {
        // Ceci pourrait être un programme CGI
        // au lieu d'une page HTML.
        URL u = new URL(getDocumentBase(), 
          "FetcherFrame.html");
        // Afficher la sortie de l'URL en utilisant
        // le navigateur Web, comme une page ordinaire:
        getAppletContext().showDocument(u);
      } catch(Exception e) {
        l.setText(e.toString());
      }
    }
  }
  public static void main(String[ « args) {
    Console.run(new ShowHTML(), 100, 50);
  }
} ///:~

Il est beau de voir combien la classe URL nous évite la complexité. Il est possible de se connecter à un serveur Web sans avoir à connaître ce qui se passe à bas niveau.

XVII-A-5-a. Lire un fichier depuis un serveur

Une variante du programme ci-dessus consiste à lire un fichier situé sur le serveur. Dans ce cas, le fichier est décrit par le client :

 
Sélectionnez
//: c15:Fetcher.java
// <applet code=Fetcher width=500 height=300>
// </applet>
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.net.*;
import java.io.*;
import com.bruceeckel.swing.*;

public class Fetcher extends JApplet {
  JButton fetchIt= new JButton("Fetch the Data");
  JTextField f = 
    new JTextField("Fetcher.java", 20);
  JTextArea t = new JTextArea(10,40);
  public void init() {
    Container cp = getContentPane();
    cp.setLayout(new FlowLayout());
    fetchIt.addActionListener(new FetchL());
    cp.add(new JScrollPane(t));
    cp.add(f); cp.add(fetchIt);
  }
  public class FetchL implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      try {
        URL url = new URL(getDocumentBase(),
          f.getText());
        t.setText(url + "\n");
        InputStream is = url.openStream();
        BufferedReader in = new BufferedReader(
          new InputStreamReader(is));
        String line;
        while ((line = in.readLine()) != null)
          t.append(line + "\n");
      } catch(Exception ex) {
        t.append(ex.toString());
      }
    }
  }
  public static void main(String[ « args) {
    Console.run(new Fetcher(), 500, 300);
  }
} ///:~

La création de l'objet URL est semblable à celle de l'exemple précédent. Comme d'habitude, getDocumentBase( ) est le point de départ, mais cette fois le nom de fichier est lu depuis le JTextField. Une fois l'objet URL créé, sa version String est affichée dans le JTextArea afin que nous puissions la visualiser. Puis un InputStream est créé à partir de l'URL, qui dans ce cas va simplement créer un flux de caractères vers le fichier. Après avoir été convertie vers un Reader et bufferisée, chaque ligne est lue et ajoutée au JTextArea. Notons que le JTextArea est placé dans un JScrollPane afin que le scrolling soit pris en compte automatiquement.

XVII-A-6. En savoir plus sur le travail en réseau

En réalité, bien d'autres choses à propos du travail en réseau pourraient être traitées dans cette introduction. Le travail en réseau Java fournit également un support correct et étendu pour les URL, incluant des handlers de protocole pour les différents types de contenu que l'on peut trouver sur un site Internet. Vous découvrirez d'autres fonctionnalités réseau de Java, complètement et minutieusement décrites dans Java Network Programming de Elliotte Rusty Harold (O.Reilly, 1997).

XVII-B. Se connecter aux bases de données : Java Database Connectivity (JDBC)

On a pu estimer que la moitié du développement de programmes implique des opérations client/serveur. Une des grandes promesses tenues par Java fut sa capacité à construire des applications de base de données client/serveur indépendantes de la plate-forme. C'est ce qui est réalisé avec Java DataBase Connectivity (JDBC).

Un des problèmes majeurs rencontrés avec les bases de données fut la guerre des fonctionnalités entre les compagnies fournissant ces bases de données. Il existe un langage standard de base de données, Structured Query Language (SQL-92), mais il est généralement préférable de connaître le vendeur de la base de données avec laquelle on travaille, malgré ce standard. JDBC est conçu pour être indépendant de la plate-forme, ainsi lorsqu'on programme on n'a pas à se soucier de la base de données qu'on utilise. Il est toutefois possible d'effectuer à partir de JDBC des appels spécifiques vers une base particulière afin de ne pas être limité dans ce que l'on pourrait faire.

Il existe un endroit pour lequel les programmeurs auraient besoin d'utiliser les noms de type SQL, c'est dans l'instruction SQL TABLE CREATE lorsqu'on crée une nouvelle table de base de données et qu'on définit le type SQL de chaque colonne. Malheureusement il existe des variantes significatives entre les types SQL supportés par différents produits base de données. Des bases de données différentes supportant des types SQL de même sémantique et de même structure peuvent appeler ces types de manières différentes. La plupart des bases de données importantes supportent un type de données SQL pour les grandes valeurs binaires : dans Oracle ce type s'appelle LONG RAW, Sybase le nomme IMAGE, Informix BYTE, et DB2 LONG VARCHAR FOR BIT DATA. Par conséquent, si on a pour but la portabilité des bases de données il faut essayer de n'utiliser que les identifiants de type SQL génériques.

La portabilité est une question d'actualité lorsqu'on écrit pour un livre dont les lecteurs vont tester les exemples avec toutes sortes de stockage de données inconnus. J'ai essayé de rendre ces exemples aussi portables qu'il était possible. Remarquez également que le code spécifique à la base de données a été isolé afin de centraliser toutes les modifications que vous seriez obligés d'effectuer pour que ces exemples deviennent opérationnels dans votre environnement.

JDBC, comme bien des API Java, est conçu pour être simple. Les appels de méthode que l'on utilise correspondent aux opérations logiques auxquelles on pense pour obtenir des données depuis une base de données, créer une instruction, exécuter la demande, et voir le résultat.

Pour permettre cette indépendance de plate-forme, JDBC fournit un gestionnaire de driver qui maintient dynamiquement les objets driver nécessités par les interrogations de la base. Ainsi si on doit se connecter à trois différentes sortes de base, on a besoin de trois objets driver différents. Les objets driver s'enregistrent eux-mêmes auprès du driver manager lors de leur chargement, et on peut forcer le chargement avec la méthode Class.forName( ).

Pour ouvrir une base de données, il faut créer une « URL de base de données » qui spécifie :

  1. Qu'on utilise JDBC avec « jdbc » ;
  2. Le « sous-protocole » : le nom du driver ou le nom du mécanisme de connectivité à la base de données. Parce que JDBC a été inspiré par ODBC, le premier sous-protocole disponible est la « passerelle » jdbc-odbc », « spécifiée par « odbc » ;
  3. L'identifiant de la base de données. Il varie avec le driver de base de donnée utilisé, mais il existe généralement un nom logique qui est associé par le software d'administration de la base à un répertoire physique où se trouvent les tables de la base de données. Pour qu'un identifiant de base de données ait une signification, il faut l'enregistrer en utilisant le software d'administration de la base (cette opération varie d'une plate-forme à l'autre).

Toute cette information est condensée dans une chaîne de caractères, l'URL de la base de données. Par exemple, pour se connecter au moyen du protocole ODBC à une base appelée » people, « l'URL de base de données doit être :

 
Sélectionnez
String dbUrl = "jdbc:odbc:people";

Si on se connecte à travers un réseau, l'URL de base de données doit contenir l'information de connexion identifiant la machine distante, et peut alors devenir intimidante. Voici en exemple la base de données CloudScape que l'on appelle depuis un client éloigné en utilisant RMI :

 
Sélectionnez
jdbc:rmi://192.168.170.27:1099/jdbc:cloudscape:db

En réalité cette URL de base de données comporte deux appels jdbc en un. La première partie jdbc:rmi://192.168.170.27:1099/ « utilise RMI pour effectuer une connexion sur le moteur distant de base de données à l'écoute sur le port 1099 de l'adresse IP 192.168.170.27. La deuxième partie de l'URL, [jdbc:cloudscape:db] représente une forme plus connue utilisant le sous-protocole et le nom de la base, mais elle n'entrera en jeu que lorsque la première section aura établi la connexion à la machine distante via RMI.

Lorsqu'on est prêt à se connecter à une base, on appelle la méthode statique DriverManager.getConnection( ) en lui fournissant l'URL de base de données, le nom d'utilisateur et le mot de passe pour accéder à la base. On reçoit en retour un objet Connection que l'on peut ensuite utiliser pour effectuer des demandes et manipuler la base de données.

L'exemple suivant ouvre une base d'« information de contact » et cherche le nom de famille d'une personne, donné sur la ligne de commande. Il sélectionne d'abord les noms des personnes qui ont une adresse e-mail, puis imprime celles qui correspondent au nom donné :

 
Sélectionnez
//: c15:jdbc:Lookup.java
// Cherche les adresses e-mail dans une  
// base de données locale en utilisant JDBC.
import java.sql.*;

public class Lookup {
  public static void main(String[ « args) 
  throws SQLException, ClassNotFoundException {
    String dbUrl = "jdbc:odbc:people";
    String user = "";
    String password = "";
    // Charger le driver (qui s'enregistrera lui-même)
    Class.forName(
      "sun.jdbc.odbc.JdbcOdbcDriver");
    Connection c = DriverManager.getConnection(
      dbUrl, user, password);
    Statement s = c.createStatement();
    // code SQL:
    ResultSet r = 
      s.executeQuery(
        "SELECT FIRST, LAST, EMAIL " +
        "FROM people.csv people " +
        "WHERE " +
        "(LAST='" + args[0 « + "') " +
        " AND (EMAIL Is Not Null) " +
        "ORDER BY FIRST");
    while(r.next()) {
      // minuscules et majuscules n'ont
      // aucune importance:
      System.out.println(
        r.getString("Last") + ", " 
        + r.getString("fIRST")
        + ": " + r.getString("EMAIL") );
    }
    s.close(); // fermer également ResultSet
  }
} ///:~

Une URL de base de données est créée comme précédemment expliqué. Dans cet exemple, il n'y a pas de protection par mot de passe, c'est pourquoi le nom d'utilisateur et le mot de passe sont des chaînes vides.

Une fois la connexion établie avec DriverManager.getConnection( ) l'objet résultant Connection sert à créer un objet Statement au moyen de la méthode createStatement( ). Avec cet objet Statement, on peut appeler executeQuery( ) en lui passant une chaîne contenant une instruction standard SQL-92 (il n'est pas difficile de voir comment générer cette instruction automatiquement, on n'a donc pas à connaître grand-chose de SQL).

La méthode executeQuery( ) renvoie un objet ResultSet, qui est un itérateur : la méthode next( ) fait pointer l'objet itérateur sur l'enregistrement suivant dans l'instruction, ou bien renvoie la valeur false si la fin de l'ensemble résultat est atteinte. On obtient toujours un objet ResultSet en réponse à la méthode executeQuery( ) même lorsqu'une demande débouche sur un ensemble vide (autrement dit aucune exception n'est générée). Notons qu'il faut appeler la méthode next( ) au moins une fois avant de tenter de lire un enregistrement de données. Si l'ensemble résultant est vide, ce premier appel de next( ) renverra la valeur false. Pour chaque enregistrement dans l'ensemble résultant, on peut sélectionner les champs en utilisant (entre autres solutions) une chaîne représentant leur nom. Remarquons aussi que la casse du nom de champ est ignorée dans les transactions avec une base de données. Le type de données que l'on veut récupérer est déterminé par le nom de la méthode appelée : getInt( ), getString( ), getFloat( ), etc. À ce moment, on dispose des données de la base en format Java natif et on peut les traiter comme bon nous semble au moyen du code Java habituel.

XVII-B-1. Faire fonctionner l'exemple

Avec JDBC, il est relativement simple de comprendre le code. Le côté difficile est de faire en sorte qu'il fonctionne sur votre système particulier. La raison de cette difficulté est que vous devez savoir comment charger proprement le driver JDBC, et comment initialiser une base de données en utilisant le software d'administration de la base.

Bien entendu, ce travail peut varier de façon radicale d'une machine à l'autre, mais la manière dont j'ai procédé pour le faire fonctionner sur un système Windows 32 bits vous donnera sans doute des indications qui vous aideront à attaquer votre propre situation.

XVII-B-1-a. Étape 1 : Trouver le Driver JDBC

Le programme ci-dessus contient l'instruction :

 
Sélectionnez
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

Ceci suggère une structure de répertoire, ce qui est trompeur. Avec cette installation particulière du JDK 1.1, il n'existe pas de fichier nommé JdbcOdbcDriver.class, et si vous avez cherché ce fichier après avoir regardé l'exemple vous avez dû être déçu. D'autres exemples publiés utilisent un pseudonom, comme « myDriver.ClassName »,  ce qui nous aide encore moins. En fait, l'instruction de chargement du driver jdbc-odbc (le seul existant actuellement dans le JDK) apparaît en peu d'endroits dans la documentation en ligne (en particulier, une page appelée JDBC-ODBC Bridge Driver). Si l'instruction de chargement ci-dessus ne fonctionne pas, il est possible que le nom ait été changé à l'occasion d'une évolution de version Java, et vous devez repartir en chasse dans la documentation.

XVII-B-1-b. Étape 2 : Configurer la base de données

Ici aussi, ceci est spécifique à Windows 32-bits ; vous devrez sans doute rechercher quelque peu pour savoir comment faire sur votre propre plate-forme.

Pour commencer, ouvrir le panneau de configuration. Vous devriez trouver deux icônes traitant de ODBC. « Il faut utiliser celle qui parle de ODBC 32-bits, l'autre n'étant là que pour la compatibilité ascendante avec le software ODBC 16-bits, et ne serait d'aucune utilité pour JDBC. En ouvrant l'icône ODBC 32-bits, vous allez voir une boîte de dialogue à onglets, parmi lesquels « User DSN, System DSN, File DSN, etc. », DSN signifiant  Data Source Name. Il apparaît que pour la passerelle JDBC-ODBC, le seul endroit important pour créer votre base de données est System DSN, mais vous devez également tester votre configuration et créer des demandes, et pour cela vous devez également créer votre base dans File DSN. Ceci permet à l'outil Microsoft Query (qui fait partie de Microsoft Office) de trouver la base. Remarquez que d'autres outils de demande sont disponibles chez d'autres vendeurs.

La base de données la plus intéressante est l'une de celles que vous utilisez déjà. Le standard ODBC supporte différents formats de fichier y compris les vénérables chevaux de labour tels que DBase. Cependant, il inclut aussi le format « ASCII, champs séparés par des virgules », que n'importe quel outil de données est capable de créer. En ce qui me concerne, j'ai simplement pris ma base de données « people » que j'ai maintenue depuis des années au moyen de divers outils de gestion d'adresse et que j'ai exportée en tant que fichier « ASCII à champs séparés par des virgules » (ils ont généralement une extension .csv). Dans la section « System DSN » j'ai choisi « Add », puis le driver texte pour mon fichier ASCII csv, puis décoché « use current directory » pour me permettre de spécifier le répertoire où j'avais exporté mon fichier de données.

Remarquez bien qu'en faisant cela vous ne spécifiez pas réellement un fichier, mais seulement un répertoire. Ceci parce qu'une base de données se trouve souvent sous la forme d'un ensemble de fichiers situés dans un même répertoire (bien qu'elle puisse aussi bien se trouver sous d'autres formes). Chaque fichier contient généralement une seule table, et une instruction SQL peut produire des résultats issus de plusieurs tables de la base (on appelle ceci une relation). Une base contenant une seule table (comme ma base « people » est généralement appelée flat-file database. La plupart des problèmes qui vont au-delà du simple stockage et déstockage de données nécessitent des tables multiples mises en relation par des relations afin de fournir les résultats voulus, on les appelle bases de données relationnelles.

XVII-B-1-c. Étape 3 : Tester la configuration

Pour tester la configuration, il faut trouver un moyen de savoir si la base est visible depuis un programme qui l'interrogerait. Bien entendu, on peut tout simplement lancer le programme exemple JDBC ci-dessus, en incluant l'instruction :

 
Sélectionnez
Connection c = DriverManager.getConnection(
  dbUrl, user, password);

Si une exception est lancée, c'est que la configuration était incorrecte.

Cependant, à ce point, il est très utile d'utiliser un outil de génération de requêtes. J'ai utilisé Microsoft Query qui est livré avec Microsoft Office, mais vous pourriez préférer un autre outil. L'outil de requête doit connaître l'emplacement de la base, et Microsoft Query exigeait que j'ouvre l'onglet administrateur ODBC File DSN  et que j'ajoute une nouvelle entrée, en spécifiant à nouveau le driver texte et le répertoire contenant ma base de données. On peut donner n'importe quel nom à cette entrée, mais il est préférable d'utiliser le nom déjà fourni dans l'onglet System DSN.

Ceci fait, vous saurez si votre base est disponible en créant une nouvelle requête au moyen de votre générateur de requêtes.

XVII-B-1-d. Étape 4 : Générer votre requête SQL

La requête créée avec Microsoft Query m'a montré que ma base de données était là et prête à fonctionner, mais a aussi généré automatiquement le code SQL dont j'avais besoin pour l'insérer dans mon programme Java. Je voulais une requête qui recherche les enregistrements contenant le même nom que celui qui était fourni sur la ligne de commande d'appel du programme. Ainsi pour commencer, j'ai cherché un nom particulier, « Eckel ». Je voulais également n'afficher que ceux qui avaient une adresse e-mail associée. Voici les étapes que j'ai suivies pour créer cette requête :

  1. Ouvrir une nouvelle requête au moyen du Query Wizard. Sélectionner la base « people » (ceci est équivalent à ouvrir la connexion avec la base au moyen de l'URL de base de données appropriée) ;
  2. Dans la base, sélectionner la table « people ». Dans la table, choisir les colonnes FIRST, LAST, et EMAIL ;
  3. Sous « Filter Data », choisir LAST et sélectionner « equals » avec comme argument « Eckel ». Cliquer sur le bouton radio « And » ;
  4. Choisir EMAIL et sélectionner « Is not Null ».
  5. Sous « Sort By », choisir FIRST.

Le résultat de cette requête vous montrera si vous obtenez ce que vous vouliez.

Maintenant, cliquez sur le bouton SQL et sans aucun effort de votre part vous verrez apparaître le code SQL correct, prêt pour un copier-coller. Le voici pour cette requête :

 
Sélectionnez
SELECT people.FIRST, people.LAST, people.EMAIL
FROM people.csv people
WHERE (people.LAST='Eckel') AND 
(people.EMAIL Is Not Null)
ORDER BY people.FIRST

Il serait très facile de se tromper dans le cas de requêtes plus compliquées, mais en utilisant un générateur de requêtes vous pouvez tester votre requête interactivement et générer automatiquement le code correct. Il serait difficile d'affirmer qu'il est préférable de réaliser cela manuellement.

XVII-B-1-e. Étape 5 : Modifier et insérer votre requête

Remarquons que le code ci-dessus ne ressemble pas à celui qui est utilisé dans le programme. Ceci est dû au fait que le générateur de requêtes utilise des noms complètement qualifiés, même lorsqu'une seule table est en jeu (lorsqu'on utilise plus d'une table, la qualification empêche les collisions entre colonnes de même nom appartenant à des tables différentes). Puisque cette requête ne concerne qu'une seule table, on peut - optionnellement - supprimer le qualificateur « people » dans la plupart des noms, de cette manière :

 
Sélectionnez
SELECT FIRST, LAST, EMAIL
FROM people.csv people
WHERE (LAST='Eckel') AND 
(EMAIL Is Not Null)
ORDER BY FIRST

En outre, on ne va pas coder en dur le nom à rechercher. Au lieu de cela, il sera créé à partir du nom fourni en argument sur la ligne de commande d'appel du programme. Ces changements effectués l'instruction SQL générée dynamiquement dans un objet String devient :

 
Sélectionnez
"SELECT FIRST, LAST, EMAIL " +
"FROM people.csv people " +
"WHERE " +
"(LAST='" + args[0 « + "') " +
" AND (EMAIL Is Not Null) " +
"ORDER BY FIRST");

SQL possède un autre mécanisme d'insertion de noms dans une requête, appelé procédures stockées, stored procedures, utilisées pour leur vitesse. Mais pour la plupart de vos expérimentations sur les bases de données et pour votre apprentissage, il est bon de construire vos chaînes de requêtes en Java.

On peut voir à partir de cet exemple que l'utilisation d'outils généralement disponibles et en particulier le générateur de requêtes simplifie la programmation avec SQL et JDBC.

XVII-B-2. Une version GUI du programme de recherche

Il serait plus pratique de laisser tourner le programme en permanence et de basculer vers lui pour taper un nom et entamer une recherche lorsqu'on en a besoin. Le programme suivant crée le programme de recherche en tant qu'application/applet, et ajoute également une fonctionnalité de complétion des noms afin qu'on puisse voir les données sans être obligé de taper le nom complet :

 
Sélectionnez
//: c15:jdbc:VLookup.java
// version GUI de Lookup.java.
// <applet code=VLookup
// width=500 height=200></applet>
import javax.swing.*; 
import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import java.sql.*;
import com.bruceeckel.swing.*;

public class VLookup extends JApplet {
  String dbUrl = "jdbc:odbc:people";
  String user = "";
  String password = "";
  Statement s;
  JTextField searchFor = new JTextField(20);
  JLabel completion = 
    new JLabel("                        ");
  JTextArea results = new JTextArea(40, 20);
  public void init() {
    searchFor.getDocument().addDocumentListener(
      new SearchL());
    JPanel p = new JPanel();
    p.add(new Label("Last name to search for:"));
    p.add(searchFor);
    p.add(completion);
    Container cp = getContentPane();
    cp.add(p, BorderLayout.NORTH);
    cp.add(results, BorderLayout.CENTER);
    try {
      // Charger le driver (qui s'enregistrera lui-même)
      Class.forName(
        "sun.jdbc.odbc.JdbcOdbcDriver");
      Connection c = DriverManager.getConnection(
        dbUrl, user, password);
      s = c.createStatement();
    } catch(Exception e) {
      results.setText(e.toString());
    }
  }
  class SearchL implements DocumentListener {
    public void changedUpdate(DocumentEvent e){}
    public void insertUpdate(DocumentEvent e){
      textValueChanged();
    }
    public void removeUpdate(DocumentEvent e){
      textValueChanged();
    }
  }
  public void textValueChanged() {
    ResultSet r;
    if(searchFor.getText().length() == 0) {
      completion.setText("");
      results.setText("");
      return;
    }
    try {
      // compléter automatiquement le nom:
      r = s.executeQuery(
        "SELECT LAST FROM people.csv people " +
        "WHERE (LAST Like '" +
        searchFor.getText()  + 
        "%') ORDER BY LAST");
      if(r.next()) 
        completion.setText(
          r.getString("last"));
      r = s.executeQuery(
        "SELECT FIRST, LAST, EMAIL " +
        "FROM people.csv people " +
        "WHERE (LAST='" + 
        completion.getText() +
        "') AND (EMAIL Is Not Null) " +
        "ORDER BY FIRST");
    } catch(Exception e) {
      results.setText(
        searchFor.getText() + "\n");
      results.append(e.toString());
      return; 
    }
    results.setText("");
    try {
      while(r.next()) {
        results.append(
          r.getString("Last") + ", " 
          + r.getString("fIRST") + 
          ": " + r.getString("EMAIL") + "\n");
      }
    } catch(Exception e) {
      results.setText(e.toString());
    }
  }
  public static void main(String[ « args) {
    Console.run(new VLookup(), 500, 200);
  }
} ///:~

La logique « base de données » est en gros la même, mais on peut remarquer qu'un DocumentListener est ajouté pour écouter l'entrée JTextField (pour plus de détails, voir l'entrée javax.swing.JTextField dans la documentation HTML Java sur java.sun.com), afin qu'à chaque nouveau caractère frappé le programme tente de compléter le nom en le cherchant dans la base et en utilisant le premier qu'il trouve (en le plaçant dans le JLabel completion, et en l'utilisant comme un texte de recherche). De cette manière, on peut arrêter de frapper des caractères dès qu'on en a tapé suffisamment pour que le programme puisse identifier de manière unique le nom qu'on cherche.

XVII-B-3. Pourquoi l'API JDBC paraît si complexe

Naviguer dans la documentation en ligne de JDBC semble décourageant. En particulier, dans l'interface DatabaseMetaData qui est vraiment énorme, à l'inverse de la plupart des interfaces rencontrées jusque-là en Java, on trouve des méthodes telles que dataDefinitionCausesTransactionCommit( ), getMaxColumnNameLength( ), getMaxStatementLength( ), storesMixedCaseQuotedIdentifiers( ), supportsANSI92IntermediateSQL( ), supportsLimitedOuterJoins( ), et ainsi de suite. Qu'en est-il de tout cela ?

Ainsi qu'on l'a dit précédemment, les bases de données semblent être depuis leur création dans un constant état de bouleversement, principalement à cause de la très grande demande en applications de base de données, et donc en outils de base de données. Ce n'est que récemment qu'on a vu une certaine convergence vers le langage commun SQL (et il existe beaucoup d'autres langages d'utilisation courante de base de données). Mais même le « standard » SQL possède tellement de variantes que JDBC doit fournir la grande interface DatabaseMetaData afin que le code puisse utiliser les possibilités de la base SQL « standard » particulière avec laquelle on est connecté. Bref, il est possible d'écrire du code SQL simple et portable, mais si on veut optimiser la vitesse le code va se multiplier terriblement au fur et à mesure qu'on découvre les possibilités d'une base de données d'un vendeur particulier.

Bien entendu, ce n'est pas la faute de Java. Ce dernier tente seulement de nous aider à compenser les disparités entre les divers produits de base de données. Mais gardons à l'esprit que la vie est plus facile si l'on peut aussi bien écrire des requêtes génériques sans trop se soucier des performances, ou bien, si l'on veut améliorer les performances, connaître la plate-forme pour laquelle on écrit afin de ne pas avoir à traîner trop de code générique.

XVII-B-4. Un exemple plus sophistiqué

Un exemple plus intéressant (58) est celui d'une base de données multitable résidant sur un serveur. Ici, la base sert de dépôt pour les activités d'une communauté et doit permettre aux gens de s'inscrire pour réaliser ces actions, c'est pourquoi on l'appelle une base de données de communauté d'intérêts, Community Interests Database (CID). Cet exemple fournira seulement une vue générale de la base et de son implémentation, il n'est en aucun cas un tutoriel complet à propos du développement des bases de données. De nombreux livres, séminaires, et packages de programmes existent pour vous aider dans la conception et le développement des bases de données.

De plus, cet exemple implique l'installation préalable d'une base SQL sur un serveur (bien qu'elle puisse aussi bien tourner sur la machine locale), ainsi que la recherche d'un driver JDBC approprié pour cette base. Il existe plusieurs bases SQL libres, et certaines sont installées automatiquement avec diverses distributions de Linux. Il est de votre responsabilité de faire le choix de la base de données et de localiser son driver JDBC ; cet exemple-ci est basé sur une base SQL nommée « Cloudscape ».

Afin de simplifier les modifications d'information de connexion, le driver de la base, son URL, le nom d'utilisateur et son mot de passe sont placés dans une classe séparée :

 
Sélectionnez
//: c15:jdbc:CIDConnect.java
// information de connexion à la base de données pour
// la «base de données de communauté d'intérêts» (CID).

public class CIDConnect {
  // Toutes les informations spécifiques à CloudScape:
  public static String dbDriver = 
    "COM.cloudscape.core.JDBCDriver";
  public static String dbURL =    "jdbc:cloudscape:d:/docs/_work/JSapienDB";
  public static String user = "";
  public static String password = "";
} ///:~

Dans cet exemple, il n'y a pas de protection de la base par mot de passe, le nom d'utilisateur et le mot de passe sont des chaînes vides.

La base de données comprend un ensemble de tables dont voici la structure :

 
Sélectionnez
border="0" alt="Image">

« Members » contient les informations sur les membres de la communauté, « Events » et « Locations » des informations à propos des activités et où on peut les trouver, et « Evtmems » connecte les événements et les membres qui veulent suivre ces événements. On peut constater qu'à une donnée « membre » d'une table correspond une clef dans une autre table.

La classe suivante contient les chaînes SQL qui vont créer les tables de cette base (référez-vous à un guide SQL pour une explication du code SQL) :

 
Sélectionnez
//: c15:jdbc:CIDSQL.java
// chaînes SQL créant les tables pour la CID.

public class CIDSQL {
  public static String[ « sql = {
    // Créer la table MEMBERS:
    "drop table MEMBERS",
    "create table MEMBERS " +
    "(MEM_ID INTEGER primary key, " +
    "MEM_UNAME VARCHAR(12) not null unique, "+
    "MEM_LNAME VARCHAR(40), " +
    "MEM_FNAME VARCHAR(20), " +
    "ADDRESS VARCHAR(40), " +
    "CITY VARCHAR(20), " +
    "STATE CHAR(4), " +
    "ZIP CHAR(5), " +
    "PHONE CHAR(12), " +
    "EMAIL VARCHAR(30))",
    "create unique index " +
    "LNAME_IDX on MEMBERS(MEM_LNAME)",
    // Créer la table EVENTS:
    "drop table EVENTS",
    "create table EVENTS " +
    "(EVT_ID INTEGER primary key, " +
    "EVT_TITLE VARCHAR(30) not null, " +
    "EVT_TYPE VARCHAR(20), " +
    "LOC_ID INTEGER, " +
    "PRICE DECIMAL, " +
    "DATETIME TIMESTAMP)",
    "create unique index " +
    "TITLE_IDX on EVENTS(EVT_TITLE)",
    // Créer la table EVTMEMS:
    "drop table EVTMEMS",
    "create table EVTMEMS " +
    "(MEM_ID INTEGER not null, " +
    "EVT_ID INTEGER not null, " +
    "MEM_ORD INTEGER)",
    "create unique index " +
    "EVTMEM_IDX on EVTMEMS(MEM_ID, EVT_ID)",
    // Créer la table LOCATIONS:
    "drop table LOCATIONS",
    "create table LOCATIONS " +
    "(LOC_ID INTEGER primary key, " +
    "LOC_NAME VARCHAR(30) not null, " +
    "CONTACT VARCHAR(50), " +
    "ADDRESS VARCHAR(40), " +
    "CITY VARCHAR(20), " +
    "STATE VARCHAR(4), " +
    "ZIP VARCHAR(5), " +
    "PHONE CHAR(12), " +
    "DIRECTIONS VARCHAR(4096))",
    "create unique index " +
    "NAME_IDX on LOCATIONS(LOC_NAME)",
  };
} ///:~

Dans cet exemple, il est intéressant de laisser les exceptions s'afficher sur la console :

 
Sélectionnez
//: c15:jdbc:CIDCreateTables.java
// Crée les tables d'une base de données pour la
// «community interests database».
import java.sql.*;

public class CIDCreateTables {
  public static void main(String[ « args) 
  throws SQLException, ClassNotFoundException,
  IllegalAccessException {
    // Charger le driver (qui s'enregistrera lui-même)
    Class.forName(CIDConnect.dbDriver);
    Connection c = DriverManager.getConnection(
      CIDConnect.dbURL, CIDConnect.user, 
      CIDConnect.password);
    Statement s = c.createStatement();
    for(int i = 0; i < CIDSQL.sql.length; i++) {
      System.out.println(CIDSQL.sql[i]);
      try {
        s.executeUpdate(CIDSQL.sql[i]);
      } catch(SQLException sqlEx) {
        System.err.println(
          "Probably a 'drop table' failed");
      }
    }
    s.close();
    c.close();
  }
} ///:~

Remarquons que les modifications de la base peuvent être contrôlées en changeant Strings dans la table CIDSQL, sans modifier CIDCreateTables.

La méthode executeUpdate( ) renvoie généralement le nombre d'enregistrements affectés par l'instruction SQL. Elle est très souvent utilisée pour exécuter des instructions INSERT, UPDATE, ou DELETE modifiant une ou plusieurs lignes. Pour les instructions telles que CREATE TABLE, DROP TABLE, et CREATE INDEX, executeUpdate( ) renvoie toujours zéro.

Pour tester la base, celle-ci est chargée avec quelques données exemples. Ceci est réalisé au moyen d'une série d'INSERT face="Georgia" suivie d'un SELECT afin de produire le jeu de données. Pour effectuer facilement des additions et des modifications aux données de test, ce dernier est construit comme un tableau d'Object à deux dimensions, et la méthode executeInsert( ) peut alors utiliser l'information d'une ligne de la table pour construire la commande SQL appropriée.

 
Sélectionnez
//: c15:jdbc:LoadDB.java
// Charge et teste la base de données.
import java.sql.*;

class TestSet {
  Object[][ « data = {
    { "MEMBERS", new Integer(1),
      "dbartlett", "Bartlett", "David",
      "123 Mockingbird Lane",
      "Gettysburg", "PA", "19312",
      "123.456.7890",  "bart@you.net" },
    { "MEMBERS", new Integer(2),
      "beckel", "Eckel", "Bruce",
      "123 Over Rainbow Lane",
      "Crested Butte", "CO", "81224",
      "123.456.7890", "beckel@you.net" },
    { "MEMBERS", new Integer(3),
      "rcastaneda", "Castaneda", "Robert",
      "123 Downunder Lane",
      "Sydney", "NSW", "12345",
      "123.456.7890", "rcastaneda@you.net" },
    { "LOCATIONS", new Integer(1),
      "Center for Arts",
      "Betty Wright", "123 Elk Ave.",
      "Crested Butte", "CO", "81224",
      "123.456.7890",
      "Go this way then that." },
    { "LOCATIONS", new Integer(2),
      "Witts End Conference Center",
      "John Wittig", "123 Music Drive",
      "Zoneville", "PA", "19123",
      "123.456.7890",
      "Go that way then this." },
    { "EVENTS", new Integer(1),
      "Project Management Myths",
      "Software Development",
      new Integer(1), new Float(2.50),
      "2000-07-17 19:30:00" },
    { "EVENTS", new Integer(2),
      "Life of the Crested Dog",
      "Archeology",
      new Integer(2), new Float(0.00),
      "2000-07-19 19:00:00" },
    // Met en relation personnes et événements
    {  "EVTMEMS", 
      new Integer(1),  // Dave est mis en relation avec
      new Integer(1),  // l'événement Software.
      new Integer(0) },
    { "EVTMEMS", 
      new Integer(2),  // Bruce est mis en relation avec
      new Integer(2),  // l'événement Archeology.
      new Integer(0) },
    { "EVTMEMS", 
      new Integer(3),  // Robert est mis en relation avec
      new Integer(1),  // l'événement Software...
      new Integer(1) },
    { "EVTMEMS", 
      new Integer(3), // ... et 
      new Integer(2), // l'événement Archeology.
      new Integer(1) },
  };
  // Utiliser les données par défaut:
  public TestSet() {}
  // Utiliser un autre ensemble de données:
  public TestSet(Object[][ « dat) { data = dat; }
}

public class LoadDB {
  Statement statement;
  Connection connection;
  TestSet tset;
  public LoadDB(TestSet t) throws SQLException {
    tset = t;
    try {
      // Charger le driver (qui s'enregistrera lui-même)
      Class.forName(CIDConnect.dbDriver);
    } catch(java.lang.ClassNotFoundException e) {
      e.printStackTrace(System.err);
    }
    connection = DriverManager.getConnection(
      CIDConnect.dbURL, CIDConnect.user, 
      CIDConnect.password);
    statement = connection.createStatement();
  }
  public void cleanup() throws SQLException {
    statement.close();
    connection.close();
  }
  public void executeInsert(Object[ « data) {
    String sql = "insert into " 
      + data[0 « + " values(";
    for(int i = 1; i < data.length; i++) {
      if(data[i « instanceof String)
        sql += "'" + data[i « + "'";
      else
        sql += data[i];
      if(i < data.length - 1)
        sql += ", ";
    }
    sql += ')';
    System.out.println(sql);
    try {
      statement.executeUpdate(sql);
    } catch(SQLException sqlEx) {
      System.err.println("Insert failed.");
      while (sqlEx != null) {
        System.err.println(sqlEx.toString());
        sqlEx = sqlEx.getNextException();
      }
    } 
  }
  public void load() {
    for(int i = 0; i< tset.data.length; i++)
      executeInsert(tset.data[i]);
  }
  // Lever l'exception en l'envoyant vers la console:
  public static void main(String[ « args) 
  throws SQLException {
    LoadDB db = new LoadDB(new TestSet());
    db.load();
    try {
      // Obtenir un ResultSet de la base chargée:
      ResultSet rs = db.statement.executeQuery(
        "select " +
        "e.EVT_TITLE, m.MEM_LNAME, m.MEM_FNAME "+
        "from EVENTS e, MEMBERS m, EVTMEMS em " +
        "where em.EVT_ID = 2 " +
        "and e.EVT_ID = em.EVT_ID " +
        "and m.MEM_ID = em.MEM_ID");
      while (rs.next())
        System.out.println(
          rs.getString(1) + "  " + 
          rs.getString(2) + ", " +
          rs.getString(3));
    } finally {
      db.cleanup();
    }
  }
} ///:~

La classe TestSet contient un ensemble de données par défaut qui est mis en œuvre lorsqu'on appelle le constructeur par défaut ; toutefois, il est possible de créer au moyen du deuxième constructeur un objet TestSet utilisant un deuxième ensemble de données. L'ensemble de données est contenu dans un tableau à deux dimensions de type Object, car il peut contenir n'importe quel type, y compris String ou des types numériques. La méthode executeInsert( ) utilise RTTI pour différencier les données String (qui doivent être entre guillemets) et les données non String en construisant la commande SQL à partir des données. Après avoir affiché cette commande sur la console, executeUpdate( ) l'envoie à la base de données.

Le constructeur de LoadDB établit la connexion, et load( ) parcourt les données en appelant executeInsert( ) pour chaque enregistrement. Cleanup( ) termine l'instruction et la connexion ; tout ceci est placé dans une clause finally afin d'en garantir l'appel.

Une fois la base chargée, une instruction executeQuery( ) produit un ensemble résultat. La requête concernant plusieurs tables, nous avons bien un exemple de base de données relationnelle.

On trouvera d'autres informations sur JDBC dans les documents électroniques livrés avec la distribution Java de Sun. Pour en savoir plus, consulter le livre JDBC Database Access with Java (Hamilton, Cattel, and Fisher, Addison-Wesley, 1997). D'autres livres à propos de JDBC sortent régulièrement.

XVII-C. Les servlets

Les accès clients sur l'Internet ou les intranets d'entreprise représentent un moyen sûr de permettre à beaucoup d'utilisateurs d'accéder facilement aux données et ressources (59). Ce type d'accès est basé sur des clients utilisant les standards du World Wide Web Hypertext Markup Language (HTML) et Hypertext Transfer Protocol (HTTP). L'API Servlet fournit une abstraction pour un ensemble de solutions communes en réponse aux requêtes HTTP.

Traditionnellement, la solution permettant à un client Internet de mettre à jour une base de données est de créer une page HTML contenant des champs texte et un bouton « soumission ». L'utilisateur frappe l'information requise dans les champs texte puis clique sur le bouton « soumission ». Les données sont alors soumises au moyen d'une URL qui indique au serveur ce qu'il doit en faire en lui indiquant l'emplacement d'un programme Common Gateway Interface (CGI) lancé par le serveur, qui prend ces données en argument. Le programme CGI est généralement écrit en Perl, Python, C, C++, ou n'importe quel langage capable de lire sur l'entrée standard et d'écrire sur la sortie standard. Le rôle du serveur Web s'arrête là : le programme CGI est appelé, et des flux standard (ou, optionnellement pour l'entrée, une variable d'environnement) sont utilisés pour l'entrée et la sortie. Le programme CGI est responsable de toute la suite. Il commence par examiner les données et voir si leur format est correct. Si ce n'est pas le cas, le programme CGI doit fournir une page HTML décrivant le problème ; cette page est prise en compte par le serveur Web (via la sortie standard du programme CGI), qui la renvoie à l'utilisateur. Habituellement, l'utilisateur revient à la page précédente et fait une nouvelle tentative. Si les données sont correctes, le programme CGI traite les données de la manière appropriée, par exemple en les ajoutant à une base de données. Il élabore ensuite une page HTML appropriée que le serveur Web enverra à l'utilisateur.

Afin d'avoir une solution basée entièrement sur Java, l'idéal serait d'avoir côté client une applet qui validerait et enverrait les données, et côté serveur une servlet qui les recevrait et les traiterait. Malheureusement, bien que les applets forment une technologie éprouvée et bien supportée, leur utilisation sur le Web s'est révélée problématique, car on ne peut être certain de la disponibilité d'une version particulière de Java sur le navigateur Web du client ; en fait, on ne peut même pas être certain que le navigateur Web supporte Java ! Dans un intranet, on peut exiger qu'un support donné soit disponible, ce qui apporte une certaine flexibilité à ce qu'on peut faire, mais sur le Web l'approche la plus sûre est d'effectuer tout le traitement du côté serveur puis de délivrer une page HTML au client. De cette manière, aucun client ne se verra refuser l'utilisation de votre site simplement parce qu'il ne dispose pas dans sa configuration du software approprié.

Parce que les servlets fournissent une excellente solution pour le support de programmation côté serveur, ils représentent l'une des raisons les plus populaires pour passer à Java. Non seulement ils fournissent un cadre pour remplacer la programmation CGI (et éliminer nombre de problèmes CGI épineux), mais tout le code gagne en portabilité inter plate-forme en utilisant Java, et l'on a accès à toutes les API Java (excepté, bien entendu, celles qui fournissent des GUI, comme Swing).

XVII-C-1. Le servlet de base

L'architecture de l'API servlet est celle d'un fournisseur de services classique comportant une méthode service( ) appartenant au software conteneur de la servlet, chargée de recevoir toutes les requêtes client, et les méthodes liées au cycle de vie, init( ) et destroy( ), qui sont appelées seulement lorsque la servlet est chargée ou déchargée (ce qui arrive rarement).

 
Sélectionnez
public interface Servlet {
  public void init(ServletConfig config)
    throws ServletException;
  public ServletConfig getServletConfig();
  public void service(ServletRequest req,
    ServletResponse res) 
    throws ServletException, IOException;
  public String getServletInfo();
  public void destroy();
}

La raison d'être de getServletConfig( ) est de renvoyer un objet ServletConfig contenant l'initialisation et les paramètres de départ de cette servlet. La méthode getServletInfo( ) renvoie une chaîne contenant des informations à propos de la servlet, telles que le nom de l'auteur, la version, et le copyright.

La classe GenericServlet est une implémentation de cette interface et n'est généralement pas utilisée. La classe HttpServlet est une extension de GenericServlet, elle est explicitement conçue pour traiter le protocole HTTP. C'est cette classe, HttpServlet, que vous utiliserez la plupart du temps.

Les attributs les plus commodes de l'API servlet sont les objets auxiliaires fournis par la classe HttpServlet. En regardant la méthode service( ) de l'interface Servlet, on constate qu'elle a deux paramètres : ServletRequest et ServletResponse. Dans la classe HttpServlet, deux objets sont développés pour HTTP : HttpServletRequest and HttpServletResponse. Voici un exemple simple montrant l'utilisation de HttpServletResponse  :

 
Sélectionnez
//: c15:servlets:ServletsRule.java
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class ServletsRule extends HttpServlet {
  int i = 0; // «persistance» de Servlet
  public void service(HttpServletRequest req, 
  HttpServletResponse res) throws IOException {
    res.setContentType("text/html");
    PrintWriter out = res.getWriter();
    out.print("<HEAD><TITLE>");
    out.print("A server-side strategy");
    out.print("</TITLE></HEAD><BODY>");
    out.print("<h1>Servlets Rule! " + i++);
    out.print("</h1></BODY>");  
    out.close();    
  }
} ///:~

La classe ServletsRule est la chose la plus simple que peut recevoir une servlet. La servlet est initialisée au démarrage en appelant sa méthode init( ), en chargeant la servlet après que le conteneur de la servlet soit chargé. Lorsqu'un client envoie une requête à une URL qui semble reliée à une servlet, le conteneur de servlet intercepte cette demande et effectue un appel de la méthode service( ), après avoir créé les objets HttpServletRequest et HttpServletResponse.

La principale responsabilité de la méthode service( ) est d'interagir avec la requête HTTP envoyée par le client, et de construire une réponse HTTP basée sur les attributs contenus dans la demande. La méthode ServletsRule se contente de manipuler l'objet réponse sans chercher à savoir ce que voulait le client.

Après avoir mis en place le type du contenu de la réponse (ce qui doit toujours être fait avant d'initialiser Writer ou OutputStream), la méthode getWriter( ) de l'objet réponse renvoie un objet PrintWriter, utilisé pour écrire les données en retour sous forme de caractères (de manière similaire, getOutputStream( ) fournit un OutputStream, utilisé pour les réponses binaires, uniquement dans des solutions plus spécifiques).

Le reste du programme se contente d'envoyer une page HTML au client (on suppose que le lecteur comprend le langage HTML, qui n'est pas décrit ici) sous la forme d'une séquence de Strings. Toutefois, il faut remarquer l'inclusion du « compteur de passages » représenté par la variable i. Il est automatiquement converti en String dans l'instruction print( ).

En lançant le programme, on peut remarquer que la valeur de i ne change pas entre les requêtes vers la servlet. C'est une propriété essentielle des servlets : tant qu'il n'existe qu'une servlet d'une classe particulière chargée dans le conteneur, et jamais déchargée (sauf en cas de fin du conteneur de servlet, ce qui ne se produit normalement que si l'on reboote l'ordinateur serveur), tous les champs de cette classe servlet sont des objets persistants ! Cela signifie que vous pouvez sans effort supplémentaire garder des valeurs entre les requêtes à la servlet, alors qu'avec CGI vous auriez dû écrire ces valeurs sur disque afin de les préserver, ce qui aurait demandé du temps supplémentaire et fini par déboucher sur une solution qui n'aurait pas été interplate-forme.

Bien entendu, le serveur Web ainsi que le conteneur de servlet doivent de temps en temps être rebootés pour des raisons de maintenance ou après une coupure de courant. Pour éviter de perdre toute information persistante, les méthodes de servlet init( ) et destroy( ) sont appelées automatiquement chaque fois que la servlet est chargée ou déchargée, ce qui nous donne l'opportunité de sauver des données lors d'un arrêt, puis de les restaurer après que la machine a été rebootée. Le conteneur de la servlet appelle la méthode destroy( ) lorsqu'il se termine lui-même, et on a donc toujours une opportunité de sauver des données essentielles pour peu que la machine serveur soit intelligemment configurée.

Quand un formulaire est soumis à un servlet, HttpServletRequest est préchargée avec les données du formulaire présentées sous la forme de paires clef/valeur. Si on connaît le nom des champs, il suffit d'y accéder directement avec la méthode getParameter( ) pour connaître leur valeur. Il est également possible d'obtenir un objet Enumeration (l'ancienne forme d'un Iterator) vers les noms des champs, ainsi que le montre l'exemple qui suit. Cet exemple montre aussi comment un seul servlet peut être utilisé pour produire à la fois la page contenant le formulaire et la réponse à cette page (on verra plus tard une meilleure solution utilisant les JSP). Si Enumeration est vide, c'est qu'il n'y a plus de champ ; cela signifie qu'aucun formulaire n'a été soumis. Dans ce cas, le formulaire est élaboré, et le bouton de soumission rappellera la même servlet. Toutefois les champs sont affichés lorsqu'ils existent.

 
Sélectionnez
//: c15:servlets:EchoForm.java
// Affiche les couples nom-valeur d'un formulaire HTML
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;

public class EchoForm extends HttpServlet {
  public void service(HttpServletRequest req, 
    HttpServletResponse res) throws IOException {
    res.setContentType("text/html");
    PrintWriter out = res.getWriter();
    Enumeration flds = req.getParameterNames();
    if(!flds.hasMoreElements()) {
      // Pas de formulaire soumis -- on en crée un:
      out.print("<html>");
      out.print("<form method=\"POST\"" + 
        " action=\"EchoForm\">");
      for(int i = 0; i < 10; i++)
        out.print("<b>Field" + i + "</b> " +
          "<input type=\"text\""+
          " size=\"20\" name=\"Field" + i + 
          "\" value=\"Value" + i + "\"><br>");
      out.print("<INPUT TYPE=submit name=submit"+
      " Value=\"Submit\"></form></html>");
    } else {
      out.print("<h1>Your form contained:</h1>");
      while(flds.hasMoreElements()) {
        String field= (String)flds.nextElement();
        String value= req.getParameter(field);
        out.print(field + " = " + value+ "<br>");
      }
    }
    out.close();    
  }
} ///:~

On peut penser en lisant cela que Java ne semble pas conçu pour traiter des chaînes de caractères, car le formatage des pages à renvoyer est pénible à cause des retours à la ligne, des séquences escape, et du signe + inévitable dans la construction des objets String. Il n'est pas raisonnable de coder une page HTML quelque peu volumineuse en Java. Une des solutions est de préparer la page en tant que fichier texte séparé, puis de l'ouvrir et de la passer au serveur Web. S'il fallait de plus effectuer des substitutions de chaînes dans le contenu de la page, ce n'est guère mieux, car le traitement des chaînes en Java est très pauvre. Si vous rencontrez un de ces cas, il serait préférable d'adopter une solution mieux appropriée (mon choix irait vers Python ; voici une version incluse dans un programme Java appelé JPython) qui génère une page-réponse.

XVII-C-2. Les servlets et le multithreading

Le conteneur de servlet dispose d'un ensemble de threads qu'il peut lancer pour traiter les demandes des clients. On peut imaginer cela comme si deux clients arrivant au même moment étaient traités simultanément par la méthode service( ). En conséquence la méthode service( ) doit être écrite d'une manière sécurisée dans un contexte de thread. Tous les accès aux ressources communes (fichiers, bases de données) demandent à être protégés par le mot-clef synchronized.

L'exemple très simple qui suit utilise une clause synchronized autour de la méthode sleep( ) du thread. En conséquence les autres threads seront bloqués jusqu'à ce que le temps imparti (cinq secondes) soit écoulé. Pour tester cela, il faut lancer plusieurs instances d'un navigateur puis lancer ce servlet aussi vite que possible ; remarquez alors que chacun d'eux doit attendre avant de voir le jour.

 
Sélectionnez
//: c15:servlets:ThreadServlet.java
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class ThreadServlet extends HttpServlet {
  int i;
  public void service(HttpServletRequest req, 
    HttpServletResponse res) throws IOException {
    res.setContentType("text/html");
    PrintWriter out = res.getWriter();
    synchronized(this) {
      try {
        Thread.currentThread().sleep(5000);
      } catch(InterruptedException e) {
        System.err.println("Interrupted");
      }
    }
    out.print("<h1>Finished " + i++ + "</h1>");
    out.close();    
  }
} ///:~

On peut aussi synchroniser complètement la servlet en mettant le mot-clef synchronized juste avant la méthode service( ). En réalité, l'unique justification pour utiliser la clause synchronized à la place de cela est lorsque la section critique se trouve dans un chemin d'exécution qui ne doit pas être exécuté. Dans un tel cas, il serait préférable d'éviter la contrainte de synchronisation à chaque fois en utilisant une clause synchronized. Sinon, chaque thread particulier devrait systématiquement attendre, il vaut donc mieux synchroniser la méthode en entier.

XVII-C-3. Gérer des sessions avec les servlets

HTTP est un protocole qui ne possède pas la notion de session, on ne peut donc savoir d'un appel serveur à un autre s'il s'agit du même appelant ou s'il s'agit d'une personne complètement différente. Beaucoup d'efforts ont été faits pour créer des mécanismes permettant aux développeurs Web de suivre les sessions. À titre d'exemple, les compagnies ne pourraient pas faire d' e-commerce si elles ne gardaient pas la trace d'un client, ainsi que les renseignements qu'il a saisis sur sa liste de courses.

Il existe plusieurs méthodes pour suivre une session, mais la plus commune utilise les « cookies persistants », qui font intégralement partie du standard Internet. Le HTTP Working Group de l'Internet Engineering Task Force a décrit les cookies du standard officiel dans RFC 2109 (ds.internic.net/rfc/rfc2109.txt ou voir www.cookiecentral.com).

Un cookie n'est pas autre chose qu'une information de petite taille envoyée par un serveur Web à un navigateur. Le navigateur sauvegarde ce cookie sur le disque local, puis lors de chaque appel à l'URL associée au cookie, ce dernier est envoyé de manière transparente en même temps que l'appel, fournissant ainsi au serveur l'information désirée en retour (en lui fournissant généralement d'une certaine manière votre identité). Toutefois les clients peuvent inhiber la capacité de leur navigateur à accepter les cookies. Si votre site doit suivre un client qui a inhibé cette possibilité, alors une autre méthode de suivi de session doit être intégrée à la main (réécriture d'URL ou champs cachés dans un formulaire), car les fonctionnalités de suivi de session intégrées à l'API servlet sont construites autour des cookies.

XVII-C-3-a. La classe Cookie

L'API servlet (à partir de la version 2.0) fournit la classe Cookie. Cette classe inclut tous les détails de l'en-tête HTTP et permet de définir différents attributs de cookie. L'utilisation d'un cookie consiste simplement à l'ajouter à l'objet réponse. Le constructeur a deux arguments, le premier est un nom du cookie et le deuxième une valeur. Les cookies sont ajoutés à l'objet réponse avant que l'envoi ne soit effectif.

 
Sélectionnez
Cookie oreo = new Cookie("TIJava", "2000");
res.addCookie(cookie);

Les cookies sont récupérés en appelant la méthode getCookies( ) de l'objet HttpServletRequest, qui renvoie un tableau d'objets Cookie.

 
Sélectionnez
Cookie[ « cookies = req.getCookies();

En appelant getValue( ) pour chaque cookie, on obtient une String initialisée avec le contenu du cookie. Dans l'exemple ci-dessus, getValue("TIJava") renverrait une String contenant « 2000 ».

XVII-C-3-b. La classe Session

Une session consiste en une ou plusieurs requêtes de pages adressées par un client à un site Web durant une période définie. Par exemple, si vous faites vos courses en ligne, la session sera la période démarrant au moment où vous ajoutez un achat dans votre panier jusqu'au moment où vous envoyez effectivement la demande. Chaque achat ajouté au panier déclenchera une nouvelle connexion HTTP, qui n'a aucun rapport ni avec les connexions précédentes ni avec les achats déjà inclus dans votre panier. Pour compenser ce manque d'information, les mécanismes fournis par la spécification des cookies permettent au servlet de suivre la session.

Un objet servlet Session réside du côté serveur sur le canal de communication ; son rôle est de capturer les données utiles à propos du client pendant qu'il navigue sur votre site Web et qu'il interagit avec lui. Ces données peuvent être pertinentes pour la session actuelle, comme les achats dans le panier, ou bien peuvent être des informations d'authentification fournies lors de l'accès du client au site Web, et qu'il n'y a pas lieu de donner à nouveau durant un ensemble particulier de transactions.

La classe Session de l'API servlet utilise la classe Cookie pour effectuer ce travail. Toutefois, l'objet Session n'a besoin que d'une sorte d'identifiant unique stocké chez le client et passé au serveur. Les sites Web peuvent aussi utiliser les autres systèmes de suivi de session, mais ces mécanismes sont plus difficiles à mettre en œuvre, car ils n'existent pas dans l'API servlet (ce qui signifie qu'on doit les écrire à la main pour traiter le cas où le client n'accepte pas les cookies).

Voici un exemple implémentant le suivi de session au moyen de l'API servlet :

 
Sélectionnez
//: c15:servlets:SessionPeek.java
// Utilise la classe HttpSession.
import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class SessionPeek extends HttpServlet { 
  public void service(HttpServletRequest req, 
  HttpServletResponse res)
  throws ServletException, IOException {
    // Obtenir l'Objet Session avant tout
    // envoi vers le client.
    HttpSession session = req.getSession();
    res.setContentType("text/html");
    PrintWriter out = res.getWriter();
    out.println("<HEAD><TITLE> SessionPeek ");
    out.println(" </TITLE></HEAD><BODY>");
    out.println("<h1> SessionPeek </h1>");
    // Un simple compteur pour cette session.
    Integer ival = (Integer) 
      session.getAttribute("sesspeek.cntr");
    if(ival==null) 
      ival = new Integer(1);
    else 
      ival = new Integer(ival.intValue() + 1);
    session.setAttribute("sesspeek.cntr", ival);
    out.println("You have hit this page <b>"
      + ival + "</b> times.<p>");
    out.println("<h2>");
    out.println("Saved Session Data </h2>");
    // Boucler au travers de toutes les données de la session:
    Enumeration sesNames = 
      session.getAttributeNames();
    while(sesNames.hasMoreElements()) {
      String name = 
        sesNames.nextElement().toString();
      Object value = session.getAttribute(name);
      out.println(name + " = " + value + "<br>");
    }
    out.println("<h3> Session Statistics </h3>");
    out.println("Session ID: " 
      + session.getId() + "<br>");
    out.println("New Session: " + session.isNew()
      + "<br>");
    out.println("Creation Time: "
      + session.getCreationTime());
    out.println("<I>(" + 
      new Date(session.getCreationTime())
      + ")</I><br>");
    out.println("Last Accessed Time: " +
      session.getLastAccessedTime());
    out.println("<I>(" +
      new Date(session.getLastAccessedTime())
      + ")</I><br>");
    out.println("Session Inactive Interval: "
      + session.getMaxInactiveInterval());
    out.println("Session ID in Request: "
      + req.getRequestedSessionId() + "<br>");
    out.println("Is session id from Cookie: "
      + req.isRequestedSessionIdFromCookie()
      + "<br>");
    out.println("Is session id from URL: "
      + req.isRequestedSessionIdFromURL()
      + "<br>");
    out.println("Is session id valid: "
      + req.isRequestedSessionIdValid()
      + "<br>");
    out.println("</BODY>");
    out.close();
  }
  public String getServletInfo() {
    return "A session tracking servlet";
  }
} ///:~

À l'intérieur de la méthode service( ), la méthode getSession( ) est appelée pour l'objet requête et renvoie un objet Session associé à la requête. L'objet Session ne voyage pas sur le réseau, il réside sur le serveur et est associé à un client et à ses requêtes.

La méthode getSession( ) possède deux versions : sans paramètres, ainsi qu'elle est utilisée ici, et getSession(boolean). L'appel de getSession(true) est équivalent à getSession( ). Le boolean sert à indiquer si on désire créer l'objet session lorsqu'on ne le trouve pas. L'appel le plus probable est getSession(true), d'où la forme getSession( ).

L'objet Session, s'il n'est pas nouveau, nous donne des informations sur le client à partir de visites antérieures. Si l'objet Session est nouveau alors le programme commencera à recueillir des informations à propos des activités du client lors de cette visite. Le recueil de cette information est effectué au moyen des méthodes setAttribute( ) et getAttribute( ) de l'objet session.

 
Sélectionnez
java.lang.Object getAttribute(java.lang.String)
void setAttribute(java.lang.String name,
                  java.lang.Object value)

L'objet Session utilise une simple paire nom/valeur pour garder l'information. Le nom est du type String, et la valeur peut être n'importe quel objet dérivé de java.lang.Object. SessionPeek garde la trace du nombre de fois où le client est revenu pendant cette session, au moyen d'un objet Integer nommé sesspeek.cntr. Si le nom n'existe pas on crée un Integer avec une valeur de un, sinon on crée un Integer en incrémentant la valeur du précédent. Le nouvel Integer est rangé dans l'objet Session. Si on utilise la même clef dans un appel à setAttribute( ), le nouvel objet écrase l'ancien. Le compteur incrémenté sert à afficher le nombre de visites du client pendant cette session.

La méthode getAttributeNames( ) est en relation avec getAttribute( ) et setAttribute( ) et renvoie une énumération des noms des objets associés à l'objet Session. Une boucle while de SessionPeek montre cette méthode en action.

Vous vous interrogez sans doute sur la durée de vie d'un objet Session. La réponse dépend du conteneur de servlet qu'on utilise ; généralement la durée de vie par défaut est de 30 minutes (1800 secondes), ce que l'on peut voir au travers de l'appel de getMaxInactiveInterval( ) par ServletPeek. Les tests semblent montrer des résultats différents suivant le conteneur de servlet utilisé. De temps en temps l'objet Session peut faire le tour du cadran, mais je n'ai jamais rencontré de cas où l'objet Session disparaît avant que le temps spécifié par « inactive interval » soit écoulé. On peut tester cela en initialisant « inactive interval » à 5 secondes au moyen de setMaxInactiveInterval( ) puis voir si l'objet Session est toujours là ou au contraire a été détruit à l'heure déterminée. Il se pourrait que vous ayez à étudier cet attribut lorsque vous choisirez un conteneur de servlet.

XVII-C-4. Faire fonctionner les exemples de servlet

Si vous ne travaillez pas encore sur un serveur d'applications gérant les servlets Sun ainsi que les technologies JSP, il vous faudra télécharger l'implémentation Tomcat des servlets Java et des JSP, qui est une implémentation libre et « open-source » des servlets, et de plus l'implémentation officielle de référence de Sun. Elle se trouve à jakarta.apache.org.

Suivez les instructions d'installation de l'implementation Tomcat, puis éditez le fichier server.xml pour décrire l'emplacement de votre répertoire qui contiendra vos servlets. Une fois lancé le programme Tomcat vous pouvez tester vos programmes servlet.

Ceci n'était qu'une brève introduction aux servlets ; il existe des livres entiers traitant de ce sujet. Toutefois, cette introduction devrait vous donner suffisamment d'idées pour démarrer. De plus, beaucoup de thèmes développés dans la section suivante ont une compatibilité ascendante avec les servlets.

XVII-D. Les Pages Java Serveur - Java Server Pages

Les Java Server Pages (JSP) sont une extension standard Java définie au-dessus des extensions servlet. Le propos des JSP est de simplifier la création et la gestion des pages Web dynamiques.

L'implémentation de référence Tomcat, déjà mentionnée et disponible librement sur jakarta.apache.org « font-style: normal », supporte automatiquement les JSP.

Les JSP permettent de mélanger le code HTML d'une page Web et du code Java dans le même document. Le code Java est entouré de tags spéciaux qui indiquent au conteneur JSP qu'il doit utiliser le code pour générer une servlet complètement ou en partie. L'avantage que procurent les JSP est de maintenir un seul document qui est à la fois la page HTML et le code Java qui la gère. Le revers est que celui qui maintient la page JSP doit être autant qualifié en HTML qu'en Java (toutefois, des environnements GUI de construction de JSP devraient apparaître sur le marché).

À partir de là, et tant que le code source JSP de la page n'est pas modifié, tout se passe comme si on avait une page HTML statique associée à des servlets (en réalité, le code HTML est généré par la servlet). Si on modifie le code source de la JSP, il est automatiquement recompilé et rechargé dès que cette page sera redemandée. Bien entendu, à cause de ce dynamisme le temps de réponse sera long lors du premier accès à une JSP. Toutefois, étant donné qu'une JSP est généralement plus utilisée qu'elle n'est modifiée, on ne sera pas généralement affecté par ce délai.

La structure d'une page JSP est à mi-chemin d'une servlet et d'une page HTML. Les tags JSP commencent et finissent comme les tags HTML, sauf qu'ils utilisent également le caractère pour cent (%), ainsi tous les tags JSP ont cette structure :

 
Sélectionnez
<% ici, le code JSP %>

Le premier caractère pour cent doit être suivi d'un autre caractère qui définit précisément le type du code JSP du tag.

Voici un exemple extrêmement simple de JSP utilisant un appel standard à une bibliothèque Java pour récupérer l'heure courante en millisecondes, et diviser le résultat par 1000 pour produire l'heure en secondes. Une expression JSP (<%= ) est utilisée, puis le résultat du calcul est mis dans une String et intégré à la page Web générée :

 
Sélectionnez
//:! c15:jsp:ShowSeconds.jsp
<html><body>
<H1>The time in seconds is: 
<%= System.currentTimeMillis()/1000 %></H1>
</body></html>
///:~

Dans les exemples JSP de ce livre, la première et la dernière ligne ne font pas partie du fichier code réel qui est extrait et placé dans l'arborescence du code source de ce livre.

Lorsque le client demande une page JSP, le serveur Web doit avoir été configuré pour relayer la demande vers le conteneur JSP, qui à son tour appelle la page. Comme on l'a déjà dit plus haut, lors du premier appel de la page, les composants spécifiés par la page sont générés et compilés par le conteneur JSP en tant qu'une ou plusieurs servlets. Dans les exemples ci-dessus, la servlet doit contenir le code destiné à configurer l'objet HttpServletResponse, produire un objet PrintWriter (toujours nommé out), et enfin transformer le résultat du calcul en un objet String qui sera envoyé vers out. Ainsi qu'on peut le voir, tout ceci est réalisé au travers d'une instruction très succincte, mais en moyenne les programmeurs HTML/concepteurs de site Web ne seront pas qualifiés pour écrire un tel code.

XVII-D-1. Les objets implicites

Les servlets comportent des classes fournissant des utilitaires pratiques, comme HttpServletRequest, HttpServletResponse, Session, etc. Les objets de ces classes font partie de la spécification JSP et sont automatiquement disponibles pour vos JSP sans avoir à écrire une ligne de code supplémentaire. Les objets implicites d'une JSP sont décrits dans le tableau ci-dessous.

Variable implicite "text-decoration: none">Du Type (javax.servlet) Description Visibilité
demande (request) Sous-type de HttpServletRequest dépendant du protocole La demande qui déclenche l'appel du service. demande
réponse Sous-type de HttpServletResponse dépendant du protocole La réponse à la demande. page
pageContext jsp.PageContext Le contexte de page encapsule les choses qui dépendent de l'implémentation et fournissent des méthodes commodes ainsi que l'accès à un espace de nommage pour ce JSP. page
session Sous-type de http.HttpSession dépendant du protocole L'objet session créé pour le client demandeur. Voir l'objet Session pour les servlets. session
application ServletContext Le contexte de servlet obtenu depuis l'objet configuration de servlet (e.g., getServletConfig(), getContext( ). app
out jsp.JspWriter L'objet qui écrit dans le flux sortant. page
config ServletConfig Le ServletConfig pour ce JSP. page
page java.lang.Object L'instance de cette classe d'implémentation de page.s gérant la demande courante. page

La visibilité de chaque objet est extrêmement variable. Par exemple, l'objet session a une visibilité qui dépasse la page, car il englobe plusieurs demandes client et plusieurs pages. L'objet application peut fournir des services à un groupe de pages JSP représentant une application Web.

XVII-D-2. Les directives JSP

Les directives sont des messages adressés au conteneur de JSP et sont reconnaissables au caractère « @ »

 
Sélectionnez
<%@ directive {attr="value"}* %>

Les directives n'envoient rien sur le flux out, mais sont importantes lorsqu'on définit les attributs et les dépendances de pages avec le conteneur JSP. Par exemple, la ligne :

 
Sélectionnez
<%@ page language="java" %>

exprime le fait que le langage de scripting utilisé dans la page JSP est Java. En fait, la spécification JSP décrit seulement « font-style: normal » la sémantique des scripts pour un attribut de langage égal à « Java ». La raison d'être de cette directive est d'introduire la flexibilité dans la technologie JSP. Dans le futur, si vous aviez à choisir un autre langage, par exemple Python (un bon choix de langage de sripting), alors ce langage devra supporter le Java Run-time Environment en exposant la technologie du modèle objet Java à l'environnement de scripting, en particulier les variables implicites définies plus haut, les propriétés JavaBeans, et les méthodes publiques.

La directive la plus importante est la directive de page. Elle définit un certain nombre d'attributs dépendant de la page et les communique au conteneur JSP. Parmi ces attributs : language, extends, import, session, buffer, autoFlush, isThreadSafe, info et errorPage. Par exemple :

 
Sélectionnez
<%@ page session=]true « import=]java.util.* « %>

Cette ligne indique tout d'abord que la page nécessite une participation à une session HTTP. Puisque nous n'avons pas décrit de directive de langage le conteneur JSP utilisera Java par défaut et la variable de langage de scripting implicite appelée session sera du type javax.servlet.http.HttpSession. Si la directive avait été false alors la variable implicite session n'aurait pas été disponible. Si la variable session n'est pas spécifiée, sa valeur par défaut est « true ».

L'attribut import décrit les types disponibles pour l'environnement de scripting. Cet attribut est utilisé tout comme il le serait dans le langage de programmation Java, c'est-à-dire une liste d'expressions import ordinaires séparées par des virgules. Cette liste est importée par l'implémentation de la page JSP traduite et reste disponible pour l'environnement de scripting. À nouveau, ceci est actuellement défini uniquement lorsque la valeur de la directive de langage est « java ».

XVII-D-3. Les éléments de scripting JSP

Une fois l'environnement de scripting mis en place au moyen des directives on peut utiliser les éléments du langage de scripting. JSP 1.1 possède trois éléments de langage de scripting declaration, scriptlet, et expression. Une déclaration déclare des éléments, un scriptlet est un fragment d'instruction, et une expression est une expression complète du langage. En JSP chaque élément de scripting commence par « <% ». Voici la syntaxe de chacun :

 
Sélectionnez
<%! declaration %>
<%  scriptlet   %>
<%= expression  %>
 
Sélectionnez
L'espace est facultatif après » <%!], » <%], » <%=], et avant [%>.]

Tous ces tags s'appuient sur la norme XML ; on pourrait dire qu'une page JSP est un document XML. La syntaxe équivalente en XML pour les éléments de scripting ci-dessus serait :

 
Sélectionnez
<jsp:declaration> declaration </jsp:declaration>
<jsp:scriptlet>   scriptlet   </jsp:scriptlet>
<jsp:expression>  expression  </jsp:expression>

De plus, il existe deux types de commentaires :

 
Sélectionnez
<%-- jsp comment --%>
<!-- html comment -->

La première forme crée dans les pages sources JSP des commentaires qui n'apparaîtront jamais dans la page HTML envoyée au client. Naturellement, la deuxième forme n'est pas spécifique à JSP, c'est un commentaire HTML ordinaire. Ceci est intéressant, car on peut insérer du code JSP dans un commentaire HTML, le commentaire étant inclus dans la page résultante ainsi que le résultat du code JSP.

Les déclarations servent à déclarer des variables et des méthodes dans les langages de scripting utilisés dans une page JSP (uniquement Java pour le moment). La déclaration doit être une instruction Java complète et ne doit pas écrire dans le flux out. Dans l'exemple ci-dessous Hello.jsp, les déclarations des variables loadTime, loadDate et hitCount sont toutes des instructions Java complètes qui déclarent et initialisent de nouvelles variables.

 
Sélectionnez
//:! c15:jsp:Hello.jsp
<%-- This JSP comment will not appear in the
generated html --%>
<%-- This is a JSP directive: --%>
<%@ page import="java.util.*" %>
<%-- These are declarations: --%>
<%!
    long loadTime= System.currentTimeMillis();
    Date loadDate = new Date();
    int hitCount = 0;
%>
<html><body>
<%-- The next several lines are the result of a 
JSP expression inserted in the generated html;
the '=' indicates a JSP expression --%>
<H1>This page was loaded at <%= loadDate %> </H1>
<H1>Hello, world! It's <%= new Date() %></H1>
<H2>Here's an object: <%= new Object() %></H2>
<H2>This page has been up 
<%= (System.currentTimeMillis()-loadTime)/1000 %>
seconds</H2>
<H3>Page has been accessed <%= ++hitCount %> 
times since <%= loadDate %></H3>
<%-- A "scriptlet" that writes to the server
console and to the client page. 
Note that the ';' is required: --%>
<%
   System.out.println("Goodbye");
   out.println("Cheerio");
%>
</body></html>
///:~

Lorsque ce programme fonctionne, on constate que les variables loadTime, loadDate et hitCount gardent leurs valeurs entre les appels de la page, il s'agit donc clairement de champs et non de variables locales.

À la fin de l'exemple, un scriplet écrit « Goodbye » sur la console du serveur Web et « Cheerio » sur l'objet JspWriter implicite out. Les scriptlets peuvent contenir tout fragment de code composé d'instructions Java valides. Les scriptlets sont exécutés au moment du traitement de la demande. Lorsque tous les fragments de scriptlet d'une page JSP donnée sont combinés dans l'ordre où ils apparaissent dans la page JSP, ils doivent contenir une instruction valide telle que définie dans le langage de programmation Java. Le fait qu'ils écrivent ou pas dans le flux out dépend du code du scriplet. Il faut garder à l'esprit que les scriplets peuvent produire des effets de bord en modifiant les objets se trouvant dans leur champ de visibilité.

Les expressions JSP sont mêlées au code HTML dans la section médiane de Hello.jsp. Les expressions doivent être des instructions Java complètes, qui sont évaluées, traduites en String, et envoyées à out. Si le résultat d'une expression ne peut pas être traduit en String alors une exception ClassCastException est lancée.

XVII-D-4. Extraire des champs et des valeurs

L'exemple suivant ressemble à un autre vu précédemment dans la section servlet. La première fois que la page est appelée, il détecte s'il n'existe pas de champ et renvoie une page contenant un formulaire, en utilisant le même code que dans l'exemple de la servlet, mais au format JSP. Lorsque le formulaire contenant des champs remplis est envoyé à la même URL JSP, il détecte les champs et les affiche. C'est une technique agréable parce qu'elle permet d'avoir dans un seul fichier, le formulaire destiné au client et le code de réponse pour cette page, ce qui rend les choses plus simples à créer et maintenir.

 
Sélectionnez
//:! c15:jsp:DisplayFormData.jsp
<%-- Fetching the data from an HTML form. --%>
<%-- This JSP also generates the form. --%>
<%@ page import="java.util.*" %>
<html><body>
<H1>DisplayFormData</H1><H3>
<%
  Enumeration flds = request.getParameterNames();
  if(!flds.hasMoreElements()) { // No fields %>
    <form method="POST" 
    action="DisplayFormData.jsp">
<%  for(int i = 0; i < 10; i++) {  %>
      Field<%=i%>: <input type="text" size="20"
      name="Field<%=i%>" value="Value<%=i%>"><br>
<%  } %>
    <INPUT TYPE=submit name=submit 
    value="Submit"></form>
<%} else { 
    while(flds.hasMoreElements()) {
      String field = (String)flds.nextElement();
      String value = request.getParameter(field);
%>
      <li><%= field %> = <%= value %></li>
<%  }
  } %>
</H3></body></html>
///:~

Ce qui est intéressant dans cet exemple est de montrer comment le code scriptlet et le code HTML peuvent être entremêlés, au point de générer une page HTML à l'intérieur d'une boucle Java for. En particulier ceci est très pratique pour construire tout type de formulaire qui sans cela nécessiterait du code HTML répétitif.

XVII-D-5. Attributs et visibilité d'une page JSP

En cherchant dans la documentation HTML des servlets et des JSP, on trouve des fonctionnalités donnant des informations à propos de la servlet ou de la page JSP actuellement en cours. L'exemple suivant montre quelques-unes de ces données.

 
Sélectionnez
//:! c15:jsp:PageContext.jsp
<%--Viewing the attributes in the pageContext--%>
<%-- Note that you can include any amount of code
inside the scriptlet tags --%>
<%@ page import="java.util.*" %>
<html><body>
Servlet Name: <%= config.getServletName() %><br>
Servlet container supports servlet version:
<% out.print(application.getMajorVersion() + "."
+ application.getMinorVersion()); %><br>
<%
  session.setAttribute("My dog", "Ralph");
  for(int scope = 1; scope <= 4; scope++) {  %>
    <H3>Scope: <%= scope %> </H3>
<%  Enumeration e =      pageContext.getAttributeNamesInScope(scope);
    while(e.hasMoreElements()) {
      out.println("\t<li>" + 
        e.nextElement() + "</li>");
    }
  }
%>
</body></html>
///:~

Cet exemple montre également l'utilisation du mélange de code HTML et d'écriture sur out pour fabriquer la page HTML résultante.

La première information générée est le nom de la servlet, probablement « JSP », mais cela dépend de votre implémentation. On peut voir également la version courante du conteneur de servlet au moyen de l'objet application. Pour finir, après avoir déclaré les attributs de la session, les noms d'attributs sont affichés avec une certaine visibilité. On n'utilise pas beaucoup les visibilités dans la plupart des programmes JSP ; on les a montrés ici simplement pour donner de l'intérêt à l'exemple. Il existe quatre attributs de visibilité, qui sont : la visibilité de page (visibilité 1), la visibilité de demande (visibilité 2), la visibilité de session (visibilité 3 : ici, le seul élément disponible dans la visibilité de session est « My dog »,  ajouté juste après la boucle for), et la visibilité d'application (visibilité 4), basée sur l'objet ServletContext. Il existe un ServletContext pour chaque application Web tournant sur une Machine Virtuelle Java (une application Web est une collection de servlets et de contenus placés dans un sous-ensemble spécifique de l'espace de nommage de l'URL serveur tel que /catalog. Ceci est généralement réalisé au moyen d'un fichier de configuration). Au niveau de visibilité de l'application, on peut voir des objets représentant les chemins du répertoire de travail et du répertoire temporaire.

XVII-D-6. Manipuler les sessions en JSP

Les sessions ont été introduites dans les sections précédentes à propos des servlets, et sont également disponibles dans les JSP. L'exemple suivant utilise l'objet session et permet de superviser le temps au bout duquel la session deviendra invalide.

L'objet session est fourni par défaut, il est donc disponible sans code supplémentaire. Les appels de getID( ), getCreationTime( ) et getMaxInactiveInterval( ) servent à afficher des informations sur l'objet session.

Quand on ouvre la session pour la première fois, on a, par exemple, MaxInactiveInterval égal à 1800 secondes (30 minutes). Ceci dépend de la configuration du conteneur JSP/servlet. MaxInactiveInterval est ramené à 5 secondes afin de rendre les choses intéressantes. Si on rafraîchit la page avant la fin de l'intervalle de 5 secondes, alors on voit :

 
Sélectionnez
Session value for "My dog" = Ralph

Mais si on attend un peu plus longtemps, alors « Ralph » devient null.

Pour voir comment les informations de sessions sont répercutées sur les autres pages, ainsi que pour comparer le fait d'invalider l'objet session à celui de le laisser se terminer, deux autres JSP sont créées. La première (qu'on atteint avec le bouton « invalidate » de SessionObject.jsp) lit l'information de session et invalide explicitement cette session :

 
Sélectionnez
//:! c15:jsp:SessionObject2.jsp
<%--The session object carries through--%>
<html><body>
<H1>Session id: <%= session.getId() %></H1>
<H1>Session value for "My dog" 
<%= session.getValue("My dog") %></H1>
<% session.invalidate(); %>
</body></html>
///:~

Pour tester cet exemple, rafraîchir SessionObject.jsp, puis cliquer immédiatement sur le bouton invalidate pour activer SessionObject2.jsp. À ce moment on voit toujours « Ralph », immédiatement (avant que l'intervalle de 5 secondes ait expiré). Rafraîchir SessionObject2.jsp pour voir que la session a été invalidée manuellement et que Ralph a disparu.

En recommençant avec SessionObject.jsp, rafraîchir la page ce qui démarre un nouvel intervalle de 5 secondes, puis cliquer sur le bouton « Keep Around », ce qui nous amène à la page suivante, SessionObject3.jsp, qui N'invalide PAS la session :

 
Sélectionnez
//:! c15:jsp:SessionObject3.jsp
<%--The session object carries through--%>
<html><body>
<H1>Session id: <%= session.getId() %></H1>
<H1>Session value for "My dog" 
<%= session.getValue("My dog") %></H1>
<FORM TYPE=POST ACTION=SessionObject.jsp>
<INPUT TYPE=submit name=submit Value="Return">
</FORM>
</body></html>
///:~

Dû au fait que cette page n'invalide pas la session, « Ralph » est toujours là aussi longtemps qu'on rafraîchit la page avant la fin de l'intervalle de 5 secondes. Ceci n'est pas sans ressembler à un « Tomagotchi », et « Ralph » restera là tant que vous jouerez avec lui, sinon il disparaîtra.

XVII-D-7. Créer et modifier des cookies

Les cookies ont été introduits dans la section précédente concernant les servlets. Ici encore, la concision des JSP rend l'utilisation des cookies plus simple que dans les servlets. L'exemple suivant montre cela en piégeant les cookies liés à une demande en entrée, en lisant et modifiant leur date d'expiration, et en liant un nouveau cookie à la réponse :

 
Sélectionnez
//:! c15:jsp:Cookies.jsp
<%--This program has different behaviors under
different browsers! --%>
<html><body>
<H1>Session id: <%= session.getId() %></H1>
<%
Cookie[ « cookies = request.getCookies();
for(int i = 0; i < cookies.length; i++) { %>
  Cookie name: <%= cookies[i].getName() %> <br>
  value: <%= cookies[i].getValue() %><br>
  Old max age in seconds: 
  <%= cookies[i].getMaxAge() %><br>
  <% cookies[i].setMaxAge(5); %>
  New max age in seconds: 
  <%= cookies[i].getMaxAge() %><br>
<% } %>
<%! int count = 0; int dcount = 0; %>
<% response.addCookie(new Cookie(
    "Bob" + count++, "Dog" + dcount++)); %>
</body></html>
///:~

Chaque navigateur ayant sa manière de stocker les cookies, le résultat sera différent suivant le navigateur (ce qui n'est pas rassurant, mais peut-être réparerez-vous un certain nombre de bogues en lisant cela). Par ailleurs, il se peut aussi que l'on ait des résultats différents en arrêtant le navigateur et en le relançant, plutôt que de visiter une autre page puis de revenir à Cookies.jsp. Remarquons que l'utilisation des objets session semble plus robuste que l'utilisation directe des cookies.

Après l'affichage de l'identifiant de session, chaque cookie du tableau de cookies arrivant avec l'objet request object est affiché, ainsi que sa date d'expiration. La date d'expiration est modifiée et affichée à son tour pour vérifier la nouvelle valeur, puis un nouveau cookie est ajouté à la réponse. Toutefois, il est possible que votre navigateur semble ignorer les dates d'expiration ; il est préférable de jouer avec ce programme en modifiant la date d'expiration pour voir ce qui se passe avec divers navigateurs.

XVII-D-8. Résumé sur les JSP

Cette section était un bref aperçu des JSP ; cependant avec les sujets abordés ici (ainsi qu'avec le langage Java appris dans le reste du livre, sans oublier votre connaissance personnelle du langage HTML) vous pouvez dès à présent écrire des pages Web sophistiquées via les JSP. La syntaxe JSP n'est pas particulièrement profonde ni compliquée, et si vous avez compris ce qui était présenté dans cette section vous êtes prêt à être productif en utilisant les JSP. Vous trouverez d'autres informations dans la plupart des livres sur les servlets, ou bien à java.sun.com.

La disponibilité des JSP est très agréable, même lorsque votre but est de produire des servlets. Vous découvrirez que si vous vous posez une question à propos du comportement d'une fonctionnalité servlet, il est plus facile et plus rapide d'y répondre en écrivant un programme de test JSP qu'en écrivant une servlet. Ceci est dû en partie au fait qu'on ait moins de code à écrire et qu'on puisse mélanger le code Java et le code HTML, mais l'avantage devient particulièrement évident lorsqu'on voit que le Conteneur JSP se charge de la recompilation et du chargement du JSP à votre place chaque fois que la source est modifiée.

Toutefois, aussi fantastiques que soient les JSP, il vaut mieux garder à l'esprit que la création de pages JSP requiert un plus haut niveau d'habileté que la simple programmation en Java ou la simple création de pages Web. En outre, déboguer une page JSP morcelée n'est pas aussi facile que déboguer un programme Java, car (pour le moment) les messages d'erreur sont assez obscurs. Cela changera avec l'évolution des systèmes de développement, et peut-être verrons-nous d'autres technologies construites au-dessus de Java plus adaptées aux qualités des concepteurs de site web.

XVII-E. RMI (Remote Method Invocation) : Invocation de méthodes distantes

Les approches traditionnelles pour exécuter des instructions à travers un réseau sur d'autres ordinateurs étaient aussi confuses qu'ennuyeuses et sujettes aux erreurs. La meilleure manière d'aborder ce problème est de considérer qu'en fait un objet donné est présent sur une autre machine, on lui envoie un message et l'on obtient le résultat comme si l'objet était instancié sur votre machine locale. Cette simplification est exactement celle que Java 1.1 Remote Method Invocation (RMI - Invocation de méthodes distantes) permet de faire. Cette section vous accompagne à travers les étapes nécessaires pour créer vos propres objets RMI.

XVII-E-1. Interfaces Remote

RMI utilise beaucoup d'interfaces. Lorsque l'on souhaite créer un objet distant, l'implémentation sous-jacente est masquée en passant par une interface. Ainsi, lorsqu'un client obtient une référence vers un objet distant, ce qu'il possède réellement est une référence intermédiaire, qui renvoie à un bout de code local capable de communiquer à travers le réseau. Mais nul besoin de se soucier de cela, il suffit d'envoyer des messages par le biais de cette référence intermédiaire.

La création d'une interface distante doit respecter ces directives :

  1. L'interface distante doit être public (il ne peut y avoir accès package, autrement dit d'accès friendly). Sinon, le client recevra une erreur lorsqu'il tentera d'obtenir un objet distant qui implémente l'interface distante ;
  2. L'interface distante doit hériter de l'interface java.rmi.Remote ;
  3. Chaque méthode de l'interface distante doit déclarer java.rmi.RemoteException dans sa clause throws en plus des autres exceptions spécifiques à l'application ;
  4. Un objet distant passé en argument ou en valeur de retour (soit directement ou inclus dans un objet local), doit être déclaré en tant qu'interface distante et non comme la classe d'implémentation.

Voici une interface distante simple qui représente un service d'heure exacte :

 
Sélectionnez
//: c15:rmi:PerfectTimeI.java
// L'interface distante PerfectTime.
package c15.rmi;
import java.rmi.*;

interface PerfectTimeI extends Remote {
  long getPerfectTime() throws RemoteException;
} ///:~

Cela ressemble à n'importe quelle autre interface mis à part qu'elle hérite de Remote et que toutes ses méthodes émettent RemoteException. Rappelez-vous qu'une interface et toutes ses méthodes sont automatiquement publiques.

XVII-E-2. Implémenter l'interface distante

Le serveur doit contenir une classe qui hérite de UnicastRemoteObject et qui implémente l'interface distante. Cette classe peut avoir aussi des méthodes supplémentaires, mais bien sûr, seules les méthodes appartenant à l'interface distante seront disponibles pour le client puisque celui-ci n'obtiendra qu'une référence vers l'interface, et non vers la classe qui l'implémente.

Vous devez explicitement définir le constructeur de cet objet distant même si vous définissez seulement un constructeur par défaut qui appelle le constructeur de base. Vous devez l'écrire parce qu'il doit émettre RemoteException.

Voici l'implémentation de l'interface distante PerfectTimeI :

 
Sélectionnez
//: c15:rmi:PerfectTime.java
// L'implémentation de 
// l'objet distant PerfectTime.
package c15.rmi;
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
import java.net.*;

public class PerfectTime
    extends UnicastRemoteObject
    implements PerfectTimeI {
  // Implémentation de l'interface:
  public long getPerfectTime()
      throws RemoteException {
    return System.currentTimeMillis();
  }
  // Doit implémenter le constructeur
  // pour émettre RemoteException:
  public PerfectTime() throws RemoteException {
    // super(); // Appelé implicitement
  }
  // Inscription auprès du service RMI :
  public static void main(String[ « args) {
    System.setSecurityManager(
      new RMISecurityManager());
    try {
      PerfectTime pt = new PerfectTime();
      Naming.bind(
        "//peppy:2005/PerfectTime", pt);
      System.out.println("Ready to do time");
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

Ici, main( ) se charge de tous les détails de mise en place du serveur. Une application qui met en service des objets RMI doit à un moment :

  1. Créer et installer un gestionnaire de sécurité qui supporte RMI. Le seul disponible pour RMI fourni dans la distribution Java est RMISecurityManager ;
  2. Créer une ou plusieurs instances de l'objet distant. Ici, vous pouvez voir la création de l'objet PerfectTime ;
  3. Enregistrer au moins un des objets distants grâce au registre d'objets distants RMI pour des raisons d'amorçage. Un objet distant peut avoir des méthodes qui retournent des références vers les autres objets distants. En le mettant en place, le client ne doit s'adresser au registre qu'une seule fois pour obtenir ce premier objet distant.
Mise en place du registre

Ici, vous pouvez voir un appel à la méthode statique Naming.bind( ). Toutefois, cet appel nécessite que le registre fonctionne dans un autre processus de l'ordinateur. Le nom du registre serveur est rmiregistry, et sous Windows 32 bit vous utiliserez :

 
Sélectionnez
start rmiregistry

pour démarrer celui-ci en fond. Sous Unix, ce sera :

 
Sélectionnez
rmiregistry &

Comme beaucoup d'applications réseau, le rmiregistry est localisé à l'adresse IP de la machine qui l'a démarré, mais il doit aussi écouter un port. Si vous invoquez le rmiregistry comme ci-dessus, sans argument, le port écouté par le registre sera par défaut 1099. Si vous souhaitez que ce soit un autre port, vous devez ajouter un argument à la ligne de commande pour le préciser. Dans cet exemple, le port sera 2005, ainsi le rmiregistry devra être démarré de cette manière sous Windows 32 bit :

 
Sélectionnez
start rmiregistry 2005

ou pour Unix:

 
Sélectionnez
rmiregistry 2005 &

L'information concernant le port doit aussi être fournie à la commande bind( ), ainsi que l'adresse IP de la machine où se trouve le registre. Mais il faut mentionner que cela peut être un problème frustrant en cas de tests de programmes RMI en local (de la même manière que les programmes réseau testés plus loin dans ce chapitre). En effet dans le JDK version 1.1.1, il y a quelques problèmes : (60)

  1. localhost ne fonctionne pas avec RMI. Aussi, pour faire une expérience avec RMI sur une seule machine, vous devez fournir le nom de la machine. Pour découvrir le nom de votre machine sous Windows 32-bit, allez dans le Panneau de configuration et sélectionnez Réseau. Sélectionnez l'onglet Identification, vous verrez apparaître le nom de l'ordinateur. Dans mon cas, j'ai appelé mon ordinateur Peppy. Il semble que la différence entre majuscules et minuscules soit ignorée ;
  2. RMI ne fonctionnera pas à moins que votre ordinateur ait une connexion TCP/IP active, même si tous vos composants discutent entre eux sur la machine locale. Cela signifie que vous devez vous connecter à Internet pour essayer de faire fonctionner le programme ou vous obtiendrez quelques messages d'exception obscurs.

En ayant tout cela à l'esprit, la commande bind( ) devient :

 
Sélectionnez
Naming.bind("//peppy:2005/PerfectTime", pt);

Si vous utilisez le port par défaut 1099, nul besoin de préciser le port, donc vous pouvez dire :

 
Sélectionnez
Naming.bind("//peppy/PerfectTime", pt);

Vous devriez être capable de réaliser un test local en omettant l'adresse IP et en utilisant simplement l'identifiant :

 
Sélectionnez
Naming.bind("PerfectTime", pt);

Le nom pour le service est arbitraire ; il se trouve que c'est PerfectTime ici, comme le nom de la classe, mais un tout autre nom conviendrait. L'important est que ce nom soit unique dans le registre, connu par le client pour qu'il puisse se procurer l'objet distant. Si le nom est déjà utilisé dans le registre, celui-ci renvoie une AlreadyBoundException. Pour éviter cela, il faut passer à chaque fois par un appel à rebind( ) à la place de bind( ), en effet rebind( ) soit ajoute une nouvelle entrée, soit remplace celle existant déjà.

Durant l'exécution de main( ), l'objet a été créé et enregistré, il reste ainsi en activité dans le registre, attendant qu'un client arrive et fasse appel à lui. Tant que rmiregistry fonctionne et que Naming.unbind( ) n'est pas appelé avec le nom choisi, l'objet sera présent. C'est pour cela que lors de la conception du code, il faut redémarrer rmiregistry après chaque compilation d'une nouvelle version de l'objet distant.

rmiregistry n'est pas forcément démarré en tant que processus externe. S'il est sûr que l'application est la seule qui utilise le registre, celui peut être mis en fonction à l'intérieur même du programme grâce à cette ligne :

 
Sélectionnez
LocateRegistry.createRegistry(2005);

Comme auparavant, nous utilisons le numéro de port 2005 dans cet exemple. C'est équivalent au fait de lancer rmiregistry 2005 depuis la ligne de commande, mais c'est souvent plus pratique lorsque l'on développe son code RMI puisque cela élimine les étapes supplémentaires qui consistent à arrêter et redémarrer le registre. Une fois cette instruction exécutée, la méthode bind( ) de la classe Naming peut être utilisée comme précédemment.

Si l'on compile et exécute PerfectTime.java, cela ne fonctionnera pas même si rmiregistry a été correctement mis en route. Cela parce que la machinerie pour RMI n'est pas complète. Il faut d'abord créer les stubs et skeletons qui assurent les opérations de connexions réseau et permettent de faire comme si l'objet distant était juste un objet local sur de la machine.

Ce qui se passe derrière la scène est complexe. Tous les objets envoyés à un objet distant ou reçus de celui-ci doivent implémenter Serializable (pour passer des références distantes plutôt que les objets complets, les arguments peuvent implémenter Remote), ainsi on peut considérer que les stubs et les skeletons assurent automatiquement la sérialisation et la désérialisation tout en gérant l'acheminement des arguments et du résultat au travers du réseau. Par chance, vous n'avez rien à connaître de tout cela, mais vous devez avoir créé les stubs et les skeletons. C'est une procédure simple : on invoque l'outil rmic sur le code compilé, et il crée les fichiers nécessaires. La seule chose obligatoire est donc d'ajouter cette étape à la procédure de compilation.

L'outil rmic est particulier en ce qui concerne les packages et les classpaths. PerfectTime.java est dans le package c15.rmi, et même rmic est invoqué dans le même répertoire que celui où se trouve PerfectTime.class, rmic ne trouvera pas le fichier, puisqu'il se repère grâce au classpath. Vous devez ainsi préciser la localisation du classpath, comme ceci :

 
Sélectionnez
rmic c15.rmi.PerfectTime

La commande ne nécessite pas d'être exécutée à partir du répertoire contenant PerfectTime.class, mais les résultats seront placés dans le répertoire courant.

Lorsque rmic a été exécuté avec succès, deux nouvelles classes sont obtenues dans le répertoire :

 
Sélectionnez
PerfectTime_Stub.class
PerfectTime_Skel.class

correspondant au stub et au skeleton. Dès lors, vous êtes prêt à faire communiquer le serveur et le client.

XVII-E-3. Utilisation de l'objet distant

Le but de RMI est de simplifier l'utilisation d'objets distants. La seule chose supplémentaire qui doit être réalisée dans le programme client est de rechercher et de rapatrier depuis le serveur l'interface distante. Après quoi, c'est simplement de la programmation Java classique : envoyer des messages aux objets. Voici le programme qui utilise PerfectTime :

 
Sélectionnez
//: c15:rmi:DisplayPerfectTime.java
// Utilise l'objet distant PerfectTime.
package c15.rmi;
import java.rmi.*;
import java.rmi.registry.*;

public class DisplayPerfectTime {
  public static void main(String[ « args) {
    System.setSecurityManager(
      new RMISecurityManager());
    try {
      PerfectTimeI t =        (PerfectTimeI)Naming.lookup(
          "//peppy:2005/PerfectTime");
      for(int i = 0; i < 10; i++)
        System.out.println("Perfect time = " +
          t.getPerfectTime());
    } catch(Exception e) {
      e.printStackTrace();
    }
  }
} ///:~

L'identifiant alphanumérique est le même que celui utilisé pour enregistrer l'objet avec Naming, et la première partie représente l'adresse et le numéro du port. Cette URL permet par exemple de désigner une machine sur Internet.

Ce qui est retourné par Naming.lookup( ) doit être transtypé vers l'interface distante, pas vers la classe. Sinon l'utilisation de la classe à la place renverrait une exception.

Vous pouvez observer dans l'appel de la méthode

 
Sélectionnez
t.getPerfectTime( )

qu'une fois la référence vers l'objet distant obtenue, la programmation avec celle-ci ou avec un objet distant est identique (avec une différence : les méthodes distantes émettent RemoteException).

XVII-F. Introduction à CORBA

Dans le cadre d'importantes applications distribuées, vos besoins risquent de ne pas être satisfaits par les approches précédentes : par exemple, l'intégration de bases existantes, ou l'accès aux services d'un objet serveur sans se soucier de sa localisation physique. Ces situations nécessitent une certaine forme de Remote Procedure Call (RPC), et peut-être une indépendance par rapport au langage. Là, CORBA peut vous aider.

CORBA n'est pas une fonctionnalité du langage ; c'est une technologie d'intégration. C'est une spécification que les fabricants peuvent suivre pour implémenter des produits supportant une intégration CORBA. CORBA fait partie du travail réalisé par OMG afin de définir une structure standard pour l'interopérabilité d'objets distribués et indépendants du langage.

CORBA fournit la possibilité de faire des appels à des procédures distantes dans des objets Java et des objets non Java, et d'interfacer des systèmes existants sans se soucier de leur emplacement. Java ajoute un support réseau et un langage orienté objet agréable pour construire des applications graphiques ou non. Le modèle objet de l'OMG et celui de Java vont bien ensemble ; par exemple, Java et CORBA mettent tous deux en œuvre le concept d'interface et le modèle de référence objet.

XVII-F-1. Principes de base de CORBA

La spécification de l'interopérabilité objet est généralement désignée comme l'Object Manager Architecture (OMA). L'OMA définit deux composants : le Core Object Model et l'OMA Reference Architecture. Le Core Object Model met en place les concepts de base d'objet, d'interface, d'opération, et ainsi de suite (CORBA est un raffinement de Core Object Model). L'OMA Reference Architecture définit l'infrastructure sous-jacente des services et des mécanismes qui permettent aux objets d'interopérer. L'OMA Reference Architecture contient l'Object Request Broker (ORB), les Object Services (désignés aussi comme les services CORBA) et les outils communs.

L'ORB est le bus de communication par lequel les objets peuvent réclamer des services auprès des autres objets, sans rien connaître de leur emplacement physique. Cela signifie que ce qui ressemble à un appel de méthode dans le code client est vraiment une opération complexe. D'abord, une connexion avec l'objet servant doit exister et pour créer cette connexion, l'ORB doit savoir où se trouve le code implémentant cette partie serveur. Une fois que la connexion est établie, les arguments de la méthode doivent être arrangés (marshaled), c'est-à-dire convertis en un flux binaire pour être envoyés à travers le réseau. Les autres informations qui doivent être envoyées sont le nom de la machine serveur, le processus serveur et l'identité de l'objet servant au sein de ce processus. Finalement, l'information est envoyée par l'intermédiaire d'un protocole bas-niveau, elle est décodée du côté du serveur, l'appel est exécuté. L'ORB masque tout de cette complexité au programmeur et rend l'opération presque aussi simple que l'appel d'une méthode d'un objet local.

Cette spécification n'a pas pour but d'expliquer comment le cœur de l'ORB devrait être implémenté, mais elle permet une compatibilité fondamentale entre les différents ORB des fournisseurs, l'OMG définit un ensemble de services qui sont accessibles par l'intermédiaire d'interfaces standards.

XVII-F-1-a. CORBA Interface Definition Language (IDL - Langage de Définition d'Interface)

CORBA a été mis au point pour être transparent vis-à-vis du langage : un objet client peut appeler des méthodes d'un objet serveur d'une classe différente, sans se préoccuper du langage avec lequel elles sont implémentées. Bien sûr, l'objet client doit connaître le nom et les prototypes des méthodes que l'objet servant met à disposition. C'est là que l'IDL intervient. L'IDL de CORBA est un moyen indépendant du langage qui permet de préciser les types de données, les attributs, les opérations, les interfaces, et plus encore. La syntaxe de l'IDL est similaire à celles du C++ ou de Java. La table qui suit montre la correspondance entre quelques-uns des concepts communs aux trois langages qui peuvent être spécifiés à travers l'IDL de CORBA :

CORBA IDL Java C++
Module Package Namespace
Interface Interface Pure abstract class
Method Method Member function

Le concept d'héritage est également supporté, en utilisant le séparateur deux-points comme en C++. Le programmeur écrit en IDL la description des attributs, des méthodes et des interfaces qui seront implémentés et utilisés par le serveur et les clients. L'IDL est ensuite compilé par un compilateur IDL/Java propre au fournisseur, qui lit le source IDL et génère le code Java.

Le compilateur IDL est un outil extrêmement utile : il ne génère pas juste le source Java équivalent à l'IDL, il génère aussi le code qui sera utilisé pour réunir les arguments des méthodes et pour réaliser les appels distants. Ce code, appelé le code stub et le code skeleton, est organisé en plusieurs fichiers source Java et fait habituellement partie d'un même package Java.

XVII-F-1-b. Le service de nommage (naming service)

Le service de nommage est l'un des services fondamentaux de CORBA. L'objet CORBA est accessible par l'intermédiaire d'une référence, une information qui n'a pas de sens pour un lecteur humain. Mais les références peuvent être des chaînes de caractères définies par le programmeur. Cette opération est désignée comme chaînifier la référence, et l'un des composants de l'OMA, le service de nommage, est dédicacé à la conversion nom-vers-objet et objet-vers-nom et gère ces correspondances. Puisque le service de nommage joue le rôle d'un annuaire téléphonique que les serveurs et les clients peuvent consulter et manipuler, il fonctionne dans un processus séparé. Créer une correspondance objet-vers-nom est appelé lier (binding) un objet, et supprimer cette correspondance est dit délier (unbinding). Obtenir l'objet référence en passant la chaîne de caractères est appelé résoudre le nom.

Par exemple, au démarrage, une application serveur peut créer un objet servant, enregistrer l'objet auprès du service de nommage, et attendre que des clients fassent des requêtes. Un client obtient d'abord une référence vers cet objet servant en résolvant le nom et ensuite peut faire des appels auprès du serveur en utilisant cette référence.

À nouveau, la spécification du service de nommage fait partie de CORBA, mais l'application qui l'implémente est mise à disposition par le fournisseur de l'ORB. Le moyen d'accéder à ce service de nommage peut varier d'un fournisseur à l'autre.

XVII-F-2. Un exemple

Le code montré ici ne sera pas très élaboré, car les différents ORB ont des moyens d'accéder aux services CORBA qui divergent, ainsi les exemples dépendent du fournisseur. L'exemple qui suit utilise JavaIDL, un produit gratuit de Sun, qui fournit un ORB basique, un service de nommage, et un compilateur IDL-vers-Java. De plus, comme Java est encore jeune et en constante évolution, les différents produits CORBA pour Java n'incluent forcément toutes les fonctionnalités de CORBA.

Nous voulons implémenter un serveur, fonctionnant sur une machine donnée, qui soit capable de retourner l'heure exacte. Nous voulons aussi implémenter un client qui demande l'heure exacte. Dans notre cas, nous allons réalisons les deux programmes en Java, mais nous pourrions faire de même avec deux langages différents (ce qui se produit souvent dans la réalité).

XVII-F-2-a. Écrire le source IDL

La première étape consiste à écrire une description en IDL des services proposés. Ceci est généralement réalisé par le programmeur du serveur, qui est ensuite libre d'implémenter le serveur dans n'importe quel langage pour lequel un compilateur CORBA IDL existe. Le fichier IDL est communiqué au programme de la partie cliente et devient le pont entre les langages.

L'exemple qui suit montre la description IDL de notre serveur ExactTime :

 
Sélectionnez
//: c15:corba:ExactTime.idl
//# Vous devez installer idltojava.exe de 
//# java.sun.com et ajuster le paramétrage pour utiliser
//# votre préprocesseur C local pour compiler
//# ce fichier. Voyez la documentation sur java.sun.com.
module remotetime {
   interface ExactTime {
      string getTime();
   };
}; ///:~

C'est donc la déclaration de l'interface ExactTime au sein de l'espace de nommage remotetime. L'interface est composée d'une seule méthode qui retourne l'heure actuelle dans une chaîne de caractères.

XVII-F-2-b. Création des stubs et des skeletons

La deuxième étape consiste à compiler l'IDL pour créer le code Java du stub et du skeleton que nous utiliserons pour implémenter le client et le serveur. L'outil fourni par le produit JavaIDL est idltojava :

 
Sélectionnez
idltojava remotetime.idl

Cela générera automatiquement à la fois le code pour le stub et celui pour le skeleton. Idltojava génère un package Java nommé selon le module IDL remotetime et les fichiers Java générés sont déposés dans ce sous-répertoire remotetime. _ExactTimeImplBase.java est le skeleton que nous allons utiliser pour implémenter l'objet servant et _ExactTimeStub.java sera utilisé pour le client. Il y a des représentations Java de l'interface IDL dans ExactTime.java et toute une série d'autres fichiers de support utilisés, par exemple, pour faciliter l'accès aux fonctions du service de nommage.

XVII-F-2-c. Implémentation du serveur et du client

Ci-dessous, vous pouvez voir le code pour la partie serveur. L'implémentation de l'objet servant est dans la classe ExactTimeServer. RemoteTimeServer est l'application qui crée l'objet servant, l'enregistre auprès de l'ORB, donne un nom à la référence vers l'objet, et qui ensuite s'assoit tranquillement en attendant les requêtes des clients.

 
Sélectionnez
//: c15:corba:RemoteTimeServer.java
import remotetime.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
import org.omg.CORBA.*;
import java.util.*;
import java.text.*;

// Implémentation de l'objet servant
class ExactTimeServer extends _ExactTimeImplBase {
  public String getTime(){
    return DateFormat.
        getTimeInstance(DateFormat.FULL).
          format(new Date(
              System.currentTimeMillis()));
  }
}

// Implémentation de l'application distante
public class RemoteTimeServer {
  public static void main(String[ « args) {
    try {
      // Crée l'ORB et l'initialise:
      ORB orb = ORB.init(args, null);
      // Crée l'objet servant et l'enregistre :
      ExactTimeServer timeServerObjRef =        new ExactTimeServer();
      orb.connect(timeServerObjRef);
      // Obtient la racine du contexte de nommage :
      org.omg.CORBA.Object objRef =        orb.resolve_initial_references(
          "NameService");
      NamingContext ncRef =        NamingContextHelper.narrow(objRef);
      // Associe un nom
      // à la référence de l'objet (binding):
      NameComponent nc =        new NameComponent("ExactTime", "");
      NameComponent[ « path = { nc };
      ncRef.rebind(path, timeServerObjRef);
      // Attend les requêtes des clients :
      java.lang.Object sync =        new java.lang.Object();
      synchronized(sync){
        sync.wait();
      }
    }
    catch (Exception e) {
      System.out.println(
         "Remote Time server error: " + e);
      e.printStackTrace(System.out);
    }
  }
} ///:~

Comme vous pouvez le constater, implémenter l'objet servant est simple ; c'est une classe Java classique qui hérite du code du skeleton généré par le compilateur IDL. Les choses se complexifient un peu lorsqu'on en vient à interagir avec l'ORB et les autres services CORBA.

XVII-F-2-d. Quelques services CORBA

Voici une courte description de ce qui réalise le code relevant de JavaIDL (en évitant la partie de code CORBA qui dépend du fournisseur). La première ligne dans le main( ) démarre l'ORB parce que bien sûr notre objet servant aura besoin d'interagir avec celui-ci. Juste après l'initialisation de l'ORB, l'objet servant est créé. En réalité, le terme correct serait un objet servant temporaire (transient) : un objet qui reçoit les requêtes provenant des clients, et dont la durée de vie est limitée à celle du processus qui l'a créé. Une fois l'objet servant temporaire créé, il est enregistré auprès de l'ORB, ce qui signifie alors que l'ORB connaît son existence et peut rediriger les requêtes vers lui.

À partir de là, tout ce dont nous disposons est timeServerObjRef, une référence vers un objet qui n'est connu qu'à l'intérieur du processeur serveur actuel. L'étape suivante va consister à associer un nom alphanumérique à cet objet servant ; les clients utiliseront ce nom pour localiser l'objet servant. Cette opération sera réalisée grâce à l'aide du service de nommage. Tout d'abord, nous avons besoin d'une référence vers le service de nommage ; la méthode resolve_initial_references( ) utilise la référence objet « chainifiée » du service de nommage qui s'appelle NameService dans JavaIDL, et retourne la référence vers l'objet. Celle-ci est transformée en une référence spécifique à NamingContext au moyen de la méthode narrow( ). Nous pouvons maintenant utiliser les services de nommage.

Pour associer l'objet servant avec une référence objet « chainifiée », nous créons d'abord un objet NameComponent, initialisé avec la chaîne de caractères qui sera associée : face="Georgia">« ExactTime ». Ensuite, en utilisant la méthode rebind( ), la référence alphanumérique est associée à la référence vers l'objet. Nous utilisons rebind( ) pour mettre en place une référence, même si celle-ci existe déjà. Un nom est composé dans CORBA d'une séquence de NameComponents (voilà pourquoi nous utilisons un tableau pour associer le nom à la référence).

L'objet est enfin prêt à être utilisé par des clients. À ce moment-là, le serveur entre dans un état d'attente. Encore une fois, ceci est nécessaire puisqu'il s'agit d'un serveur temporaire : sa durée de vie dépend du processus serveur. JavaIDL ne supporte pas actuellement les objets persistants, qui survivent après la fin de l'exécution du processus qui les a créés.

Maintenant que nous avons une idée de ce que fait le code du serveur, regardons le code du client :

 
Sélectionnez
//: c15:corba:RemoteTimeClient.java
import remotetime.*;
import org.omg.CosNaming.*;
import org.omg.CORBA.*;

public class RemoteTimeClient {
  public static void main(String[ « args) {
    try {
      // Création et initialisation de l'ORB :
      ORB orb = ORB.init(args, null);
      // Obtient la racine du contexte de nommage :
      org.omg.CORBA.Object objRef =        orb.resolve_initial_references(
          "NameService");
      NamingContext ncRef =        NamingContextHelper.narrow(objRef);
      // Résout la référence alphanumérique
      // du serveur d'heure :
      NameComponent nc =        new NameComponent("ExactTime", "");
      NameComponent[ « path = { nc };
      ExactTime timeObjRef =        ExactTimeHelper.narrow(
          ncRef.resolve(path));
      // Effectue une requête auprès de l'objet servant :
      String exactTime = timeObjRef.getTime();
      System.out.println(exactTime);
    } catch (Exception e) {
      System.out.println(
         "Remote Time server error: " + e);
      e.printStackTrace(System.out);
    }
  }
} ///:~
XVII-F-2-e. Activation du processus du service de nommage

Nous avons enfin un serveur et un client prêts à interopérer. Nous avons pu voir que tous deux ont besoin du service de nommage pour associer et résoudre les références alphanumériques. Le processus du service de nommage doit être démarré avant de faire fonctionner aussi bien le serveur que le client. Dans JavaIDL, le service de nommage est une application Java fournie avec le produit, mais qui peut être différente dans d'autres produits. Le service de nommage de JavaIDL fonctionne dans une JVM et écoute par défaut le port réseau 900.

XVII-F-2-f. Activation du serveur et du client

Vos applications serveur et client sont prêtes à démarrer (dans cet ordre, puisque votre serveur est transient (temporaire)). Si tout est bien en place, vous obtiendrez une simple ligne de sortie dans la fenêtre console de votre client, indiquant l'heure courante. Bien sûr, cela ne semble pas très excitant en soi, mais vous devriez prendre en compte une chose : même s'ils sont physiquement sur la même machine, les applications client et serveur fonctionnent dans des machines virtuelles différentes et peuvent communiquer à travers une couche de l'intégration sous-jacente, l'ORB et le service de nommage.

Ceci constitue un exemple simple, destiné à fonctionner sans réseau, mais un ORB est généralement configuré pour qu'il rende les localisations transparentes. Lorsque le serveur et le client sont sur des machines différentes, l'ORB peut résoudre des références alphanumériques en utilisant un composant désigné Implementation Repository. Bien que le Implementation Repository fasse partie de CORBA, il n'y a quasiment pas de spécification, et donc il est différent d'un fabricant à l'autre.

Ainsi que vous vous en doutez, CORBA représente davantage que ce qui est montré ici, mais ainsi vous aurez compris l'idée de base. Si vous souhaitez plus d'informations à propos de CORBA, le premier endroit à visiter est le site Web de l'OMG : http://www.omg.org. Vous y trouverez la documentation, les white papers et les références vers les autres sources et produits pour CORBA.

XVII-F-3. Les applets Java et CORBA

Les applets Java peuvent se comporter comme des clients CORBA. Par ce biais, une applet peut accéder à des informations et des services distants publiés sous la forme d'objets CORBA. Mais une applet ne peut se connecter qu'au serveur depuis lequel elle a été téléchargée, aussi tous les objets CORBA avec lesquels l'applet interagit doivent être situés sur ce serveur. C'est l'opposé de ce que CORBA essaye de faire : vous offrir une totale transparence de localisation.

C'est une question de sécurité réseau. Si vous êtes sur un intranet, la solution est d'éliminer les restrictions de sécurité du navigateur. Ou, de mettre en place une politique de firewall pour la connexion vers des serveurs externes.

Certains produits proposant des ORB Java offrent des solutions propriétaires à ce problème. Par exemple, certains implémentent ce qui s'appelle un HTTP Tunneling, alors que d'autres ont des fonctionnalités firewall spécifiques.

C'est un point trop complexe pour être exposé dans une annexe, mais c'est quelque chose que vous devriez approfondir.

XVII-F-4. CORBA face à RMI

Vous avez vu que la principale fonctionnalité de CORBA est le support de RPC, qui permet à vos objets locaux d'appeler des méthodes d'objets distants. Bien sûr, il y a déjà une fonctionnalité native à Java qui réalise exactement la même chose : RMI (voir titre XVII). Là où RMI rend possible RPC entre des objets Java, CORBA rend possible RPC entre des objets implémentés dans n'importe quel langage.

Toutefois, RMI peut être utilisé pour appeler des services auprès de code distant non Java. Tout ce dont vous avez besoin est d'une sorte d'objet Java adaptateur autour du code non Java sur la partie serveur. L'objet adaptateur est connecté à l'extérieur aux objets clients par RMI, et en interne il se connecte au code non Java en utilisant l'une des techniques vues précédemment, comme JNI ou J/Direct.

Cette approche nécessite que vous écriviez une sorte de couche d'intégration, exactement ce que CORBA fait pour vous, mais vous n'avez pas besoin ici d'un ORB venu de l'extérieur.

XVII-G. Enterprise Java Beans

(61)À partir de maintenant, vous avez été présenté à CORBA et à RMI. Mais pourriez-vous imaginer de tenter le développement d'une application à grande échelle en utilisant CORBA et/ou RMI ? Votre patron vous a demandé de développer une application multitiers pour consulter et mettre à jour des enregistrements dans une base de données, le tout à travers une interface Web. Vous vous assoyez et pensez à ce que cela veut vraiment dire. Sûr, vous pouvez écrire une application qui utilise JDBC, une interface Web qui utilise JSP et des Servlets, et un système distribué qui utilise CORBA/RMI. Mais quelles autres considérations devez-vous prendre en compte lorsque vous développez un système basé sur des objets distribués plutôt qu'avec des API classiques ? Voici les problèmes.

Performance : les nouveaux objets distribués que vous avez créés devront être performants puisqu'ils devront potentiellement répondre à plusieurs clients en même temps. Donc vous allez vouloir mettre en place des techniques d'optimisation comme l'utilisation de cache, la mise en commun des ressources (comme les connexions à des bases de données par JDBC). Vous devrez aussi gérer le cycle de vie de votre objet distribué.

Adaptation à la charge : les objets distribués doivent aussi s'adapter à l'augmentation de la charge. La scalabilité dans une application distribuée signifie que le nombre d'instances de votre objet distribué peut augmenter et passer sur une autre machine sans la modification du moindre code. Prenez par exemple un système que vous développez en interne comme une petite recherche de clients dans votre organisation depuis une base de données. L'application fonctionne bien quand vous l'utilisez, mais votre patron l'a vue et a dit : « Robert, c'est un excellent système, mettez ça sur notre site Web public maintenant !!! » Est-ce que mon objet distribué est capable de supporter la charge d'une demande potentiellement illimitée.

Sécurité : mon objet distribué contrôle-t-il l'accès des clients ? Puis-je ajouter de nouveaux utilisateurs et des rôles sans tout recompiler ?

Transactions distribuées : mon objet distribué peut-il utiliser de manière transparente des transactions distribuées ? Puis-je mettre à jour ma base de données Oracle et Sybase simultanément au sein de la même transaction et les annuler ensemble si un certain critère n'est pas respecté ?

Réutilisation : ai-je créé mon objet distribué de telle sorte que je puisse le passer d'une application serveur d'un fournisseur à une autre ? Puis-je revendre mon objet distribué (composant) à quelqu'un d'autre ? Puis-je acheter un composant de quelqu'un d'autre et l'utiliser sans avoir à recompiler et à tailler dans son code ?

Disponibilité : si l'une des machines de mon système tombe en panne, mes clients sont-ils capables de basculer des copies de sauvegarde de mes objets fonctionnant sur d'autres machines ?

Comme vous avez pu le voir ci-dessus, les considérations qu'un développeur doit prendre en compte lorsqu'il développe un système distribué sont complexes, et nous n'avons même pas parlé de la solution du problème qui nous essayons de résoudre à l'origine !

Donc maintenant vous avez une liste de problèmes supplémentaires que vous devez résoudre. Alors comme allez-vous vous y prendre ? Quelqu'un l'a certainement déjà fait ? Ne pourrais-je pas utiliser des modèles de conception bien connus pour m'aider à résoudre ces problèmes ? Soudain, une idée vous vient à l'esprit... « Je pourrais créer un framework qui prend en charge toutes ces contraintes et écrire mes composants en m'appuyant sur ce framework ! »... C'est là que les Entreprise JavaBeans rentrent en jeu.

Sun, avec d'autres fournisseurs d'objets distribués a compris que tôt ou tard toute équipe de développement serait en train de réinventer la roue. Ils ont donc créé la spécification des Entreprise JavaBeans (EJB). Les EJB sont une spécification d'un modèle de composant côté serveur qui prend en compte toutes les considérations mentionnées plus haut utilisant une approche standard et définie qui permet aux développeurs de créer des composants (qui sont appelés des Entreprise JavaBeans), qui sont isolés du code de la « machinerie » bas niveau et focalisés seulement sur la mise en place de la logique métier. Et puisque les EJB sont définis sur un standard, ils peuvent être utilisés sans être dépendants d'un fournisseur.

XVII-G-1. JavaBeans contre EJB

La similitude entre les deux noms amène souvent une confusion entre le modèle de composants JavaBeans et la spécification des Enterprise JavaBeans. Bien que les spécifications des JavaBeans et des Enterprise JavaBeans partagent toutes deux les mêmes objectifs (comme la mise en avant de la réutilisation et la portabilité du code Java entre développements, comme aussi des outils de déploiement qui utilise des modèles de conception standards), les motivations derrière chaque spécification ont pour but de résoudre des problèmes différents.

Les standards définis dans le modèle de composants JavaBeans sont conçus pour créer des composants réutilisables qui sont typiquement utilisés dans des outils de développement IDE et sont communément, mais pas exclusivement, des composants visuels.

La spécification des Enterprise JavaBeans définit un modèle de composants pour développer du code Java côté serveur. Comme les EJB peuvent potentiellement fonctionner sur différentes plates-formes serveur (incluant des systèmes qui n'ont pas d'affichage visuel), un EJB ne peut pas utiliser de bibliothèques graphiques telles que AWT ou Swing.

XVII-G-2. Que définit la spécification des EJB ?

La spécification des EJB, actuellement la version 1.1 (public release 2) définit un modèle de composant côté serveur. Elle définit six rôles qui sont utilisés pour réaliser les tâches de développement et de déploiement ainsi que la définition des composants du système.

XVII-G-2-a. Les rôles

La spécification des EJB définit des rôles qui sont utilisés durant le développement, le déploiement et le fonctionnement d'un système distribué. Les fabricants, les administrateurs et les développeurs jouent des rôles variés. Ils permettent la séparation du savoir-faire technique, de celui propre au domaine. Ceci permet au fabricant de proposer un framework technique et aux développeurs de créer des composants spécifiques au domaine comme un composant de compte bancaire. Un même intervenant peut jouer un ou plusieurs rôles. Les rôles définis dans la spécification des EJB sont résumés dans le tableau suivant :

Rôle Responsabilité
Fournisseur d'Enterprise Bean Le développeur qui est responsable de la création des composants EJB réutilisables. Ces composants sont regroupés dans un fichier jar spécial (fichier ejb-jar).
Assembleur d'Application Crée et assemble des applications à partir d'une collection de fichiers ejb-jar. Ceci inclut la réalisation d'applications mettant en œuvre la collection d'EJB (comme des Servlets, JSP, Swing, etc.).
Déployeur Le rôle du déployeur est de prendre la collection de fichiers ejb-jar de l'Assembleur et/ou du Fournisseur d'EJB et de déployer ceux-ci dans un environnement d'exécution (un ou plusieurs Conteneurs d'EJB).
Fournisseur de Conteneurs/Serveurs d'EJB Fournit un environnement d'exécution et les outils qui sont utilisés pour déployer, administrer et faire fonctionner les composants EJB.
Administrateur Système Par-dessus tout, le rôle le plus important de l'ensemble du système : mettre en place et faire fonctionner. La gestion d'une application distribuée consiste à ce que les composants et les services soient tous configurés et interagissent correctement.
XVII-G-2-b. Composants EJB

Les composants EJB sont de la logique métier réutilisable. Les composants EJB suivent strictement les standards et les modèles de conception définis dans la spécification des EJB. Cela permet à ces composants d'être portables et aussi à tous les autres services tels que la sécurité, la mise en cache et les transactions distribuées, d'être mis en œuvre sur ces composants eux-mêmes. Un fournisseur d'Entreprise Bean est responsable du développement des composants EJB. Les entrailles d'un composant EJB sont traitées dans Qu'est-ce qui compose un composant EJB ?

XVII-G-2-b-i. Conteneur d'EJB

Le conteneur d'EJB est un environnement d'exécution qui contient et fait fonctionner les composants EJB tout en leur fournissant un ensemble de services. Les responsabilités des conteneurs d'EJB sont définies précisément par la spécification pour permettre une neutralité vis-à-vis du fournisseur. Les conteneurs d'EJB fournissent la machinerie bas niveau des EJB, incluant les transactions distribuées, la sécurité, la gestion du cycle de vie des beans, la mise en cache, la gestion de la concurrence et des sessions. Le fournisseur de conteneur d'EJB est responsable de la mise à disposition d'un conteneur d'EJB.

XVII-G-2-b-ii. Serveur EJB

Un serveur d'EJB est défini comme un Serveur d'Applications et comporte un ou plusieurs conteneurs d'EJB. Le fournisseur de serveur EJB est responsable de la mise à disposition d'un serveur EJB. Vous pouvez généralement considérer que le conteneur d'EJB et le serveur EJB sont une seule et même chose.

XVII-G-2-b-iii. Java Naming and Directory Interface (JNDI)

Java Naming and Directory Interface (JNDI) est utilisé dans les Entreprise JavaBeans comme le service de nommage pour les composants EJB sur le réseau et pour les autres services du conteneur comme les transactions. JNDI ressemble fort aux autres standards de nommage et de répertoires tels que CORBA CosNaming et peut être implémenté comme un adaptateur de celui-ci.

XVII-G-2-b-iv. Java Transaction API / Java Transaction Service (JTA/JTS)

JTA/JTS est utilisé dans les Enterprise JavaBeans comme API transactionnelle. Un fournisseur d'Entreprise Bean peut utiliser le JTS pour créer un code transactionnel bien que le conteneur d'EJB implémente généralement les transactions dans les EJB sur les composants EJB eux-mêmes. Le déployeur peut définir les attributs transactionnels d'un composant EJB au moment du déploiement. Le conteneur d'EJB est responsable de la prise en charge de la transaction qu'elle soit locale ou distribuée. La spécification du JTS est la version Java de CORBA OTS (Object Transaction Service).

XVII-G-2-b-v. CORBA et RMI/IIOP

La spécification des EJB définit l'interopérabilité avec CORBA. La spécification 1.1 précise que L'architecture des Entreprise JavaBeans sera compatible avec les protocoles CORBA. L'interopérabilité avec CORBA passe par l'adaptation des services EJB comme JTS et JNDI aux services CORBA et l'implémentation de RMI à travers le protocole CORBA IIOP.

L'utilisation de CORBA et de RMI/IIOP dans les Entreprise JavaBeans est implémentée dans le conteneur EJB et est sous la responsabilité du fournisseur du conteneur d'EJB. L'utilisation de CORBA et de RMI/IIOP dans le conteneur EJB est invisible pour le composant EJB lui-même. Cela signifie que le fournisseur d'Entreprise Bean peut écrire ses composants EJB et les déployer dans un conteneur EJB sans s'inquiéter du protocole de communication qui est utilisé.

XVII-G-3. Qu'est-ce qui compose un composant EJB ?

Un EJB se décompose en un ensemble de pièces, dont le Bean lui-même, l'implémentation d'interfaces et un fichier d'informations. Le tout est rassemblé dans un fichier jar spécial.

XVII-G-3-a. Enterprise Bean

L'Enterprise Bean est une classe Java que le fournisseur d'Enterprise Bean développe. Elle implémente une interface EnterpriseBean (pour plus de détails, voir la section qui suit) et fournit l'implémentation des méthodes métier que le composant supporte. La classe n'implémente aucun mécanisme d'autorisation ou d'authentification, de concurrence ou transactionnel.

XVII-G-3-b. Interface Home

Chaque Enterprise Bean créé doit être associé à une interface Home. L'interface Home est utilisée comme une Factory de votre EJB. Les clients utilisent l'interface Home pour trouver une instance de votre EJB ou pour créer une nouvelle instance de votre EJB.

XVII-G-3-c. Interface Remote

L'interface Remote est l'interface Java qui reflète les méthodes de l'Enterprise Bean que l'on souhaite rendre disponible au monde extérieur. L'interface Remote joue un rôle similaire à celui de l'interface IDL de CORBA.

XVII-G-3-d. Descripteur de déploiement

Le descripteur de déploiement est un fichier XML qui contient les informations relatives à l'EJB. L'utilisation de XML permet au déployeur de facilement changer les attributs propres à l'EJB. Les attributs configurables définis dans le descripteur de déploiement incluent :

  • les noms des interfaces Home et Remote que nécessite l'EJB ;
  • le nom avec lequel sera publiée dans JNDI l'interface Home de l'EJB ;
  • les attributs transactionnels pour chaque méthode de l'EJB ;
  • les listes de contrôle d'accès (Access Control Lists) pour l'authentification.
Fichier EJB-Jar

Le fichier EJB-Jar est un fichier jar Java normal qui contient un EJB, les interface Home et Remote ainsi que le descripteur de déploiement.

XVII-G-4. Comment travaille un EJB ?

Maintenant que l'on a un fichier EJB-Jar contenant un Bean, les interfaces Home et Remote, et un descripteur de déploiement, voyons un peu comment toutes ces pièces vont ensemble, pourquoi les interfaces Home et Remote sont nécessaires et comment le conteneur d'EJB les utilise.

Le conteneur d'EJB implémente les interfaces Home et Remote qui sont dans le fichier EJB-Jar. Comme mentionné précédemment, l'interface Home met à disposition les méthodes pour créer et trouver votre EJB. Cela signifie que le conteneur d'EJB est responsable de la gestion du cycle de vie de votre EJB. Ce niveau d'abstraction permet aux optimisations d'intervenir. Par exemple, cinq clients peuvent demander simultanément la création d'un EJB à travers l'interface Home, le conteneur d'EJB pourrait n'en créer qu'un seule et partager cet EJB entre les cinq clients. Ceci est réalisé à travers l'interface Remote, qui est aussi implémentée par le conteneur d'EJB. L'objet implémentant Remote joue le rôle d'objet proxy vers l'EJB.

Tous les appels de l'EJB sont redirigés à travers le conteneur d'EJB grâce aux interfaces Home et Remote. Cette abstraction explique aussi pourquoi le conteneur d'EJB peut contrôler la sécurité et le comportement transactionnel.

XVII-G-5. Types d'EJB

Il doit y avoir une question dans votre tête depuis le dernier paragraphe : partager le même EJB entre les clients peut certainement augmenter les performances, mais qu'en est-il lorsque je souhaite conserver l'état sur le serveur ?

La spécification des Enterprise JavaBeans définit différents types d'EJB qui peuvent avoir différentes caractéristiques et adopter un comportement différent. Deux catégories d'EJB ont été définies dans cette spécification : les Session Beans et les Entity Beans, et chacune de ces catégories a des variantes.

XVII-G-5-a. Session Beans

Les Session Beans sont utilisés pour représenter les cas d'utilisations ou des traitements spécifiques du client. Ils représentent les opérations sur les données persistantes, mais non les données persistantes elles-mêmes. Il y a deux types de Session Beans : non persistant (Stateless) et persistant (Stateful). Tous les Session Beans doivent implémenter l'interface javax.ejb.SessionBean. Le conteneur d'EJB contrôle la vie d'un Session Bean.

XVII-G-5-a-i. Les Session Beans non persistants

Les Session Beans non persistants sont le type d'EJB le plus simple à implémenter. Ils ne conservent aucun état de leur conversation avec les clients entre les invocations de méthodes donc ils sont facilement réutilisables dans la partie serveur et puisqu'ils peuvent être mis en cache, ils supportent bien les variations de la demande. Lors de l'utilisation de Session Beans non persistants, tous les états doivent être stockés à l'extérieur de l'EJB.

XVII-G-5-a-ii. Les Session Beans persistants

Les Session Beans persistants conservent un état entre l'invocation de leurs méthodes (comme vous l'aviez probablement compris). Ils ont une association 1-1 avec un client et sont capables de conserver leurs états eux-mêmes. Le conteneur d'EJB a en charge le partage et la mise en cache des Session Beans persistants, ceux-ci passent par la Passivation et l'Activation.

XVII-G-5-b. Entity Beans

Les Entity Beans sont des composants qui représentent une donnée persistante et le comportement de cette donnée. Les Entity Beans peuvent être partagés par plusieurs clients, comme une donnée d'une base. Le conteneur d'EJB a en charge de mettre en cache les Entity Beans et de maintenir leur intégrité. La vie d'un Entity Bean est supérieure à celle du conteneur d'EJB, donc si un conteneur tombe en panne, l'Entity Bean est censé être encore disponible lorsque le conteneur le devient à nouveau.

Il y a deux types d'Entity Beans, ceux dont la persistance est assurée par le Bean lui-même et ceux dont la persistance est assurée par le conteneur.

Un CMP Entity Bean a sa persistance assurée par le conteneur d'EJB. À travers les attributs spécifiés dans le descripteur de déploiement, le conteneur d'EJB fera correspondre les attributs de l'Entity Bean avec un stockage persistant (habituellement, mais pas toujours, une base de données). La gestion de la persistance par le conteneur réduit le temps de développement et réduit considérablement le code nécessaire pour l'EJB.

XVII-G-5-b-i. Gestion de la persistance par le Bean (BMP - Bean Managed Persistence)

Un BMP Entity Bean a sa persistance implémentée par le fournisseur de l'Entreprise Bean. Le fournisseur d'Entity Bean a en charge d'implémenter la logique nécessaire pour créer un nouvel EJB, mettre à jour certains attributs des EJB, supprimer un EJB et trouver un EJB dans le stockage persistance. Cela nécessite habituellement d'écrire du code JDBC pour interagir avec une base de données ou un autre stockage persistant. Avec la gestion de persistance par le Bean (BMP), le développeur a le contrôle total de la manière dont la persistance de l'Entity Bean est réalisée.

Le principe de BMP apporte aussi de la flexibilité là où l'implémentation en CMP n'est pas possible, par exemple si vous souhaitez créer un EJB qui encapsule du code d'un système mainframe existant, vous pouvez écrire votre persistance en utilisant CORBA.

XVII-G-6. Développer un Enterprise Java Bean

Nous allons maintenant implémenter l'exemple de Perfect Time de la précédente section à propos de RMI sous forme d'un composant Entreprise JavaBean. Cet exemple est un simple Session Bean non persistant. Les composants Enterprise JavaBean représenteront toujours au moins à une classe et deux interfaces.

La première interface définie est l'interface Remote de notre composant Enterprise JavaBean. Lorsque vous créez votre interface Remote de votre EJB, vous devez suivre ces règles :

  1. L'interface Remote doit être public ;
  2. L'interface Remote doit hériter de l'interface javax.ejb.EJBObject ;
  3. Chaque méthode de l'interface Remote doit déclarer java.rmi.RemoteException dans sa section throws en addition des exceptions spécifiques à l'application ;
  4. Chaque objet passé en argument ou retourné par valeur (soit directement soit encapsulé dans un objet local) doit être un type de donnée valide pour RMI-IIOP (ce qui inclut les autres objets EJB).

Voici l'interface Remote plutôt simple de notre EJB PerfectTime :

 
Sélectionnez
//: c15:ejb:PerfectTime.java
//# Vous devez installer le J2EE Java Enterprise 
//# Edition de java.sun.com et ajouter j2ee.jar
//# à votre CLASSPATH pour pouvoir compiler
//# ce fichier. Pour plus de détails, 
//# reportez vous au site java.sun.com.
//# Interface Remote de PerfectTimeBean
import java.rmi.*;
import javax.ejb.*;

public interface PerfectTime extends EJBObject {
  public long getPerfectTime()
    throws RemoteException;
} ///:~

La seconde interface définie est l'interface Home de notre composant Enterprise JavaBeans. L'interface Home est la Factory du composant que vous allez créer. L'interface Home peut définir des méthodes de création ou de recherche. Les méthodes de création créent les instances des EJB, les méthodes de recherche localisent les EJB existants et sont utilisés pour les Entity Beans seulement. Lorsque vous créez votre interface Home d'un EJB, vous devez respecter ces quelques règles :

  1. L'interface Home doit être public ;
  2. L'interface Home doit hériter de l'interface javax.ejb.EJBHome ;
  3. Chaque méthode de l'interface Home doit déclarer java.rmi.RemoteException dans sa section throws de même que javax.ejb.CreateException ;
  4. La valeur retournée par une méthode de création doit être une interface Remote ;
  5. La valeur retournée par une méthode de recherche (pour les Entity Beans uniquement) doit être une interface Remote, java.util.Enumeration ou java.util.Collection ;
  6. Chaque objet passé en argument ou retourné par valeur (sous directement ou encapsulé dans un objet local) doit être un type de donnée valide pour RMI-IIOP (ce qui inclut les autres objets EJB).

La convention standard de nommage des interfaces Home consiste à prendre le nom de l'interface Remote et d'y ajouter à la fin Home. Voici l'interface Home de notre EJB PerfectTime :

 
Sélectionnez
//: c15:ejb:PerfectTimeHome.java
// Interface Home de PerfectTimeBean.
import java.rmi.*;
import javax.ejb.*;

public interface PerfectTimeHome extends EJBHome {
  public PerfectTime create()
    throws CreateException, RemoteException;
} ///:~

Maintenant que nous avons défini les interfaces de notre composant, nous pouvons implémenter la logique métier qu'il y a derrière. Lorsque vous créez la classe d'implémentation de votre EJB, vous devez suivre ces règles (notez que vous pourrez trouver dans la spécification des EJB la liste complète des règles de développement des Entreprise JavaBeans) :

  1. La classe doit être public ;
  2. La classe doit implémenter une interface (soit javax.ejb.SessionBean, soit javax.ejb.EntityBean) ;
  3. La classe doit définir les méthodes correspondant aux méthodes de l'interface Remote. Notez que la classe n'implémente pas l'interface Remote, c'est le miroir des méthodes de l'interface Remote, mais elle n'émet pas java.rmi.RemoteException ;
  4. Définir une ou plusieurs méthodes ejbCreate( ) qui initialisent votre EJB ;
  5. La valeur retournée et les arguments de toutes les méthodes doivent être des types de données compatibles avec RMI-IIOP.
 
Sélectionnez
//: c15:ejb:PerfectTimeBean.java
// Un Session Bean non-persistant
// qui retourne l'heure système courante.
import java.rmi.*;
import javax.ejb.*;

public class PerfectTimeBean
  implements SessionBean {
  private SessionContext sessionContext;
  // retourne l'heure courante
  public long getPerfectTime() {
     return System.currentTimeMillis();
  }
  // méthodes EJB
  public void
  ejbCreate() throws CreateException {}
  public void ejbRemove() {}
  public void ejbActivate() {}
  public void ejbPassivate() {}
  public void
  setSessionContext(SessionContext ctx) {
    sessionContext = ctx;
  }
}///:~

Notez que les méthodes EJB (ejbCreate( ), ejbRemove( ), ejbActivate( ), ejbPassivate( )) sont toutes vides. Ces méthodes sont appelées par le conteneur d'EJB et sont utilisées pour contrôler l'état de votre composant. Comme c'est un exemple simple, nous pouvons les laisser vides. La méthode setSessionContext( ) transmet un objet javax.ejb.SessionContext qui contient les informations concernant le contexte dans lequel se trouve le composant, telles que la transaction courante et des informations de sécurité.

Après avoir créé notre Enterprise JavaBean, nous avons maintenant besoin de créer le descripteur de déploiement. Dans les EJB 1.1, le descripteur de déploiement est un fichier XML qui décrit le composant EJB. Le descripteur de déploiement doit être stocké dans un fichier appelé ejb-jar.xml.

 
Sélectionnez
<?xml version="1.0" encoding="Cp1252"?>
<!DOCTYPE ejb-jar PUBLIC '-//Sun Microsystems, Inc.//DTD Enterprise JavaBeans 1.1//EN' 'http://java.sun.com/j2ee/dtds/ejb-jar_1_1.dtd'>

<ejb-jar>
  <description>Exemple pour le titre XVII</description>
  <display-name></display-name>
  <small-icon></small-icon>
  <large-icon></large-icon>
  <enterprise-beans>
    <session>
      <ejb-name>PerfectTime</ejb-name>
      <home>PerfectTimeHome</home>
      <remote>PerfectTime</remote>
      <ejb-class>PerfectTimeBean</ejb-class>
      <session-type>Stateless</session-type>
      <transaction-type>Container</transaction-type>
    </session>
  </enterprise-beans>
  <ejb-client-jar></ejb-client-jar>
</ejb-jar>

Dans la balise <session> de votre descripteur de déploiement, vous pouvez voir que le composant, les interfaces Remote et Home sont définis en partie. Les descripteurs de déploiement peuvent facilement être générés automatiquement grâce à des outils tels que JBuilder.

Par l'intermédiaire de ce descripteur de déploiement standard ejb-jar.xml, la spécification des EJB 1.1 institue que toutes balises spécifiques aux fournisseurs doivent être stockées dans un fichier séparé. Ceci a pour but d'assurer une compatibilité complète entre les composants et les conteneurs d'EJB de différentes marques.

Maintenant que nous avons créé notre composant et défini sa composition dans le descripteur de déploiement, nous devons alors archiver les fichiers dans un fichier archive Java (JAR). Le descripteur de déploiement doit être placé dans le sous-répertoire META-INF du fichier Jar.

Une fois que nous avons défini notre composant EJB dans le descripteur de déploiement, le déployeur doit maintenant déployer le composant EJB dans le conteneur d'EJB. À ce moment-là du développement, le processus est plutôt orienté IHM et spécifique à chaque conteneur d'EJB. Nous avons donc décidé de ne pas documenter entièrement le processus de déploiement dans cette présentation. Chaque conteneur d'EJB propose cependant un processus détaillé pour déployer un EJB.

Puisqu'un composant EJB est un objet distribué, le processus de déploiement doit créer certains stubs clients pour appeler le composant EJB. Ces classes seront placées dans le classpath de l'application cliente. Puisque les composants EJB sont implémentés par-dessus RMI-IIOP (CORBA) ou RMI-JRMP, les stubs générés peuvent varier entre les conteneurs d'EJB, néanmoins ce sont des classes générées.

Lorsqu'un programme client désire invoquer un EJB, il doit rechercher le composant EJB dans JNDI et obtenir une référence vers l'interface Home du composant EJB. L'interface HOME peut alors être invoquée pour créer une instance de l'EJB, qui peut à son tour être invoquée.

Dans cet exemple le programme client est un simple programme Java, mais vous devez garder en mémoire qu'il pourrait s'agir aussi bien d'un Servlet, d'un JSP que d'un objet distribué CORBA ou RMI.

Le code de PerfectTimeClient code est le suivant.

 
Sélectionnez
//: c15:ejb:PerfectTimeClient.java
// Programme Client pour PerfectTimeBean

public class PerfectTimeClient {
public static void
main(String[ « args) throws Exception {
  // Obtient un context JNDI utilisant le
  // service de nommage JNDI :
  javax.naming.Context context =    new javax.naming.InitialContext();
  // Recherche l'interface Home dans le 
  // service de nommage JNDI : 
  Object ref = context.lookup("perfectTime");
  // Transforme l'objet distant en une interface Home :
  PerfectTimeHome home = (PerfectTimeHome)
    javax.rmi.PortableRemoteObject.narrow(
      ref, PerfectTimeHome.class);
  // Crée un objet distant depuis l'interface Home :
  PerfectTime pt = home.create();
  // Invoque  getPerfectTime()
  System.out.println(
    "Perfect Time EJB invoked, time is: " +
    pt.getPerfectTime() );
  }
} ///:~

Le déroulement de cet exemple est expliqué dans les commentaires. Notez l'utilisation de la méthode narrow() pour réaliser une sorte de transtypage de l'objet avant qu'un transtypage Java soit fait. Ceci est très similaire à ce qui se passe en CORBA. Notez aussi que l'objet Home devient une Factory pour les objets PerfectTimes.

XVII-G-7. En résumé

La spécification des Enterprise JavaBeans est un pas important vers la standardisation et la simplification de l'informatique distribuée. C'est une pièce majeure de Java 2, Enterprise Edition Platform et reçoit en plus le concours la communauté travaillant sur les objets distribués. De nombreux outils sont actuellement disponibles ou le seront dans un futur proche pour accélérer le développement de composants EJB.

Cette présentation avait pour but de donner un bref aperçu de ce que sont les EJB. Pour plus d'informations à propos de la spécification des Enterprise JavaBeans, vous pouvez vous reporter à la page officielle des Enterprise JavaBeans à l'adresse http://java.sun.com/products/ejb/. Vous pourrez y télécharger la dernière spécification ainsi que Java 2, Enterprise Edition Reference Implementation, qui vous permettra de développer et de déployer vos propres composants EJB.

XVII-H. Jini : services distribués

Cette section (62) donne un aperçu de la technologie Jini de Sun Microsystem. Elle décrit quelques spécificités Jini et montre comment l'architecture Jini aide à augmenter le niveau d'abstraction dans la programmation de systèmes distribués, transformant réellement la programmation réseau en programmation orientée objet.

XVII-H-1. Contexte de Jini

Traditionnellement, les systèmes d'exploitation sont conçus dans l'hypothèse qu'un ordinateur aura un processeur, un peu de mémoire et un disque. Lorsque vous démarrez votre ordinateur, la première chose qu'il fait est de chercher un disque. S'il ne le trouve pas, il ne peut assurer sa fonction d'ordinateur. Cependant de plus en plus les ordinateurs apparaissent sous diverses formes : comme des systèmes embarqués qui possèdent un processeur, un peu de mémoire et une connexion réseau, mais pas de disque. La première chose que fait, par exemple, un téléphone cellulaire lorsqu'il s'allume est de rechercher le réseau téléphonique. S'il ne le trouve pas, il ne peut assurer sa fonction de téléphone. Cette nouvelle mode dans l'environnement matériel, le passage d'un système centré sur un disque à un système centré sur un réseau, va affecter la manière d'organiser notre logiciel. C'est là qu'intervient Jini.

Jini est une manière de repenser l'architecture de l'ordinateur, étant donné l'importance croissante des réseaux et la prolifération des processeurs dans des systèmes qui n'ont pas de disque dur. Ces systèmes, qui proviennent de nombreux fabricants différents, vont avoir besoin d'interagir à travers le réseau. Le réseau lui-même sera très dynamique : les systèmes et les services seront ajoutés et retirés régulièrement. Jini apporte les mécanismes permettant facilement l'ajout, la suppression et la recherche de systèmes et de services sur le réseau. De plus, Jini propose un modèle de programmation qui rend tout cela plus facile pour les programmeurs qui souhaitent voir leurs systèmes discuter entre eux.

S'appuyant sur Java, la sérialisation objet et RMI (qui permet aux objets de bouger à travers le réseau en passant d'une machine virtuelle à une autre), Jini permet d'étendre les bénéfices de la programmation orientée objet au réseau. Au lieu de nécessiter un accord entre les différents fabricants sur un protocole réseau à travers lequel les systèmes peuvent interagir, Jini permet à ces systèmes de discuter ensemble par l'intermédiaire d'interfaces vers des objets.

XVII-H-2. Qu'est-ce que Jini ?

Jini est un ensemble d'API et de protocoles réseau qui peuvent vous aider à construire et déployer des systèmes distribués qui sont organisés sous la forme de fédérations de services. Un service peut être n'importe quoi qui se trouve sur le réseau et qui est prêt à réaliser une fonction utile. Des composants matériels, logiciels, des canaux de communications, les utilisateurs eux-mêmes peuvent être des services. Une imprimante compatible Jini pourra offrir un service d'impression. Une fédération de services est un ensemble de services, actuellement disponibles sur le réseau, que le client (ce qui signifie programme, service ou utilisateur) peut combiner pour s'aider à atteindre son but.

Pour réaliser une tâche, un client enchaîne les possibilités des services. Par exemple, un programme client peut charger des photographies d'un système de stockage d'images d'un appareil numérique, envoyer les photos vers un service de stockage persistant offert par un disque dur, et transmettre une page contenant les vignettes de chaque image à un service d'impression d'une imprimante couleur. Dans cet exemple, le programme client construit un système distribué constitué de lui-même, le service de stockage d'images, le service de stockage persistant et le service d'impression couleur. Le client et ces services de ce système distribué collaborent pour réaliser une tâche : décharger et stocker les images d'un appareil numérique et imprimer une page de vignettes.

L'idée derrière le mot fédération est que la vision Jini d'un réseau n'instaure pas d'autorité de contrôle centrale. Puisqu'aucun service n'est responsable, l'ensemble de tous les services disponibles sur le réseau forme une fédération, un groupe composé de membres égaux. Au lieu d'une autorité centrale, l'infrastructure d'exécution de Jini propose un moyen pour les clients et les services de se trouver mutuellement (à travers un service de recherche, qui stocke la liste des services disponibles à un moment donné). Après que les services se sont trouvés, ils sont indépendants. Le client et ces services mis à contribution réalisent leurs tâches indépendamment de l'infrastructure d'exécution de Jini. Si le service de recherche Jini tombe, tous les systèmes distribués mis en place par le service de recherche, avant qu'il ne plante, peuvent continuer les travaux. Jini incorpore même un protocole réseau qui permet aux clients de trouver les services en l'absence d'un service de nommage.

XVII-H-3. Comment fonctionne Jini

Jini définit une infrastructure d'exécution qui réside sur le réseau et met à disposition des mécanismes qui vous permettent d'ajouter, d'enlever, de localiser et d'accéder aux services. L'infrastructure d'exécution se situe à trois endroits : dans les services de recherche qui sont sur le réseau, au niveau des fournisseurs de services (tels que les systèmes supportant Jini), et dans les clients. Les services de recherche forment le mécanisme centralisé d'organisation des systèmes basés sur Jini. Lorsque des services deviennent disponibles sur le réseau, ils s'enregistrent eux-mêmes grâce à un service de recherche. Lorsque des clients souhaitent localiser un service pour être assistés dans leur travail, ils consultent le service de recherche.

L'infrastructure d'exécution utilise un protocole au niveau réseau, appelé discovery (découverte), et deux protocoles au niveau objet appelés join (joindre) et lookup (recherche). Discovery permet aux clients et aux services de trouver les services de recherche. Join permet au service de s'enregistrer lui-même auprès du service de recherche. Lookup permet à un client de rechercher des services qui peuvent l'aider à atteindre ses objectifs.

XVII-H-4. Le processus de découverte

Le processus de découverte travaille ainsi : imaginez un disque supportant Jini et offrant un service de stockage persistant. Dès que le disque est connecté au réseau, il diffuse une annonce de présence en envoyant un paquet multicast sur un port déterminé. Dans l'annonce de présence, sont inclus une adresse IP et un numéro de port où le disque peut être contacté par le service de recherche.

Les services de recherche scrutent sur le port déterminé la présence des paquets d'annonces. Lorsqu'un service de recherche reçoit une annonce de présence, il l'ouvre et inspecte le paquet. Le paquet contient les informations qui permettent au service de recherche de déterminer s'il doit ou non contacter l'expéditeur de ce paquet. Si tel est le cas, il contacte directement l'expéditeur en établissant une connexion TCP à l'adresse IP et sur le numéro de port extraits du paquet. En utilisant RMI, le service de recherche envoie à l'initiateur du paquet un objet appelé un enregistreur de service (service registrar). L'objectif de cet enregistreur de service est de faciliter la communication future avec le service de recherche. Dans le cas d'un disque dur, le service de recherche établirait une connexion TCP vers le disque dur et lui enverrait un enregistreur de service, grâce auquel le disque dur pourra faire enregistrer son service de stockage persistant par le processus de jonction.

Dès lors qu'un fournisseur de service possède un enregistreur de service, le produit final du processus de découverte, il est prêt à entreprendre une jonction pour intégrer la fédération des services qui sont enregistrés auprès du service de recherche. Pour réaliser une jonction, le fournisseur de service fait appel à la méthode register( )de l' « font-weight: medium » enregistreur de service, passant en argument un objet appelé élément de service (service item), un ensemble d'objets qui décrit le service. La méthode register( ) envoie une copie de cet élément de service au service de recherche, où celui-ci sera stocké. Lorsque ceci est achevé, le fournisseur de service a fini le processus de jonction : son service est maintenant enregistré auprès du service de recherche.

L'élément de service contient plusieurs objets, parmi lesquels un objet appelé un objet service, que les clients utilisent pour interagir avec le service. L'élément de service peut aussi inclure un certain nombre d'attributs, qui peuvent être n'importe quel objet. Certains de ces attributs sont des icônes, des classes qui fournissent des interfaces graphiques pour le service et des objets apportant plus de détails sur le service.

Les objects service implémentent généralement une ou plusieurs interfaces à travers lesquelles les clients interagissent avec le service. Par exemple, le service de recherche est un service Jini, et son objet service est un service de registre. La méthode register( ) appelée par les fournisseurs de service durant la jonction est déclarée dans l'interface ServiceRegistrar (un membre du package net.jini.core.lookup) que tous les services de registre implémentent. Les clients et les fournisseurs de registre discutent avec le service de recherche à travers l'objet de service de registre en invoquant les méthodes déclarées dans l'interface ServiceRegistrar. De la même manière, le disque dur fournit un objet service qui implémente l'une des interfaces connues de service de stockage. Les clients peuvent rechercher le disque dur et interagir avec celui-ci par cette interface de service de stockage.

XVII-H-5. Le processus de recherche

Une fois qu'un service a été enregistré par un service de recherche grâce au processus de jonction, ce service est utilisable par les clients qui le demandent au service de recherche. Pour construire un système distribué de services qui collaborent pour réaliser une tâche, un client doit localiser ses services et s'aider de chacun d'eux. Pour trouver un service, les clients formulent des requêtes auprès des services de recherche par l'intermédiaire d'un processus appelé recherche.

Pour réaliser une recherche, un client fait appel à la méthode lookup( ) d'un service de registre (comme un fournisseur de service, un client obtient un service de registre grâce au processus de découverte décrit précédemment). Le client passe en argument un modèle de service à lookup( ), un objet utilisé comme critère de recherche. Le modèle de service peut inclure une référence à un tableau d'objets Class. Ces objets Class indiquent au service de recherche le type Java (ou les types)) de l'objet service voulu par le client. Le modèle de service peut aussi inclure un service ID, qui identifie de manière unique le service, ainsi que des attributs, qui doivent correspondre exactement aux attributs fournis par le fournisseur de service dans l'élément de service. Le modèle de service peut aussi contenir des critères génériques pour n'importe quel attribut. Par exemple, un critère générique dans le champ service ID correspondra à n'importe quel service ID. La méthode lookup( ) envoie le modèle de service au service de recherche, qui exécute la requête et renvoie s'il y en a les objets services correspondants. Le client récupère une référence vers ces objets services comme résultat de la méthode lookup( ).

En général, un client recherche un service selon le type Java, souvent une interface. Par exemple, si un client avait besoin d'utiliser une imprimante, il pourrait créer un modèle de service qui comprend un objet Class d'une interface connue de services d'impression. Tous les services d'impression implémenteraient cette interface connue. Le service de recherche retournerait un ou plusieurs objets services qui implémentent cette interface. Les attributs peuvent être inclus dans le modèle de service pour réduire le nombre de correspondances de ce genre de recherche par type. Le client pourrait utiliser le service d'impression en invoquant sur l'objet service les méthodes définies dans l'interface.

XVII-H-6. Séparation de l'interface et de l'implémentation

L'architecture Jini met en place pour le réseau une programmation orientée objet en permettant aux services du réseau de tirer parti de l'un des fondements des objets : la séparation de l'interface et l'implémentation. Par exemple, un objet service peut permettre aux clients d'accéder au service de différentes manières. L'objet peut réellement représenter le service entier, qui sera donc téléchargé par le client lors de la recherche et exécuté localement ensuite. Autrement, l'objet service peut n'être qu'un proxy vers un serveur distant. Lorsqu'un client invoque des méthodes de l'objet service, il envoie les requêtes au serveur à travers le réseau, qui fait réellement le travail. Une troisième option consiste à partager le travail entre l'objet service local et le serveur distant.

Une conséquence importante de l'architecture Jini est que le protocole réseau utilisé pour communiquer entre l'objet service proxy et le serveur distant n'a pas besoin d'être connu du client. Comme le montre la figure suivante, le protocole réseau est une partie de l'implémentation du service. Ce protocole est une question privée prise en compte par le développeur du service. Le client peut communiquer avec l'implémentation du service à travers un protocole privée, car le service injecte un peu de son propre code (l'objet service) au sein de l'espace d'adressage du client. L'objet service ainsi injecté peut communiquer avec le service à travers RMI, CORBA, DCOM, un protocole fait maison construit sur des sockets et des flux ou n'importe quoi d'autre. Le client ne porte simplement aucune attention quant aux protocoles réseau, puisqu'il ne fait que communiquer avec l'interface publique que le service implémente. L'objet service prend en charge toutes les communications nécessaires sur le réseau.

height="38" border="0">

Le client communique avec le service à travers une interface publique

Différentes implémentations de la même interface d'un service peuvent utiliser des approches et des protocoles réseau totalement différents. Un service peut utiliser un matériel spécialisé pour répondre aux requêtes clientes, ou il peut tout réaliser de manière logicielle. En fait, le choix d'implémentation d'un même service peut évoluer dans le temps. Le client peut être sûr qu'il possède l'objet service qui comprend l'implémentation actuelle de ce service, puisque le client reçoit l'objet service (grâce au service de recherche) du fournisseur du service lui-même. Du point de vue du client, le service ressemble à une interface publique, sans qu'il ait à se soucier de l'implémentation du service.

XVII-H-7. Abstraction des systèmes distribués

Jini tente d'élever le niveau d'abstraction de la programmation de systèmes distribués, passant du niveau du protocole réseau à celui de l'interface objet. Dans l'optique d'une prolifération des systèmes embarqués connectés au réseau, beaucoup de pièces d'un système distribué pourront venir de fournisseurs différents. Jini évite aux fournisseurs de devoir se mettre d'accord sur les protocoles réseau qui permettent à leurs systèmes d'interagir. À la place, les fournisseurs doivent se mettre d'accord sur les interfaces Java à travers lesquelles leurs systèmes peuvent interagir. Les processus de découverte, de jonction et de recherche, fournis par l'infrastructure d'exécution de Jini, permettront aux systèmes de se localiser les uns les autres sur le réseau. Une fois qu'ils se sont localisés, les systèmes sont capables de communiquer entre eux à travers des interfaces Java.

XVII-I. Résumé

Avec Jini pour des réseaux de systèmes locaux, ce chapitre vous a présenté une partie (une partie seulement) des composants que Sun regroupe sous le terme de J2EE : the Java 2 Enterprise Edition. Le but de J2EE est de construire un ensemble d'outils qui permettent au développeur Java de construire des applications serveurs beaucoup plus rapidement et indépendamment de la plate-forme. Construire de telles applications n'est pas seulement difficile et coûteux en temps, mais il est particulièrement dur de les construire en faisant en sorte qu'elles puissent être facilement portées sur une autre plate-forme, et aussi que la logique métier soit isolée des détails relevant de l'implémentation. J2EE met à disposition une structure pour assister la création d'applications serveurs ; ces applications sont très demandées en ce moment, et cette demande semble grandir.

XVII-J. Exercices

On trouvera les solutions des exercices sélectionnés dans le document électronique The Thinking in Java Annotated Solution Guide, disponible pour une participation minime à www.BruceEckel.com.

  1. Compiler et lancer les programmes JabberServer et JabberClient de ce chapitre. Éditer ensuite les fichiers pour supprimer les « buffering » d'entrée et de sortie, compiler et relancer, observer le résultat.
  2. Créer un serveur qui demande un mot de passe avant d'ouvrir un fichier et de l'envoyer sur la connexion réseau. Créer un client qui se connecte à ce serveur, donne le mot de passe requis, puis capture et sauve le fichier. Tester la paire de programmes sur votre machine en utilisant localhost (l'adresse IP de boucle locale 127.0.0.1 obtenue en appelant InetAddress.getByName(null)).
  3. Modifier le serveur de l' exercice 2 afin qu'il utilise le multithreading pour servir plusieurs clients.
  4. Modifier JabberClient.java afin qu'il n'y ait pas de vidage du tampon de sortie, observer le résultat.
  5. Modifier MultiJabberServer afin qu'il utilise la technique de « surveillance de thread » « thread pooling ». Au lieu que le thread se termine lorsqu'un client se déconnecte, il intègre de lui-même un « pool de threads disponibles ». Lorsqu'un nouveau client demande à se connecter, le serveur cherche d'abord dans le pool un thread existant capable de traiter la demande, et s'il n'en trouve pas, en crée un. De cette manière le nombre de threads nécessaires va grossir naturellement jusqu'à la quantité maximale nécessaire. L'intérêt du « thread pooling » est d'éviter l'overhead engendré par la création et la destruction d'un nouveau thread pour chaque client.
  6. À partir de ShowHTML.java, créer une applet qui fournisse un accès protégé par mot de passe à un sous-ensemble particulier de votre site Web.
  7. Modifier CIDCreateTables.java afin qu'il lise les chaînes SQL depuis un fichier texte plutôt que depuis CIDSQL.
  8. Configurer votre système afin d'exécuter avec succès CIDCreateTables.java et LoadDB.java.
  9. Modifier ServletsRule.java en surchargeant la méthode destroy( ) afin qu'elle sauvegarde la valeur de i dans un fichier, et la méthode init( ) pour qu'elle restaure cette valeur. Montrer que cela fonctionne en rechargeant le conteneur de servlet. Si vous ne possédez pas de conteneur de servlet, il vous faudra télécharger, installer, et exécuter Tomcat depuis jakarta.apache.org afin de travailler avec les servlets.
  10. Créer une servlet qui ajoute un cookie à l'objet réponse, lequel sera stocké sur le site client. Ajouter à la servlet le code qui récupère et affiche le cookie. Si vous n'avez pas de conteneur de servlet, il vous faudra télécharger, installer, et exécuter Tomcat depuis jakarta.apache.org afin de travailler avec les servlets.
  11. Créer une servlet utilisant un objet Session stockant l'information de session de votre choix. Dans la même servlet, récupérer et afficher cette information de session. Si vous ne possédez pas de conteneur de servlet, il vous faudra télécharger, installer, et exécuter Tomcat depuis jakarta.apache.org afin de travailler avec les servlets.
  12. Créer une servlet qui change la valeur de « inactive interval » de l'objet session pour la valeur 5 secondes en appelant getMaxInactiveInterval( ). Tester pour voir si la session se termine naturellement après 5 secondes. Si vous n'avez pas de conteneur de servlet, il vous faudra télécharger, installer, et exécuter Tomcat depuis jakarta.apache.org afin de travailler avec les servlets.
  13. Créer une page JSP qui imprime une ligne de texte en utilisant le tag <H1>. Générer la couleur de ce texte aléatoirement, au moyen du code Java inclus dans la page JSP. Si vous ne possédez pas de conteneur JSP, il vous nombres de 128 octetsfaudra télécharger, installer, et exécuter Tomcat depuis jakarta.apache.org afin de travailler avec JSP.
  14. Modifier la date d'expiration dans Cookies.jsp et observer l'effet avec deux navigateurs différents. Constater également les différences entre le fait de visiter à nouveau la même page, et celui de fermer puis rouvrir le navigateur. Si vous ne possédez pas de conteneur JSP, il vous faudra télécharger, installer, et exécuter Tomcat depuis jakarta.apache.org afin de travailler avec JSP.
  15. Créer une page JSP contenant un champ autorisant l'utilisateur à définir l'heure de fin de session ainsi qu'un second champ contenant les données stockées dans cette session. Le bouton de soumission rafraîchit la page, prend les valeurs courantes de l'heure de fin et les données de la session, et les garde en tant que valeurs par défaut des champs susmentionnés. Si vous ne possédez pas de conteneur JSP, il vous faudra télécharger, installer, et exécuter Tomcat depuis jakarta.apache.org afin de travailler avec JSP.
  16. (Encore plus difficile). Modifier le programme VLookup.java de telle manière qu'un clic sur le nom résultat copie automatiquement ce nom dans le presse-papier (ce qui vous permet de le coller facilement dans votre e-mail). Vous aurez peut-être besoin de revenir en arrière sur le titre XV pour vous remémorer l'utilisation du presse-papier dans les JFC.

précédentsommairesuivant
Cela signifie un nombre maximum légèrement supérieur à quatre milliards, ce qui s'avère rapidement insuffisant. Le nouveau standard des adresses IP utilisera un nombre représenté sur 128 bits, ce qui devrait fournir suffisamment d'adresses IP uniques pour le futur prévisible.
Créé par Dave Bartlett.
Dave Bartlett participa activement à ce développement, ainsi qu'à la section JSP.
Beaucoup de neurones sont morts après une atroce agonie pour découvrir cette information.
Cette section a été réalisée par Robert Castaneda, avec l'aide de Dave Bartlett.
Cette section a été réalisée par Bill Venners (www.artima.com).

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Bruce Eckel. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.