Remote Method Invocation - RMI

Chapitres traités   


Objet distant

Ce chapitre va nous permettre de nous familiariser avec la technologie des objets distribués. Il s'agit d'objets répartis sur le réseau local situés sur des machines différentes. Il faut bien comprendre, que ces objets répartis (une fois installés) demeurent sur leurs ordinateurs respectifs. Lorsque nous nous adressons à un de ces objets situé sur une autre machine que celle sur laquelle nous travaillons, nous dialoguons avec lui à distance (il ne se déplace pas), d'où le terme utilisé : "Objet distant".

Système client-serveur sans l'utilisation des objets distants

Revenons un instant sur le concept qui consiste à réunir des informations sur un ordinateur client et à les envoyer à un serveur sur le réseau, à l'image par exemple d'un serveur Web. Un utilisateur sur une machine locale va remplir un formulaire de requêtes d'informations. Les données de ce formulaire sont envoyées au serveur du vendeur, qui traite la requête et envoie la réponse au client, qui peut l'afficher.

Dans le modèle traditionnel client-serveur, la requête est traduite dans un format intermédiaire (comme les paires nom/valeur ou des données XML). Le serveur analyse le format de la requête, calcule la réponse et la formate avant de la transmettre au client. Puis le client analyse la réponse obtenue et l'affiche pour l'utilisateur.

Dans ce contexte, même si vos données sont structurées, il faudra faire face à un problème de code particulièrement délicat : vous devrez trouvez des techniques appropriées pour traduires ces données en fonction du format de transmission. En fait, il s'agit systématiquement de mettre en oeuvre un protocole de communication, éventuellement au dessus du protocole existant, entre le client et le serveur.

 

Intérêt des objets distants

Ne pourrions-nous pas trouver une solution beaucoup plus facile à mettre en oeuvre sans se tracasser sur le protocole de communication entre deux machines sur le réseau ?

En partant du principe que la programmation orientée objet est déjà fondée sur des envois de requêtes entre plusieurs objets placés sur une même machine, nous pourrions déplacer ces objets sur différents ordinateurs, de sorte qu'ils puissent s'envoyer directement des messages à distance. Nous remarquons que les objets distants s'utilisent de la même façon que les objets qui se trouvent sur la même machine. Du coup, il devient très facile de programmer des systèmes client-serveur par la technologie des objets distribués (d'un point de vue utilisateur, la technique interne est par contre relativement sophistiquée).

Avec cette technique, nous restons dans la phylosophie objet et lorsqu'un objet communique avec un objet distant, c'est comme si cet objet se trouvait sur le même ordinateur.

 

Choix du chapitre RMI - Remote Method Invocation

Le but de RMI (invocation de méthode distante) est de permettre l'appel, l'exécution et le renvoi du résultat d'une méthode exécutée dans une machine virtuelle différente de celle de l'objet l'appelant. Cette machine virtuelle peut être sur une machine différente pourvu qu'elle soit accessible par le réseau. La machine sur laquelle s'exécute la méthode distante est appelée serveur. L'appel coté client d'une telle méthode est un peu plus compliqué que l'appel d'une méthode d'un objet local mais il reste simple. Il consiste à obtenir une référence sur l'objet distant puis à simplement appeler la méthode à partir de cette référence. La technologie RMI se charge de rendre transparente la localisation de l'objet distant, son appel et le renvoi du résultat.

Le mécanisme RMI vous permet donc de faire une chose qui paraît simple au premier abord. Si vous avez accès à un objet d'une machine distante, vous pouvez appeler des méthodes de cet objet distant. Naturellement, les paramètres de ces méthodes doivent être fournis d'une manière ou d'une autre à l'ordinateur distant, l'objet doit être informé de la méthode à exécuter et la valeur de retour doit être envoyé correctement. RMI permet de gérer tous ces détails automatiquement.

 

Stubs et encodage des paramètres

Lorsque le code d'un client veut invoquer une méthode distante sur un objet distant, il commence en fait par appeler une méthode ordinaire du langage de programmation Java, qui est encapsulée dans un objet de substitut appelé un stub. Ce stub se trouve sur la machine cliente et non sur le serveur. Il est un représentant de l'objet distant. Il rassemble tous les paramètres utilisés par la méthode distante dans un bloc d'octets, ce qui lui permet d'obtenir pour chaque paramètre utilisé un codage indépendant de la machine.

L'intérêt de l'encodage des paramètres est de convertir les paramètres en un format adapté (sérialisation des objets le cas échéant) à leur transport entre plusieurs machines virtuelles. Ce mécanisme intermédiaire assure automatiquement le protocole entre les différents intervenants.

Pour résumer, la méthode du stub sur le client construit un bloc de données composé de :

  1. un identificateur de l'objet distant à utiliser ;
  2. une description de la méthode à appeler ;
  3. les paramètres encodés.

Le stub renvoie alors ces informations au serveur. Du côté du serveur, un objet de réception effectue les actions suivantes pour chaque appel de méthode distante :

  1. Il décode les paramètres encodés.
  2. Il situe l'objet à appeler.
  3. Il appelle la méthode spécifiée.
  4. Il capture et encode la valeur de retour (ou l'exception renvoyée par l'appel).
  5. Il envoie un bloc de données correspondant à la valeur de retour encodée au stub du client.

Le stub du client décode la valeur de retour ou l'exception du serveur. Cette valeur devient la valeur de retour de l'appel au stub. Cependant, si la méthode distante a déclenché une exception, le stub la renvoie à l'appelant.

Ce processus est évidemment assez complexe, mais la bonne nouvelle est qu'il est entièrement automatique, et de manière plus générale, complètement transparent pour le programmeur. De plus, les concepteurs des objets Java distants ont fait de leur mieux pour que ceux-ci aient le même comportement que les objets locaux.

Ces stubs existent mais ne sont pas visible par le programmeur. En fait, le programmeur implémente ces sources comme si les stubs n'existaient pas. Tout le mécanisme de communication est caché et la programmation avec un objet distant devient alors très facile à mettre en oeuvre.

Si vous utilisez la JDK à partir de la version 5.0, tout se fait automatiquement. Par contre, si vous utilisez la JDK 1.4 ou antérieur, vous devez utiliser un utilitaire qui est fourni avec, qui s'appelle rmic et qui permet de fabriquer automatiquement les stubs de vos objets distants. En effet, après avoir compilé vos fichier sources Java, il suffit d'exécuter cet utilitaire rmic sur les classes d'objets distants dans une deuxième passe et le tour est joué.

 

Interface


Lorsque le client décide d'utiliser un objet distant, il doit connaître les méthodes qui lui sont autorisées.
.

En général, un objet s'occupe lui même de son propre état (attributs privés) et cela reste vrai, à plus forte raison, pour un objet distant. Il autorise cependant aux autres objets d'utiliser ces propres méthodes publiques.

Dans le cas d'un objet distant, le client n'a pas directement connaissance de la façon d'utiliser cet objet. Tous les clients désireux de ce connecter à cet objet doivent donc disposer d'un représentant qui leurs indique la façon de procéder, c'est-à-dire, donnant la liste des méthodes acceptées. N'oublions pas, d'ailleurs, que RMI veut dire : Invocation de méthodes distantes.

Java dispose pour cela d'un outil que nous connaissons déjà bien et qui correspond totalement à ce critère. Il s'agit de l'interface.
.

Notre programme client doit pouvoir manipuler des objets de serveur, mais il ne dispose pas en réalité de leurs copies. Les objets eux-mêmes résident sur le serveur. Le code du client doit en permanence savoir ce qu'il est en mesure de faire avec ces objets. Leurs caractéristiques sont détaillées dans une interface partagée entre le client et le serveur, interface qui se trouve par conséquent sur chacune des deux machines.

Les objets distants sont donc des objets qui implémentent une interface distante spéciale, précisant quelles méthodes de l'objet peuvent être invoquées à distance. L'interface distante doit être créée explicitement et doit étendre l'interface java.rmi.Remote. Votre objet distant implémentera son interface distante ; comme le fera l'objet stub automatiquement généré pour lui.

Dans le code côté client, vous devez désormais faire référence à l'objet distant en tant qu'instance de l'interface distante - pas en temps qu'instance de sa classe réelle.

Toutes les méthodes de l'interface distante doivent déclarer qu'elles peuvent lever l'exception java.rmi.RemoteException. Celle-ci est levée lorsqu'une erreur quelconque du réseau se produit : par exemple, panne du serveur, défaillance du réseau ou requête sur un objet indisponible.

Etant donné que le client aura finalement accès à l'interface plutôt qu'à l'objet distant avec lequel il communique, il est judiceux que le nom de l'interface soit justement Personne. Il existe une certaine convention quant aux noms des éléments qui doivent être implémentés ou utilisés, mais ceci n'a pas un caractère obligatoire :

  1. pas de suffixe (Personne) : Interface distante.
  2. suffixe Impl (PersonneImpl) : La classe de l'objet distant sur le serveur qui implémente cette interface.
  3. suffixe Server (PersonneServer) : Service qui crée les objets distants sur le serveur, les enregistre sur un annuaire (registre RMI) et attend l'invocation des différents clients.

Pour la JDK 1.4 ou antérieur : suffixe Stub (PersonneImpl_Stub) : Une classe de stub générée automatiquement par le programme rmic. Cette classe est déployée chez le client et permet de résoudre tout le protocôle nécessaire afin que la communication demeure simple pour le client. Le client agit comme si l'objet était sur sa propre machine.

 

Choix du chapitre Développement côté serveur

Le développement côté serveur se compose de :

  1. La définition d'une interface qui contient les méthodes qui peuvent être appelées à distance.
  2. L'écriture d'une classe qui implémente cette interface, cette classe donne la définition de ou des objets distants.
  3. Enfin, vous allez écrire une classe qui instanciera l'objet (création de l'objet distant) et l'enregistrera en lui affectant un nom dans un annuaire spécialisé qui s'appelle le registre de nom RMI (RMI Registry). Cette classe correspond au service mettant en oeuvre l'objet distant. Ce service sera donc en attente des connexions extérieures afin que l'objet distant puisse rendre service aux différents clients éventuels.

Pour la JDK 1.4 ou antérieur : il faut générer la classe stub en exécutant le programme rmic avec les fichiers compilés de l'interface et de la classe représentant l'objet distant.

 

L'interface distante

L'interface à définir doit hériter de l'interface java.rmi.Remote. Cette interface Remote ne contient aucune méthode mais indique simplement que l'interface peut être appelée à distance. L'interface doit contenir toutes les méthodes qui seront succeptibles d'être appelées à distance.

 

L'écriture d'une classe qui implémente l'interface distante


Cette classe correspond à l'objet distant. Elle doit donc implémenter l'interface distante, c'est-à-dire contenir le code nécessaire pour chacune des méthodes à redéfinir déclarées par cette dernière. Cette classe doit donc respecter le contrat donnée par l'interface afin que les clients obtiennent les réponses voulues.

L'implémentation d'un objet distant étendra généralement java.rmi.server.UnicastRemoteObject. Il s'agit de l'équivalent RMI de la classe Object classique. Lorsqu'une classe fille de UnicastRemoteObject est construite, le système d'exécution RMI l'exporte automatiquement pour commencer l'écoute des connexions réseaux afin de permettre la communication entre les objets de serveur et leurs stubs distants.

Vous pouvez considérer que cette classe qui implémente l'interface distante est un serveur pour les méthodes distantes parce qu'elle étend UnicastRemoteObject, qui est une classe concrète de la plateforme Java et qui rend les objets accessibles à distance.

Cette classe doit donc implémenter toutes les méthodes définies par l'interface. Comme pour l'interface, toutes les méthodes distantes doivent indiquer qu'elles peuvent lever l'exception RemoteException mais aussi le constructeur de la classe. Ainsi, même si le constructeur ne contient pas de code il doit être redéfini pour inhiber la génération du constructeur par défaut qui ne lève pas cette exception.

Cette classe peut comporter autant de méthodes supplémentaires que nécessaire ; la plupart d'entre elles seront probablement private, mais cela n'est pas rigoureusement nécessaire.

Par moment, vous ne souhaiterez pas utiliser directement une classe qui hérite de la classe UnicastRemoteObject, parce qu'il est possible que la classe qui doit implémenter l'interface distante étende déjà une autre classe. Dans ce cas, il convient d'instancier manuellement les objets du serveur et de les transmettre à la méthode statique exportObject de la classe UnicastRemoteObject. En reprenant le même nom de classe ci-dessus, sans l'héritage à UnicastRemoteObject :

Personne objetDistant = new PersonneImpl("REMY", "Emmanuel", 45);
UnicastRemoteObject.exportObject(objetDistant, 0);

 

Création de la classe Stub pour la JDK 1.4 ou antérieur (cette partie est inutile à partir de la JDK 5.0)

Depuis la JDK 5.0, toutes les classes de stub sont générées automatiquement sans l'utilisation de l'utilitaire rmic. Si vous avez une version antérieure, vous devez utiliser cette outil, comme cela vous est précisé dans cette rubrique.

Vous devez générer maintenant le stub correspondant à la classe PersonneImpl. Rappelez-vous que le stub est une classe qui encode et qui envoie des paramètres et des valeurs de résultat sur le réseau. Les programmeurs ne se servent jamais directement de cette classe. L'outil rmic permet de la générer automatiquement, comme cela vous est montré ci-dessous :

Attention, il est nécessaire que les fichiers sources aient été compilés au préalable.
.

Cette classe stub doit ensuite être déployée chez le client avec son propre programme ainsi qu'avec l'interface distante. L'objet relatif à cette classe stub sera ensuite créé lorsque le client en fera la demande (indirectement puisqu'il demande en fait d'établir une communication avec l'objet distant). Il est donc important que la définition de la classe soit bien localisée chez le client afin que l'objet soit correctement créé.

 

Le registre RMI (localisation des objets distants), création et utilisation de l'objet distant

Le registre est l'annuaire téléphonique de RMI. Nous l'utilisons pour rechercher une référence à un objet distant référencé sur une machine autre que celle du client. Préalablement, il faut que la base de registre RMI soit lancée au moyen du programme rmiregistry.

Nous devons maintenant lancer un service qui aura deux rôles essentiels. Son premier but sera de créer tous les objets distants afin de les atteindre et ensuite de les enregistrer auprès de la base de registre RMI. L'enregistrement de vos objets distants dans le registre s'effectue avec la classe InitialContext représentée par l'interface Context.

Une fois que la base de registre est opérationnelle (le programme rmiregistry en cours d'exécution), vous pouvez effectivement enregistrer vos objets distants à l'aide de la méthode rebind() de Context en fournissant une référence vers cet objet distant et un nom (alias) qui servira à localiser cet objet distant dans le registre. Ce nom doit correspondre à une chaîne de caractères unique. Par ailleurs, cette chaîne de caractères doit posséder le suffixe "rmi:".

// côté serveur
PersonneImpl manu = new PersonneImpl("REMY", "Emmanuel", 45);
PersonneImpl gaston = new PersonneImpl("LAGAFE", "Gaston", 33);
Context nomContext = new InitialContext();
nomContext.rebind("rmi:manu", manu); // le nom dans le registre est manu
nomContext.rebind("rmi:gaston", gaston); // le nom dans le registre est gaston

// deux objets distants créés et enregistrés dans la base de registre RMI

Nous aurions pu utiliser bind() à la place, mais rebind() pose moins de problème : si l'objet distant est déjà enregistré, rebind() permet de le remplacer avec la nouvelle version éventuelle. Ceci dit, si vous enregistrer un nouvel objet distant, rien ne vous empêche d'utiliser la méthode bind() en lieu et place.

Le programme côté client, toujours par l'intermédiaire de Context et de sa méthode spécifique lookup(), peut alors obtenir un objet stub qui va permettre d'accéder à l'objet distant avec l'encodage de tout le protocole de communication nécessaire. Pour cela, vous devez spécifier le nom du serveur ainsi que le nom (alias) de l'objet enregistré dans la base de registre RMI :

// côté client
Context context = new InitialContext();
Personne manu = (Personne) context.lookup("rmi://Hp/manu");
Personne gaston = (Personne) context.lookup("rmi://Hp/gaston");

Les URL de RMI commencent par rmi:// et sont suivies par le nom du serveur, un numéro de port optionnel, un autre slash et le nom de l'objet distant.

rmi://hôte_local:99/objet_distant

Par défaut, le port de RMI est 1099.
.

Le registre RMI sert uniquement d'annuaire et permet de localiser l'objet distant que le client désire. A partir de là, l'objet stub est créé chez le client, ce qui permet d'établir les communications (avec gestion automatique de protocole) avec l'objet distant qui se trouve dans le service lancé côté serveur.

méthodes javax.naming.InitialContext
InitialContext( ) Construit un contexte de nom pouvant être utilisé pour accéder au registre RMI.
méthodes javax.naming.Context
static Object lookup(String nom) Renvoie l'objet pour le nom spécifié en argument. Déclenche une NamingException si le nom n'est pas lié actuellement.
static void bind(String nom, Object objet) Lie le nom à l'objet. Déclenche une NameAlreadyBoundException si l'objet est déjà lié.
static void unbind(String nom) Délie le nom. Vous pouvez délier un nom qui n'existe pas.
static void rebind(String nom, Object objet) Lie le nom à l'objet. Remplace toute liaison existante.
NamingEnumeration<NameClassPair> list(String nom) Renvoie une énumaration répertoriant tous les objets liés concordants. Pour énumérer tous les objets RMI, utilisez "rmi:".
méthodes javax.naming.NamingEnumeration<T>
boolean asMore() Renvoie true si cette énumération possède d'autres éléments.
T next()
Renvoie l'élément suivant de cette énumération.
méthodes javax.naming.NameClassPair
String getName()
Récupère le nom de l'objet nommé.
String getClassName()
Récupère le nom de la classe à laquelle appartient l'objet nommé.

 

Mise en place du service d'objets distants

Nous allons mettre en oeuvre maintenant le service d'objets distants. Vous devez :

  1. Compiler tous vos sources.
  2. Stocker l'ensemble de vos fichier compilés dans un même répertoire qui servira à la base de registres RMI.
  3. Lancer l'application concernant la base de registre : rmiregistry.
  4. Lancer le (ou les services) permettant de créer, d'enregistrer et d'utiliser vos différents objets distants.

Le deuxième point est important. Il est impératif que l'application rmiregistry puisse localiser les différents fichiers ".class" mis en jeu. Soit vous régler la variable d'environnement CLASSPATH, soit vous placez cette application rmiregistry au même endroit où se situent l'ensemble de vos classes.

Du coup, il peut être intéressant d'avoir un ordinateur qui serve de serveurs d'objets distants. Comme pour les autres serveurs, il est souvent nécessaire d'avoir un répertoire pour localiser l'ensemble des éléments qui servent à la mise en place de ces objets distants. Il pourrait alors être judicieux de proposer un paquetage par type d'objet distant afin d'éviter que les classes soient toutes au même niveau. Par contre, n'oubliez pas de placer l'application rmiregistry dans ce répertoire particulier.

Il faut lancer les services d'enregistrement des objets distants qu'après avoir lancer l'application rmiregistry pour qu'effectivement cette dernière puisse enregistrer les différents noms dans son annuaire.

Nous allons donc construire le service qui permettra de mettre en oeuvre les objets distants désirés. Il faut bien comprendre qu'il s'agit d'un véritable service, c'est-à-dire qu'il est constamment actif une fois qu'il est démarré. C'est en fait lui qui active les objets distants afin qu'ils soient disponibles pour les différents clients. Pour cela, trois étapes sont nécessaires :

  1. Création des objets distants,
  2. Enregistrement de ces objets distants dans le service d'annuaire RMI,
  3. Attente pour rendre service aux différents clients.

Voici un exemple de service mettant en place deux objets distants de type Personne.

PersonneServer.java
import java.net.MalformedURLException;
import java.rmi.*;
import java.rmi.server.*;
import javax.naming.*;
/**
 * Ce programme de serveur crée (instancie) deux objets distants,
 * les enregistre auprès du service de nom et attend les clients
 * pour invoquer les méthodessur les objets distants
 */
public class PersonneServer {
   public static void main(String[] args) throws Exception {
      System.out.println("Création des objets distants...");
      PersonneImpl manu = new PersonneImpl("REMY", "Emmanuel", 45);
      PersonneImpl gaston = new PersonneImpl("LAGAFE", "Gaston", 33);
      
      System.out.println("Enregistrement au service d'annuaire");
      Context nomContext = new InitialContext();
      nomContext.rebind("rmi:manu", manu);
      nomContext.rebind("rmi:gaston", gaston); 
      
      System.out.println("Attente des invocations des clients...");
   }
}

Le programme ne se terminera pas de façon normale. Cela peut sembler étrange, après tout ce programme se contente de créer deux objets et de les enregistrer. En fait, la fonction main() prend fin immédiatement après la fin de l'enregistrement, comme vous pouvez vous y attendre. Mais si vous créez un objet à partir d'une classe qui étend UnicastRemoteObject, un nouveau thread est lancé pour garder le programme activé indéfiniment. Par conséquent, le programme ne se termine pas pour permettre aux clients de s'y connecter.

Une fois que ce programme est compilé, pour le rendre actif, vous devez dans l'ordre exécuter les commandes suivantes :

  1. start rmiregistry
  2. start java PersonneServer

 

Choix du chapitre Développement côté client

Le programme côté client devient extrêmement simple. Il doit juste respecter les critères suivant :

  1. L'obtention d'une référence sur l'objet distant (en réalité le stub qui est le représentant de l'objet distant) à partir de son nom au travers de l'interface Personne et par l'intermédiare de la classe InitialContext.
  2. L'appel aux méthodes autorisées définies par l'interface Personne à partir de cette référence.

Voici ci-dessous un exemple de programme client qui exploite les deux objets distants situés sur une autre machine référencés sur le service d'annuaire RMI respectivement par manu et gaston.

PersonneClient.java
import java.net.MalformedURLException;
import java.rmi.*;
import java.rmi.server.*;
import javax.naming.*;
/**
 * Un client qui se connecte à la machine HP et utilise
 * les deux objets distants "manu" et "gaston".
 */
public class PersonneClient {
   public static void main(String[] args) throws Exception {
      Context context = new InitialContext();
      Personne manu = (Personne) context.lookup("rmi://HP/manu");
      System.out.println("Nom de manu : "+ manu.getNom());
      System.out.println("Prénom de manu"+ manu.getPrenom());
      System.out.println("Age de manu"+ manu.getAge());
      
      Personne gaston = (Personne) context.lookup("rmi://HP/gaston");
      System.out.println("Nom de gaston : "+ gaston.getNom());
      System.out.println("Prénom de gaston"+ gaston.getPrenom());
      System.out.println("Age de gaston"+ gaston.getAge());
   }
}

 

Choix du chapitre Synthèse et déploiement

Tout ce que nous venons de voir peut paraître compliquer pour la fabrication et la mise en place des objets distants. J'ai préféré prendre le temps pour bien expliquer et visualiser tous les mécanismes qui servent à cette technique relativement sophistiquée. En regardant de plus près, nous remarquons cependant que le développeur aura en réalité très peu de code à écrire.

Rappelons ce que le développeur devra construire :

  1. L'interface (par exemple Personne) qui doit hériter de l'interface Remote où nous devons juste indiquer les méthodes utilisable à distance,
  2. La classe qui servira pour instancier le ou les objets distants (par exemple PersonneImpl). Cette classe doit hériter de la classe UnicastRemoteObject et surtout implémenter l'interface qui vient juste d'être définie. A ce sujet, il est impératif, bien sûr, que chacune des méthodes présentent dans l'interface soit définie afin qu'elle puisse rendre le service désiré.
  3. Le service (par exemple PersonneServer) qui permet de mettre en place le ou les objets distants et qui se sert donc de la classe que nous venons de construire. Ces objets sont ensuite référencés par le service d'annuaire qui doit être au préalable actif par l'intermédiaire de rmiregistry. Ce service d'annuaire sert au client pour localiser le ou les objets distants.
  4. Le ou les programmes clients (par exemple PersonneClient) qui exploiterons ces objets distants.

Côté serveur

Finalement, le serveur devra disposer des fichiers suivant pour que l'objet distant puisse fonctionner :

  1. Personne.class.
  2. PersonneImpl.class.
  3. PersonneServer.class : c'est cette dernière classe qui devra être exécutée. Par contre, il faut lancer avant rmiregistry.

Ces classes devront se situer au même endroit que l'utilitaire rmiregistry.
.

Côté client

Ensuite, le développeur pourra s'occuper de fabriquer tous les programmes clients qui exploiterons ce ou ces objets distants. Il doit juste utiliser l'interface qui représente l'objet distant. Les fichiers sur le client seront donc :

  1. PersonneClient.class
  2. Personne.class

 

Choix du chapitre Conclusion

Cela vaut t-il le coup de développer des objets distants ?

Oui, puisque nous avons un fonctionnement client-serveur sans se tracasser sur la mise en oeuvre fastidieuse d'un protocole de communication. Surtout, nous utilisons l'objet distant comme s'il se trouvait sur notre propre machine. En fait, nous restons constamment dans la phylosophie orientée objet. Les temps de conception s'en trouvent alors relativement réduits.

Pour conclure, et surtout côté client, le développement et l'utilisation des objets distants demeurent une technologie intuitive, très facile et rapide à mettre en oeuvre.

L'exemple que nous venons de voir est très modeste. Le but était de montrer les différents concepts et les mécanismes mis en jeu. Cependant, lorsque nous mettons en oeuvre des objets distants, c'est normalement pour obtenir des applications beaucoup plus conséquentes. L'intérêt de ce système, c'est d'avoir un serveur qui disposent d'objets qui s'occupent de tout le processus métier. Ainsi, vu que les objets restent à distance, l'ordinateur client a besoin de très peu de ressources. De gros traitements peuvent néanmoins se réaliser, mais ceux-ci se font sur le serveur. Le client lance juste quelques méthodes qui correspondent aux requêtes souhaitées.

 

Choix du chapitre Déploiement de classes distantes

Quelquefois, les clients d'un réseau local peuvent avoir besoin d'une application plus conséquente sur leur ordinateur avec notamment une IHM très sophistiquée. La première solution consiste à placer cette application sur chacun des ordinateurs du réseau local. Cette solution est fastidieuse, et pose ensuite des problèmes de mise à jour. Par ailleurs, chaque ordinateur est encombré de cette application, même s'il n'en a pas besoin pour l'instant.

 

Déploiement de la classe distante sur chacun des ordinateurs du réseau

Une autre solution consiste à conserver cette application sur le serveur et de la déployer juste le temps nécessaire afin de rendre le service désiré. Une fois que le client a fini son travail, l'application n'existe plus sur son poste. Cette solution est intéressante, puisque l'application n'existe en permanence uniquement que sur le serveur. Seuls les clients qui en ont momentanément besoin en dispose également. Si vous avez besoin de faire une mise à jour de cette application, il suffit de le faire une seule fois, sur une seule machine, c'est-à-dire sur le serveur.

 

Déploiement par la technologie RMI

Dans ce système, nous avons en fait un mécanisme de déploiement automatique que nous allons mettre en oeuvre au moyen de la technologie RMI. Cette fois-ci, toutefois, nous n'avons pas besoin de mettre en place un objet distant. En effet, l'objet est créé directement sur le poste local.

Par contre, Il faut récupérer la classe distante afin de la placer sur notre ordinateur local pour que l'objet correspondant soit effectivement créé, ce qui permettra ensuite de lancer l'application désirée.

Vu que nous n'utilisons pas d'objet distants, nous n'avons pas besoin d'avoir la base de registre RMI, et donc il ne sera pas nécessaire de lancer le programme rmiregistry.

 

Choix de l'interface de communication

Puisque nous utilisons la technologie RMI, nous devons tout de même passer par une interface pour dialoguer avec le système distant. Nous pouvons en créer une. Toutefois, il me paraît plus judicieux d'utiliser une interface qui existe déjà dans la JVM, qui sera donc présente sur chacun des postes du réseau local.

Du coup, il ne sera plus nécessaire de déployer cette interface.
.

Une interface qui me paraît adaptée à ce genre de situation est l'interface Runnable. En effet, cette interface représente un thread et dispose d'une seule déclaration qui est la méthode run(). Dans cette méthode, vous indiquez l'ensemble des instructions que devra réaliser ce thread.

public interface Runnable {
   public void run();
}

Or toutes les applications, qu'elles soient graphiques ou pas, dispose d'au moins un thread qui est le thread courant représentant le processus courant et qui s'exprime à l'intérieur de la méthode main().

Ainsi, il suffit juste pour la classe qui implémente cette interface qu'elle redéfinisse la méthode run(). Ce n'est pas une contrainte puisqu'il suffira de remplacer la méthode main() par cette méthode run() et ainsi les instructions qui seront placées dans cette dernière seront bien équivalentes aux instructions du thread courant.

 

Mise en place de deux application graphiques distantes

Pour valider ce qui vient d'être dit, je vous propose de mettre en oeuvre les petites applications graphiques ci-dessous. Elles sont très modestes, ceci afin d'éviter d'avoir trop de code, ce qui nuirait à la compréhention du sujet.

Pensez-bien que nous pouvons avoir un grand nombre d'applications avec une interface IHM beaucoup plus riche. Cela fonctionnerait de la même façon.
.

Attention, toutefois. Si votre interface IHM est composée de plusieurs classes, il faut penser à les déployer toutes, sinon seule une partie de l'application fonctionnerait correctement.

Je vous propose donc deux applications qui joue le rôle de conversion entre les €uros et les Francs.


Fenêtre correspondante

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

public class EuroFranc extends JFrame implements Runnable, ActionListener {
   private JTextField saisie = new JTextField("0");
   private JButton calcul = new JButton("€uro / Franc");
   private JLabel résultat = new JLabel("0 Francs");
   private JPanel panneau = new JPanel();
   
   public EuroFranc() {
      this.setTitle("Conversion €uro / Franc");
      this.setSize(270, 100);
      this.setLocation(100, 100);
      this.setDefaultCloseOperation(this.EXIT_ON_CLOSE);
      saisie.setColumns(12);
      saisie.setHorizontalAlignment(saisie.RIGHT);
      panneau.add(saisie);
      calcul.addActionListener(this);
      panneau.add(calcul);
      this.getContentPane().add(panneau, BorderLayout.NORTH);
      résultat.setHorizontalAlignment(résultat.CENTER);
      this.getContentPane().add(résultat, BorderLayout.SOUTH);
   }
   public void run() {
      new EuroFranc().setVisible(true);
   }

   public void actionPerformed(ActionEvent e) {
      double €uro = Double.parseDouble(saisie.getText());
      double franc = €uro*6.55957;
      résultat.setText(franc+" Francs");
   }
}

Fenêtre correspondante

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

public class FrancEuro extends JFrame implements Runnable, ActionListener {
   private JTextField saisie = new JTextField("0");
   private JButton calcul = new JButton("Franc / €uro");
   private JLabel résultat = new JLabel("0 €uros");
   private JPanel panneau = new JPanel();
   
   public FrancEuro() {
      this.setTitle("Conversion Franc / €uro");
      this.setSize(270, 100);
      this.setLocation(100, 100);
      this.setDefaultCloseOperation(this.EXIT_ON_CLOSE);
      saisie.setColumns(12);
      saisie.setHorizontalAlignment(saisie.RIGHT);
      panneau.add(saisie);
      calcul.addActionListener(this);
      panneau.add(calcul);
      this.getContentPane().add(panneau, BorderLayout.NORTH);
      résultat.setHorizontalAlignment(résultat.CENTER);
      this.getContentPane().add(résultat, BorderLayout.SOUTH);
   }
   public void run() {
      new FrancEuro().setVisible(true);
   }

   public void actionPerformed(ActionEvent e) {
      double franc = Double.parseDouble(saisie.getText());
      double €uro = franc/6.55957;
      résultat.setText(€uro+" €uros");
   }
}

 

Quel service ?

Cette question n'est pas anodine. En effet, nous disons depuis le début de ce chapitre que les applications distantes doivent être placées dans un serveur. Or pour qu'il y ait un serveur, il faut qu'il y ait au moins un service.

Attention, pour l'instant nous n'avons aucun service d'implémenté. En effet, d'une part, d'après ce que nous avons dit, rmiregistry n'a pas besoin d'être actif. Il faut dire d'ailleurs que ce service n'est utile que comme service de référencement (service d'annuaire) des objets distants en cours de fonctionnement. D'autre part, les seuls services que nous avons montés dans les chapitres précédent étaient les objets distants eux-mêmes. Or, ici nous n'en avons pas besoin. Alors comment faire ?

De toute façon, il faut nécessairement monter un service. Les solutions sont limitées puisque seuls les protocoles HTTP et FTP sont permis pour la technologie RMI. La solution fréquemment utilisée est la mise en place d'un serveur Web. Il suffit ensuite, dans votre serveur Web, de fabriquer une application Web /download/ à l'intérieur de laquelle vous placerez toutes vos classes à déployer.

web.xml
<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
         version="2.4">
</web-app>

 

Architecture d'une application Web

 

Attention, l'application Web doit respecter une certaine architecture pour qu'elle soit opérationnelle. Ceci dit, c'est très simple, il s'agit d'un ensemble de répertoires prédéfinis et d'un descripteur de déploiement <web.xml> vide (ou presque) situé dans le répertoire (privé) <WEB-INF> dont voici le codage interne ci-dessus.

Vous devez ensuite placer l'ensemble de vos classes à la racine de votre application Web. Ici le répertoire racine est appelé <web>.

Ici, je ne vais pas m'étendre beaucoup plus sur la notion d'application Web. Ce sujet est largement traité dans l'ensemble de mes cours, notamment dans la partie J2EE.

 

Récupération des classes sur le poste local

Côté serveur tout est maintenant prêt pour déployer les classes. Dès lors, côté client, nous allons fabriquer un tout petit programme qui va permettre de récupérer l'application désirée par l'utilisateur.

Il existe une classe spécialisée qui permet de télécharger une classe présente sur un serveur distant. Il s'agit de la classe RMIClassLoader. C'est une classe utilitaire, et qui comme telle, dispose uniquement de méthodes statiques. Elle est justement composée d'une méthode loadClass() qui délivre un objet de type Class. Pour cela, vous devez alors passer en argument l'URL du site distant ainsi que le nom de la classe à télécharger. Vous avez un exemple ci-dessous :

Class classe = RMIClassLoader.loadClass("http://Hp:8084/download/", "FrancEuro");

Il ne vous reste plus qu'à créer une instance de cette classe et de la transtyper vers l'interface Runnable. Cette instance va servir à créer le thread par l'intermédiaire de la classe Thread. Vous placez alors le nom de l'objet Runnable en argument du constructeur de Thread. Une fois que le thread est créé, il suffit de le lancer au moyen de la méthode start(). Dès que le thread est lancé, tout ce qui se trouve dans la méthode run() est alors exécutée en temps partagé avec d'autres threads éventuels :

Runnable client = (Runnable) classe.newInstance();
new Thread(client).start();

 

Gestionnaire de Sécurité

Lorsque toutes les classes sont disponibles en local, soit par les classes de la JVM, soit parce qu'elles sont déjà présentes sur le poste, l'exécution du programme se déroule sans problème (revoir la partie sur l'objet distant). Cependant, chaque fois que vous faites références à de nouvelles classes, comme les applications que nous créons de toutes pièces, vous devez alors les charger dynamiquement depuis le site distant. Hors, à chaque fois que vous devez charger du code à partir d'un nouvel emplacement, vous devez impérativement passer par un gestionnaire de sécurité.

Les programmes clients qui se servent de RMI pour télécharger dynamiquement de nouveaux éléments doivent donc installer un gestionnaire de sécurité. RMISecurityManager est un gestionnaire de sécurité adapté à ce genre de situation. L'instruction suivante permet de l'installer :

System.setSecurityManager(new RMISecurityManager());

 

Règles de sécurité

Par défaut, RMISecurityManager empêche n'importe quelle partie du programme d'établir des connexions réseau. Une fois que le programme client est mis en place, il faut également lui fournir une autorisation pour charger les classes désirées. Cette autorisation se traduit par un fichier de règles de sécurité. Voici le fichier de règles de sécurité <client.policy> qui donne toutes les autorisations :

grant {
   permission java.security.AllPermission;

};

Ces règles de sécurités doivent être fournies avec le programme client sur le poste local.
.

Dans le programme client, nous devons demander au gestionnaire de sécurité de lire ces règles de sécurité. Pour cela, vous devez régler la propriété java.security.policy en spécifiant quel est le nom du fichier qui comporte ces règles. Voici la procédure à suivre :

System.setProperty("java.security.policy", "client.policy");

Voici un autre exemple de règles de sécurité qui permet à une application d'effectuer n'importe quelle connexion réseau vers un port dont le numéro est supérieur ou ou égal à 1024. Rappelez-vous que le numéro de port du service RMI est 1099. Par ailleurs, cette règle stipule d'empêcher l'avertissement sous forme de bannière indiquant "Une fenêtre d'applet Java".

grant {
   permission java.net.SocketPermission "*:1024-65535", "connect";
   permission java.awt.AWTPermission "showWindowWithoutWarningBanner";
}; 

 

Programme client

Après toutes ces considérations, voici enfin notre programme client :

Client.java
import java.rmi.RMISecurityManager;
import java.rmi.server.RMIClassLoader;
import java.util.Scanner;
import javax.swing.JFrame;

public class Client {
   public static void main(String[] args) throws Exception {
      Scanner clavier = new Scanner(System.in);
      System.out.print("Application à lancer : ");
      String application = clavier.next();
      System.setProperty("java.security.policy", "client.policy");
      System.setSecurityManager(new RMISecurityManager());
      Class classe = RMIClassLoader.loadClass("http://Portable:8084/download/", application);
      Runnable client = (Runnable) classe.newInstance();
      new Thread(client).start();
   }
}

N'oubliez pas de déployer avec votre programe client, le fichier de règles de sécurité.


 

Quelques remarques

  1. Vu que nous sommes obligé d'utiliser un serveur Web, nous aurions pu nous passer de fabriquer un programme client. En effet, lorsque vous connaissez l'URL de la page d'accueil de l'application Web <download>, il suffit de cliquer sur le lien de la classe désirée et de ratacher ce nom de fichier au programme qui permet de l'éxécuter, à savoir javaw. Attention, dans ce cas là, il faut que vos classes représentent un programme, c'est-à-dire quelle comportent une méthode main() à la place de la méthode run(). Elles ne doivent donc plus implémenter l'interface Runnable.
  2. Pour le déploiement seul d'applications, il est peut être préférable d'utiliser Java Web Start.

 

Choix du chapitre Objet distant et classes distantes

Nous allons maintenant combiner les deux approches, c'est-à-dire avoir un objet distant qui joue le rôle de serveur d'applications distantes. Lorsqu'un client communique avec l'objet distant, ce dernier lui renvoie alors une fenêtre qui rescence l'ensemble des applications disponibles sur le serveur. Cette fenêtre donne la liste des applications au travers d'une ComboBox. Le client décide ensuite de l'application à lancer, en la sélectionnant dans la liste, ce qui lance l'affichage de la fenêtre correspondante. Lorsque le client clique sur un des bouton arrêt d'une des fenêtres l'ensemble de l'application est interrompue.

Ici, ne sont présentent que deux applications. Il est bien évident que nous pouvons en placer un beaucoup plus grand nombre. Le fonctionnement demeure totalement identique.

Dans ce chapitre, nous n'alllons rien apprendre de bien nouveau. C'est juste une fusion des compétences déjà acquises dans l'ensemble de cette étude.
.

Il faut bien comprendre que ces trois fenêtres ne sont pas présentes au préalable chez le client. Elles sont automatiquement déployées juste le temps nécessaire. Lorsque le client quitte une des fenêtre, l'application globale n'existe plus sur son ordinateur local.

 

Interface Application

Puisque nous devons dialoguer avec un objet distant, le seul moyen de l'atteindre est de proposer une interface de communication adaptée. Ici, l'interface se nomme Application et dispose d'une seule méthode getFenêtre() qui sert à récupérer la fenêtre "Liste des applications disponibles" :

Application.java
import java.rmi.*;
import javax.swing.JFrame;

public interface Application extends Remote {
   Class getFenêtre() throws RemoteException;
}

La méthode getFenêtre() renvoie un objet de type Class. En effet, il est plus intéressant de fabriquer la fenêtre avec le mécanisme de réflexion du côté client, plutôt que de la fabriquer dans l'objet distant et d'envoyer une copie au client par la suite. Dans ce dernier cas de figure, cela voudrait dire que si 10 clients se connectent, il y aurait 10 fenêtres présentes sur le serveur.

 

Application cliente

Avant de créer l'objet distant, je vous propose tout de suite de voir comment le client dialogue avec l'objet distant :

Client.java
import java.rmi.RMISecurityManager;
import java.rmi.server.RMIClassLoader;
import javax.naming.*;
import javax.swing.JFrame;

public class Client {
   public static void main(String[] args) throws Exception {
      System.setProperty("java.security.policy", "client.policy");
      System.setSecurityManager(new RMISecurityManager());
      
      Context context = new InitialContext();
      Application application = (Application) context.lookup("rmi://Portable/application");
      Class classe = application.getFenêtre();
      JFrame fenêtre = (JFrame) classe.newInstance();
      fenêtre.setVisible(true);
   }
}

Cette fois-ci, nous remarquons que nous n'avons pas besoin de faire appel au chargeur de classes RMIClassLoader. Effectivement, puisque nous avons un serveur qui est l'objet distant, le chargement des classes qui ne sont pas connues dans la JVM se fait automatiquement.

Attention, comme précédemment, il est quand même nécessaire d'avoir un service adaptée au déploiement des classes, c'est-à-dire un serveur Web. Par rapport, au chapitre précédent, il faut penser à rajouter la classe qui représente la fenêtre de la "Liste d'applications disponibles".

Du coup, comme précédemment, il est nécessaire de rajouter un gestionnaire de sécurité en spécifiant les règles au moyen du fichier <client.policy> :

client.policy
grant {
   permission java.security.AllPermission;
};

 

Fenêtres de l'ensemble du dispositif correspondantes aux classes à déployer

Les deux premières sont déjà connues sauf qu'elles n'ont plkus besoin d'implémenter l'interface Runnable, et donc ne disposent plus de la méthode run().


Fenêtre correspondante

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

public class EuroFranc extends JFrame implements ActionListener {
   private JTextField saisie = new JTextField("0");
   private JButton calcul = new JButton("€uro / Franc");
   private JLabel résultat = new JLabel("0 Francs");
   private JPanel panneau = new JPanel();
   
   public EuroFranc() {
      this.setTitle("Conversion €uro / Franc");
      this.setSize(270, 100);
      this.setLocation(100, 100);
      this.setDefaultCloseOperation(this.EXIT_ON_CLOSE);
      saisie.setColumns(12);
      saisie.setHorizontalAlignment(saisie.RIGHT);
      panneau.add(saisie);
      calcul.addActionListener(this);
      panneau.add(calcul);
      this.getContentPane().add(panneau, BorderLayout.NORTH);
      résultat.setHorizontalAlignment(résultat.CENTER);
      this.getContentPane().add(résultat, BorderLayout.SOUTH);
   }

   public void actionPerformed(ActionEvent e) {
      double €uro = Double.parseDouble(saisie.getText());
      double franc = €uro*6.55957;
      résultat.setText(franc+" Francs");
   }
}

Fenêtre correspondante

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

public class FrancEuro extends JFrame implements ActionListener {
   private JTextField saisie = new JTextField("0");
   private JButton calcul = new JButton("Franc / €uro");
   private JLabel résultat = new JLabel("0 €uros");
   private JPanel panneau = new JPanel();
   
   public FrancEuro() {
      this.setTitle("Conversion Franc / €uro");
      this.setSize(270, 100);
      this.setLocation(100, 100);
      this.setDefaultCloseOperation(this.EXIT_ON_CLOSE);
      saisie.setColumns(12);
      saisie.setHorizontalAlignment(saisie.RIGHT);
      panneau.add(saisie);
      calcul.addActionListener(this);
      panneau.add(calcul);
      this.getContentPane().add(panneau, BorderLayout.NORTH);
      résultat.setHorizontalAlignment(résultat.CENTER);
      this.getContentPane().add(résultat, BorderLayout.SOUTH);
   }

   public void actionPerformed(ActionEvent e) {
      double franc = Double.parseDouble(saisie.getText());
      double €uro = franc/6.55957;
      résultat.setText(€uro+" €uros");
   }
}

Fenêtre correspondante

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

public class ListeApplication extends JFrame implements ActionListener {
   public ListeApplication() {
      this.setTitle("Liste des applications disponibles");
      this.setSize(320, 100);
      this.setLocation(100, 100);
      this.setDefaultCloseOperation(this.EXIT_ON_CLOSE);
      liste.addItem("EuroFranc");
      liste.addItem("FrancEuro");
      liste.addActionListener(this);
      this.getContentPane().add(liste, BorderLayout.NORTH);
   }

   public void actionPerformed(ActionEvent e)  {
      try {
         Class classe = Class.forName((String) liste.getSelectedItem());
         JFrame fenêtre = (JFrame) classe.newInstance();
         fenêtre.setVisible(true);
     } 
      catch (Exception ex) {
         ex.printStackTrace();
      }
   }
   private JComboBox liste = new JComboBox();
}

Nous aurions pu faire quelque chose de plus sophistiqué en prososant par exemple que cette fenêtre regarde les applications présentes dans le serveur Web et les intégre automatiquement dans la ComboBox.

 

Objet distant qui joue également le rôle de serveur

Plutôt que de fabriquer d'une part la classe qui représente l'objet distant, et d'autre part le serveur qui fabrique cet objet et le référence dans le registre RMI, il me paraît plus judicieux de mettre en oeuvre une classe qui réalise l'ensemble de ces opérations.

ApplicationServer.java
 1 import java.rmi.RemoteException;
 2 import java.rmi.server.UnicastRemoteObject;
 3 import javax.naming.*;
 4 import javax.swing.JFrame;
 5 
 6 public class ApplicationServer extends UnicastRemoteObject implements Application {
 7    public ApplicationServer() throws RemoteException {
 8    }
 9    
10    public static void main(String[] args) throws NamingException, RemoteException {
11       System.out.println("Localisation des classes distantes...");
12       System.setProperty("java.rmi.server.codebase", "http://Portable:8084/download/");
13       System.out.println("Création de l'objet distant application...");
14       ApplicationServer application = new ApplicationServer();
15       System.out.println("Enregistrement au service d'annuaire");
16       Context context = new InitialContext();
17       context.rebind("rmi:application", application);      
18       System.out.println("Attente des invocations des clients...");
19    }
20    
21    public Class getFenêtre() throws RemoteException {
22       return ListeApplication.class;
23    }
24 }
25

Il s'agit bien d'un objet distant puisqu'il hérite de UnicastRemoteObject. D'autre part, la relation avec cet objet se bien au travers de l'interface Application. La méthode getFenêtre(), qui correspond bien à l'implémentation de l'interface, propose juste de renvoyer uniquement le nom de la classe ListeApplication.

La nouveauté par rapport à ce que nous avons déjà vu se trouve à la ligne 12. En effet, nous réglons la propriété java.rmi.server.codebase afin que le client sache où récupérer les classes distantes. Grâce à cette instruction, le déploiement des classes non connues de la JVM s'effectue automatiquement.

 

Répartition de l'ensemble des classes

Pour terminer, revoyons la répartion de l'ensemble de ces classes pour les différents systèmes :

  1. Objet distant :

    Généralement doit disposer de l'ensemble des classes construites dans le projet afin que le déploiement ultérieur soit plus facile à maîtriser. N'oubliez pas qu'il faut que l'outil qui gère la base de registre RMI fasse partie du même répertoire. En tout cas, voici les classes à avoir à tout prix :

    - rmiregistry.exe
    - ApplicationServer.class
    - Application.class


  2. Serveur Web :

    - ListeApplication.class
    - EuroFranc.class
    - FrancEuro.class


  3. Ordinateur Client :

    - Client.class
    - Application.class
    - client.policy