Tutoriel sur l'utilisation de l'API Java RMI avec le protocole IIOP

Jean-Philippe MINETTI

RMI (Remote Method Invocation) est une API Java basée sur les ORB (Object Request Broker) qui permet l'invocation d'objets distants entre processus. Nous verrons comment utiliser RMI pour le rendre compatible avec le protocole IIOP et permettre ainsi des appels entre applications basées sur l'architecture CORBA et développées avec d'autres langages que Java.

Dans un premier temps, nous ferons une petite présentation de RMI. Puis nous verrons comment mettre en pratique RMI au niveau développement et mise en exploitation.

Commentez ♪

- Titre : Tutoriel sur l'utilisation de l'API Java RMI avec le protocole IIOP

- Auteur : Jean-Philippe MINETTI

- Parution : 14 mai 2013

- Public visé :

- Licence : Copyright ® 2013 Jean-Philippe MINETTI. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.

Sommaire

- I. Présentation

- II. Mise en pratique

- III. Création d'objets distants

- IV. Publication d'un objet avec JRMP

- V. Le RMI Registry

- VI. Accès à un objet distant avec JRMP

- VII. Compatibilité IIOP

- VIII. Choix de l'architecture logicielle

- IX. Industrialisation avec Maven

- X. Mise en exploitation

- XI. Conclusion

- XII. Remerciements

I - Présentation

L'API RMI (Remote Method Invocation) permet d'exécuter des instructions à travers le réseau. Elle met en œuvre tous les mécanismes nécessaires pour que deux applications puissent communiquer entre elles au travers d'objets Java. Ainsi, il sera possible d'appeler un objet instancié dans une autre application et pouvant même se trouver sur une autre machine, ceci de manière totalement transparente.

RMI est l'une des solutions les plus simples pour communiquer à travers un réseau. Néanmoins, cette solution peut s'avérer compliquée à partir du moment où il faut traverser des pare-feux. Dans ce cas, et bien qu'il existe des solutions pour contourner ce problème, la solution des Web Services est mieux adaptée.

Par défaut, RMI utilise le protocole JRMP (Java Remote Method Protocol) qui ne marche qu'entre des applications écrites en Java. Mais si l'on veut communiquer entre des applications écrites dans divers langages, il faut utiliser le protocole IIOP (Internet InterORB Protocol) qui est le protocole utilisé par CORBA (Common Object Request Broker Architecture). Il faut bien garder à l'esprit que l'utilisation du protocole JRMP est beaucoup plus simple que le protocole IIOP.

Au niveau du développeur, RMI rend complètement transparent toute la problématique des échanges entre processus. Ça a son avantage, mais aussi son inconvénient surtout lorsqu'on rencontre des problèmes réseau.

En principe, nous allons avoir trois acteurs :

- le RMI Registry (pour JRMP) ou le démon ORB (pour IIOP) ;

- une application jouant le rôle de serveur ;

- une application jouant le rôle de client.

Le RMI Registry ou le démon ORB sont des applications (livrées avec Java) permettant de fournir un service réseau (sur le port TCP 1099 pour le RMI Registry) chargé d'aiguiller chaque requête RMI vers le bon objet distant.

La première étape, consiste pour l'application serveur de publier (rebind) un objet distant auprès du RMI Registry ou du démon ORB :

Avant la publication d'un objet (rebind), l'application serveur doit rendre l'objet distant (exportObject). C'est lors de cette opération qu'un numéro de port TCP va être assigné à l'objet. Chaque objet distant aura son propre numéro de port. Par défaut, il est choisi aléatoirement rendant ainsi le passage à travers les pare-feux impossible. Mais avec JRMP, il est possible de fixer ce numéro de port.

Il est recommandé d'enregistrer auprès du RMI Registry ou du démon ORB (rebind) uniquement des fabriques (factory). Ainsi on aurait qu'une seule fabrique par application qui donnerait accès à tous les autres objets distants.

L'étape suivante consiste pour l'application cliente à appeler une méthode de l'objet distant :

Pour cela, l'application cliente interroge le RMI Registry ou le démon ORB pour obtenir le service à appeler (lookup). Puis, elle appelle la méthode de l'objet distant situé sur l'application serveur au numéro de port correspondant.

Rien n'empêche d'avoir deux applications qui communiquent entre elles en jouant chacune le rôle de serveur et de client. Ce type d'architecture est tout de même à proscrire.

Pour éviter d'avoir des applications qui soient en même temps client et serveur, il est possible d'utiliser le pattern Observateur. Le client envoie au serveur un observateur, préalablement rendu distant (exportObject). Pour envoyer des informations au client, le serveur utilisera l'observateur. Cette solution est beaucoup plus propre, même si au niveau des flux réseau ça ne change pas grand-chose.

Dans le cas où sur une machine il n'y a qu'une seule application serveur qui utilise le RMI Registry, il serait peut-être intéressant de l'intégrer dans l'application serveur.

Pour intégrer RMI dans des applications, il faut :

- voir la segmentation en plusieurs fichiers JAR ;

- modifier le fichier POM de Maven pour intégrer les spécificités liées à RMI, et plus particulièrement au protocole IIOP ;

- si nécessaire, ajouter du code pour intégrer le RMI Registry dans l'application serveur.

L'implémentation d'un objet distant suit le mode opératoire suivant :

- création de l'interface de l'objet ;

- création de la classe de l'objet avec les traitements ;

- ajout du code pour rendre l'objet distant (exportObject) ;

- si l'objet n'est pas obtenu à partir d'une fabrique distante, ajout du code pour le publier auprès du RMI Registry ou du démon ORB (rebind).

Pour appeler une méthode d'un objet distant, il faut :

- ajouter le code pour obtenir l'instance de l'objet (lookup) ;

- ajouter le code pour appeler la méthode.

II - Mise en pratique

Nous allons prendre comme exemple, un objet chargé de lire un fichier sur la machine où est hébergée l'application serveur. Dans un premier temps nous aborderons RMI avec le protocole JRMP. Puis nous verrons comment basculer vers le protocole IIOP.

- Nous allons d'abord créer notre objet distant ().

- Ensuite, nous verrons comment, avec le protocole JRMP, publier notre objet distant auprès du RMI Registry ().

- Nous parlerons ensuite du RMI Registry avec notamment les explications pour l'intégrer dans l'application serveur ().

- Ensuite, nous verrons comment, avec le protocole JRMP, obtenir une instance de notre objet distant et exécuter une de ses méthodes ().

- Nous verrons ensuite ce qu'il faut faire pour basculer du protocole JRMP vers le protocole IIOP ().

- Ensuite, nous verrons comment segmenter notre application serveur et client en plusieurs fichiers JAR ().

- Nous aborderons par la suite, et ceci uniquement pour le protocole IIOP, les spécificités à apporter au fichier POM de Maven pour prendre en compte la génération des stubs, skeletons et IDL ().

- Et enfin, nous verrons comment mettre en production notre exemple ().

III - Création d'objets distants

La mise en œuvre de RMI commence par la création d'objets distants au sein d'une application serveur. Ces objets auront :

- une partie spécification sous forme d'interface héritant de java.rmi.Remote ;

- une partie implémentation.

Par exemple :

FileInterface.java

1. import java.rmi.Remote;

2. import java.rmi.RemoteException;

4. public interface FileInterface extends Remote {

6. byte[] downloadFile (String fileName) throws RemoteException;

8. }

FileImpl.java

1. import java.io.*;

2. import java.rmi.*;

4. public class FileImpl implements FileInterface, Serializable {

6. private static final long serialVersionUID = -1150718288555769992L;

8. private String name;

10. public FileImpl (String s) throws RemoteException {

11. super();

12. name = s;

13. }

15. public byte[] downloadFile (String fileName) {

16. byte[] buffer;

17. try {

18. File file = new File(fileName);

19. buffer = new byte[(int)file.length()];

20. BufferedInputStream input = new BufferedInputStream(new FileInputStream(fileName));

21. input.read(buffer,0,buffer.length);

22. input.close();

23. }

24. catch(Exception e) {

25. System.out.println("FileImpl: "+e.getMessage());

26. e.printStackTrace();

27. buffer = null;

28. }

29. return(buffer);

30. }

32. }

Pour éviter des erreurs de type MarshalException, il ne faudra surtout pas oublier de rendre Serializable la classe de l'objet distant et d'y indiquer un serialVersionUID.

IV - Publication d'un objet avec JRMP

Une fois les objets créés, il ne reste plus qu'à les publier auprès d'un service appelé RMI Registry. Pour cela, on aura besoin des classes java.rmi.server.UnicastRemoteObject et java.rmi.Naming :

1. final Remote obj = new FileImpl("myFile.txt");

2. final Remote remoteStub = UnicastRemoteObject.exportObject (obj);

3. Naming.rebind ("//localhost/FileServer", remoteStub);

La méthode exportObject permet de rendre distant un objet et la méthode rebind permet d'enregistrer l'objet auprès du RMI Registry.

Ici, un numéro de port TCP sera assigné aléatoirement à notre objet. Chaque objet aura son numéro de port. L'assignation aléatoire de ce numéro peut devenir problématique si l'on veut traverser des pare-feux. Dans ce cas, il sera préférable de fixer le numéro de port (par exemple 19001) :

1. final Remote remoteStub = UnicastRemoteObject.exportObject (obj, 19001);

La communication RMI entre processus est toujours problématique lorsqu'il s'agit de traverser des pare-feux. Pensez à fixer un numéro de port TCP pour chacun de vos objets distants. Choisissez des numéros qui se suivent pour faciliter la configuration des pare-feux. N'oubliez pas aussi d'ouvrir un port TCP pour le RMI Registry (1099 par défaut).

Si le nom de l'objet (« FileServer » dans notre exemple), passé dans la méthode rebind, est déjà utilisé, une erreur de type AlreadyBoundException se produit.

Pour ne plus publier un objet distant :

1. Naming.unbind ("//localhost/FileServer");

Pensez à envoyer des traces dans vos logs à chaque fois que des objets sont publiés auprès du RMI Registry. Ce dernier étant avare d'information, vos logs seront très précieux en cas de problème lors de la mise en production.

V - Le RMI Registry

Avant de lancer notre application serveur (ou toute application publiant des objets distants via RMI), il faudra s'assurer que le RMI Registry soit bien lancé :

$JAVA_HOME/bin/rmiregistry

Le RMI Registry ne redonne pas la main une fois lancé. Pour l'arrêter, il suffit d'interrompre le processus par l'appui simultané des touches Ctrl + C.

Il est possible de lancer le RMI Registry sur un autre numéro de port :

$JAVA_HOME/bin/rmiregistry 18001

Dans ce cas précis, il faudra indiquer le bon numéro de port dans les URL lors de chaque connexion au RMI Registry :

1. Naming.rebind ("//localhost:18001/FileServer", remoteStub);

Dans un système avec une seule application serveur, il serait peut-être intéressant d'embarquer le RMI Registry dans notre application. Pour cela, il suffit de créer une instance de java.rmi.registry.Registry à l'aide de la classe java.rmi.registry.LocateRegistry :

1. final Registry registry = LocateRegistry.createRegistry (18001);

Ici, notre RMI Registry embarqué dans notre application écoute sur le port TCP 18001.

Pour arrêter le RMI Registry :

1. UnicastRemoteObject.unexportObject (registry, true);

VI - Accès à un objet distant avec JRMP

Une application cliente pourra obtenir une instance de notre objet distant :

1. final FileInterface obj = (FileInterface) Naming.lookup ("//localhost/FileServer");

et exécuter des méthodes de cet objet :

1. final byte[] filedata = obj.downloadFile (fileName);

Pour que cela fonctionne, notre application cliente devra avoir dans son CLASSPATH un JAR contenant l'interface FileInterface sans son implémentation (classe FileImpl).

Comme pour la publication d'objet, il est intéressant d'envoyer des traces dans vos logs à chaque fois que des instances sont demandées auprès du RMI Registry. Vos logs seront très utiles en cas de problème lors de la mise en production.

VII - Compatibilité IIOP

Pour basculer vers le protocole IIOP :

- au lieu d'utiliser un UnicastRemoteObject, on utilisera un PortableRemoteObject ;

- au lieu d'utiliser un java.rmi.Naming, on utilisera un javax.naming.InitialContext.

Ainsi, la publication d'un objet distant ressemblera à ceci :

1. final Properties properties = new Properties ();

2. properties.setProperty ("java.naming.factory.initial", "com.sun.jndi.cosnaming.CNCtxFactory");

3. properties.setProperty ("java.naming.provider.url", "iiop://localhost:1050");

4. final Context namingContext = new InitialContext (properties);

5. final Remote obj = new FileImpl("myFile.txt");

6. PortableRemoteObject.exportObject (obj);

7. namingContext.rebind ("org.minetti.test,type=FileServer", obj);

Avec IIOP, il n'est pas possible de fixer les numéros de port TCP pour chaque objet distant.

Pour ne plus publier un objet distant :

1. namingContext.unbind ("org.minetti.test,type=FileServer");

2. PortableRemoteObject.unexportObject (obj);

Pour qu'une application cliente puisse obtenir une instance de notre objet distant :

1. final Properties properties = new Properties ();

2. properties.setProperty ("java.naming.factory.initial", "com.sun.jndi.cosnaming.CNCtxFactory");

3. properties.setProperty ("java.naming.provider.url", "iiop://localhost:1050");

4. final Context namingContext = new InitialContext (properties);

5. final Object objref = namingContext.lookup ("org.minetti.test,type=FileServer");

6. final FileInterface obj = (FileInterface) PortableRemoteObject.narrow (objref, FileInterface.class);

Avec IIOP, il est impératif de générer le skeleton et le stub :

$JAVA_HOME/bin/rmic -iiop -verbose FileImpl

Dans notre exemple, cette commande va générer les classes suivantes :

- _FileInterface_Stub.class pour le stub à insérer dans le JAR de l'application cliente et serveur ;

- _FileImpl_Tie.class pour le skeleton à insérer dans le JAR de l'application serveur.

Si l'on veut développer des applications clientes dans d'autres langages, il faut générer les IDL de toutes les classes utiles pour nos communications distantes :

$JAVA_HOME/bin/rmic -idl FileImpl

Au lieu d'utiliser le RMI Registry, on utilisera un démon ORB par exemple orbd, fourni avec Java :

$JAVA_HOME/bin/orbd -ORBInitialPort 1050 -ORBInitialHost localhost

VIII - Choix de l'architecture logicielle

D'une manière générale, il est intéressant de segmenter notre application cliente et serveur en trois fichiers JAR :

- un fichier JAR commun qui se trouvera référencé dans le CLASSPATH de l'application cliente et serveur ;

- un fichier JAR (ou WAR dans le cas d'une application web) pour l'application serveur ;

- un fichier JAR (ou WAR dans le cas d'une application web) pour l'application cliente.

On mettra dans le fichier JAR commun :

- les interfaces de tous les objets distants.

Dans le fichier JAR (ou WAR) de l'application serveur, on mettra :

- les classes des objets distants (implémentation) ;

- les stubs des objets distants (uniquement avec IIOP) ;

- les skeletons des objets distants (uniquement avec IIOP).

Et enfin, on mettra dans le fichier JAR (ou WAR) de l'application cliente :

- les stubs des objets distants (uniquement avec IIOP).

On aurait pu mettre les stubs dans le fichier JAR commun. On verra par la suite (dans le ) qu'il est plus simple de générer les stubs dans les JAR des applications.

Au niveau de l'application cliente, il faudra éviter de mettre dans le CLASSPATH le JAR de l'application serveur. Le client ne doit pas avoir accès à l'implémentation des objets distants. Cela nous évitera d'avoir des accès directs à nos objets.

IX - Industrialisation avec Maven

Maven est un outil permettant d'industrialiser la création des différents JAR. Nous n'aborderons pas ici toute la configuration de Maven, mais uniquement les spécificités liées à RMI et plus particulièrement au protocole IIOP.

Pour en savoir plus sur Maven :

- Introduction à Maven 2 ;

- Utiliser Maven 2.

Pour le protocole JRMP, il n'y a aucune difficulté à paramétrer Maven puisqu'il n'y a pas de commande particulière à lancer lors de la fabrication des JAR.

Pour le protocole IIOP, c'est une autre histoire puisqu'il faut générer les stubs, les skeletons et les IDL. Au niveau du projet de l'application serveur, on ajoutera ceci dans le fichier pom.xml :

pom.xml

&#8230;
<!-- RMIC executor -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>rmic-maven-plugin</artifactId>
<version>1.1</version>
<executions>
<execution>
<id>rmic-process-classes</id>
<goals>
<goal>rmic</goal>
</goals>
<configuration>
<iiop>true</iiop>
<includes>
<include>org/minetti/test/rmi/FileImpl.class</include>
</includes>
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
</configuration>
</execution>
<execution>
<id>rmic-process-idls</id>
<goals>
<goal>rmic</goal>
</goals>
<configuration>
<idl>true</idl>
<includes>
<include>org/minetti/test/rmi/FileImpl.class</include>
</includes>
<outputDirectory>${project.build.directory}/idls</outputDirectory>
</configuration>
</execution>
<execution>
<id>stub-package</id>
<goals>
<goal>package</goal>
</goals>
<configuration>
<classifier>stub</classifier>
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>

<!-- Application & IDLs assembling -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.3</version>
<executions>
<execution>
<id>make-idls-assembly</id>
<phase>install</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptors>
<descriptor>idl.xml</descriptor>
</descriptors>
<finalName>TestServer-idl-${project.version}</finalName>
</configuration>
</execution>
</executions>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<outputDirectory>${project.build.directory}</outputDirectory>
</configuration>
</plugin>
...

idl.xml

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">

<id>idl</id>

<formats>
<format>tar.gz</format>
<format>zip</format>
</formats>

<fileSets>
<fileSet>
<directory>${project.build.directory}/idls</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>

</assembly>

L'exécution « rmic-process-classes » permet de créer le stub et le skeleton de la classe org.minetti.test.rmi.FileImpl.

L'exécution « rmic-process-idls » permet de créer tous les IDL et les range dans le répertoire « idls ».

L'exécution « stub-package » permet de créer un fichier JAR avec le classifier « stub » et contenant tous les stubs générés par « rmic-process-classes ».

Le dernier plugin (« maven-assembly-plugin ») permet de générer un fichier zip et tar.gz contenant tous les IDL générés par « rmic-process-idls ».

Au niveau du projet de l'application cliente, on ajoutera ceci dans le fichier pom.xml :

pom.xml

&#8230;
<!-- Stub classes importation & Packages installation -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.minetti</groupId>
<artifactId>TestServer</artifactId>
<version>${project.version}</version>
<type>jar</type>
<classifier>stub</classifier>
<outputDirectory>${project.build.outputDirectory}</outputDirectory>
<includes>**/_*_Stub.class</includes>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
&#8230;

Ce plugin va nous permettre de récupérer le fichier JAR avec le classifier « stub » et contenant tous les stubs pour les intégrer dans le JAR de l'application cliente. Pour que ça marche, il faudra bien sûr que le JAR de l'application serveur soit généré avant celui de l'application cliente.

X - Mise en exploitation

Avant de lancer les différentes applications, il est impératif de lancer le RMI Registry s'il n'est pas embarqué dans une des applications (voir ) ou le démon ORB de votre choix si vos applications communiquent avec le protocole IIOP :

JRMP

rmiregistry

IIOP

orbd -ORBInitialPort 1050 -ORBInitialHost localhost

Le RMI Registry ou le démon ORB doit se trouver obligatoirement sur la même machine que le ou les applications serveurs.

Si les numéros de port TCP ont été fixés pour chaque objet distant, le passage au travers des pare-feux ne posera pas de problème. Autrement, il sera très difficile de traverser le moindre pare-feu, car les numéros de port TCP seront alloués aléatoirement.

Ce problème peut être contourné par l'utilisation de « GIOP proxy » avec le protocole IIOP ou de « RMI proxy » avec le protocole JRMP.

Ensuite, il faudra lancer la ou les applications publiant des objets distants.

En cas de problème, le RMI Registry étant peu bavard, il faudra se référer aux logs des applications serveur et client. D'où la nécessité d'accorder une attention toute particulière aux traces envoyées dans les logs lors de la publication d'objets distants et de leurs accès.

Si le RMI Registry n'a pas été démarré ou qu'il est impossible de l'atteindre, une erreur de type NotBoundException se produit lors de la publication des objets distants. Pour le démon ORB, on aura une erreur de type CommunicationException.

Et enfin, on lancera en dernier toutes les applications accédant aux objets distants.

Après une mise en production, une erreur de type MarshalException peut se produire lors d'une tentative d'accès à un objet distant. Cela peut provenir d'un oubli d'une sérialisation d'une classe, mais aussi d'une mauvaise construction des JAR. L'interface RMI a dû changer et l'une des applications n'a pas été mise à jour.

XI - Conclusion

La mise en œuvre de RMI est relativement simple. Les choses se compliquent un peu lorsqu'il s'agit de rendre nos communications entre processus compatibles avec le protocole IIOP. Dans ce cas, il faut générer le skeleton, le stub, et éventuellement les classes au format IDL. Mais la compatibilité IIOP permet la communication entre applications construites dans des langages autres que Java.

Avant de commencer le développement, il faudra réfléchir sur l'architecture réseau. Bien se poser la question si les flux réseau doivent traverser des pare-feux.

Aussi, il faudra se poser la question si des applications seront écrites dans d'autres langages.

XII - Remerciements

Je tiens à remercier keulkeul, ram-0000, gueritarish et nemek pour leur relecture technique, ainsi que f-leb pour sa relecture orthographique.