Le but de ce cours est de détailler un mécanisme de communication directement entre des objets java distants. Cette méthode nommée RMI (Remote Method Invocation) permet qu'un objet java donné puisse appeler une méthode d'un objet java situé sur une autre machine.
Aujourd'hui un serveur Web possède une architecture qui peut être complexe. Quand google me donne une réponse à ma recherche, le serveur Web qui me répond ne possède pas en local sur son disque les 4 milliards de pages. Il communique donc avec d'autres machines.
Java étant facilement utilisé dans les serveurs Web, il est dès lors important de pourvoir communiquer directement entre les objets.
Le but de cette section est de réaliser un exemple où deux objets communiquent entre eux:
Il nous faut tout d'abord définir les relations entre le panier dans lequel nous mettons les pommes et le rayon dans lequel nous les prenons.
La communication entre le client et le serveur (e.g., le panier et le rayon) est codé par deux classes constituant la remote interface. Nous mettons les classes de la remote interface dans un paquetage nommé boutique.
Voici tout d'abord la définition du rayon d'un magasin, il possède deux méthodes permettant d'ajouter ou de retirer des pommes.
package boutique; // les classes vont être définies dans boutique.*
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Rayon extends Remote {
Object ajoutePomme(ChangeRayon t) throws RemoteException;
Object retirePomme(ChangeRayon t) throws RemoteException;
}
Tout d'abord la déclaration package boutique indique que les déclarations à venir vont être faites dans le package boutique. Ainsi pour utiliser ce qui va être définit il faudra utiliser un commande import boutique.* ou bien référencer explicitement les noms.
On constate ensuite l'usage du mot clé interface au lieu de class dans la déclaration de Rayon cela signifie que Rayon n'est pas une classe dont on va pouvoir directement créer des instances en faisant new Rayon. Il faut voir l'interface comme un contrat, ainsi en écrivant l'interface Rayon il suffit de donner le prototype des méthode et non leur corps.
Voici ensuite les données que l'on fournit à l'interface boutique.Rayon, qui donne le nombre d'articles que l'on souhaite prendre :
package boutique;
import java.io.Serializable;
public interface ChangeRayon extends Serializable {
Object nombreDArticles();
}
ChangeRayon hérite de Serializable. Cela signifie que les instances de ChangeRayon peuvent être transformées en une suite de caractères permettant de sauvegarder l'objet. C'est cette sauvegarde qui va être l'information qui va circuler entre les machines.
Nous allons maintenant construire un rayon précis, le rayon fruits et légumes qui va devoir respecter l'interface précédente. Cette classe sera dans un paquetage nommé mesrayons:
package mesrayons;
import boutique.ChangeRayon;
import boutique.Rayon;
public class FruitsEtLegumes
extends java.rmi.server.UnicastRemoteObject
implements boutique.Rayon
{
int nombreDePommes=0;
public FruitsEtLegumes() throws java.rmi.RemoteException {
super();
nombreDePommes=10;
}
public Object ajoutePomme(ChangeRayon t) {
nombreDePommes=nombreDePommes
+((Integer)(t.nombreDArticles())).intValue();
System.out.println("Il y a maintenant "+nombreDePommes+" pomme(s)");
return new Integer(nombreDePommes);
}
public Object retirePomme(ChangeRayon t) {
int pommesARetirer = ((Integer)(t.nombreDArticles())).intValue();
if (nombreDePommes>=pommesARetirer)
nombreDePommes=nombreDePommes-pommesARetirer;
else
pommesARetirer=0;
System.out.println("Il y reste "+nombreDePommes+" pomme(s) en rayon");
return new Integer(pommesARetirer);
}
On remarque que le constructeur par défaut du rayon fruits et légumes met 10 pommes dans le rayon. Puis les méthodes ajoutePomme et retirePomme sont implémentées.
Nous allons ensuite rajouter une méthode main pour pouvoir créer une instance de FruitsEtLegumes :
public static void main(String[] args) {
if (System.getSecurityManager() == null) {
System.setSecurityManager
(new java.rmi.RMISecurityManager());
}
try {
Rayon monRayon = new FruitsEtLegumes();
java.rmi.Naming.rebind("maboutique", monRayon);
System.out.println("FruitsEtLegumes bound");
} catch (Exception e) {
System.err.println("FruitsEtLegumes exception: " +
e.getMessage());
e.printStackTrace();
}
}
} // fin de la classe FruitsEtLegumes
Il reste maintenant à écrire la classe panier.Panier qui va demander des pommes au rayon :
package panier;
import java.rmi.*;
import boutique.Rayon;
import boutique.ChangeRayon;
public class Panier {
public static void main(String args[]) {
if (System.getSecurityManager() == null) {
System.setSecurityManager(new RMISecurityManager());
}
try {
String name = "//" + args[0] + "/maboutique";
Rayon fruitsEtLegumes = (Rayon) Naming.lookup(name);
ChangeRayon changement = new Changement(Integer.parseInt(args[1]));
Integer pommesObtenues =
(Integer)fruitsEtLegumes.retirePomme(changement);
System.out.println("Il y a "+pommesObtenues
+" pomme(s) dans le panier.");
} catch (Exception e) {
System.err.println("Panier exception: " +
e.getMessage());
e.printStackTrace();
}
}
}
Il ne reste plus qu'à écrire une implémentation de la classe boutique.ChangeRayon, qui va simplement enrober un objet de type int :
package panier;
public class Changement implements boutique.ChangeRayon {
int monNombreDArticles=0;
Changement(int n) {
monNombreDArticles=n;
}
public Object nombreDArticles() {
return new Integer(monNombreDArticles);
}
}
Ant (http://ant.apache.org) est un programme permettant de ne pas taper toutes les informations de compilation sur la ligne de commande. Ant est utilisé par les IDE NetBeans et Eclipse (donc pas besoin de le télécharger si Netbeans ou Eclipse est déjà installé).
Un fichier Ant est un fichier XML, généralement nommé build.xml. Ce fichier est l'équivalent d'un Makefile (www.gnu.org/software/make).
On suppose que l'on part de la hiérarchie suivante:
| build.xml
| java.policy
+ src
+ boutique
| | ChangeRayon.java
| | Rayon.java
+ mesrayons
| | FruitsEtLegumes.java
+ panier
| Changement.java
| Panier.java
Un fichier ant commence par une balise projet composant la cible par défaut à exécuter (ici rmic que nous verrons plus loin):
<?xml version="1.0"?> <project name="jar" basedir="." default="rmic">
Créons ensuite une cible pour compiler toute notre arborescence située dans le dossier src
<target name="compile">
<mkdir dir="build"/> <!-- creation du repertoire -->
<javac srcdir="src" <!-- compilation des *.java -->
destdir="build"
debug="on"/>
</target>
Quand cette cible sera exécutée, le répertoire
build sera crée, puis toute l'arborescence
des fichier *.java sera copiée dans build
en étant transformée en fichier *.class.
On obtient en plus de la hiérarchie précédente :
+ build
+ classes
+ boutique
| | ChangeRayon.class
| | Rayon.class
+ mesrayons
| | FruitsEtLegumes.class
+ panier
| Changement.class
| Panier.class
Les interfaces distantes doivent être compilées en utilisant un utilitaire spécial nommé rmic.
Il peut être invoqué sur la ligne de commande par la séquence :
>rmic -v1.1 -d build/classes mesrayons.FruitsEtLegumesou de manière similaire par la cible Ant suivante :
<target name="rmic" depends="compile">
<rmic stubversion="1.1"
classname="mesrayons.FruitsEtLegumes"
base="build/classes" />
</target>
L'attribut depends="compile" sert à demander l'exécution
préalable de la cible nommée compile.
On obtient en plus de la hiérarchie précédente :
+ build
+ classes
+ mesrayons
| FruitsEtLegumes_Skel.class
| FruitsEtLegumes_Stub.class
Les classes *_Skel.class et *_Stub.class servent
à prendre en charge automatiquement la sérialisation et
la communication réseau.
L'exécution du serveur se fait par la ligne de commande suivante :
java -Djava.rmi.server.codebase=file:/chemin/vers/les/classes \
-Djava.rmi.server.hostname=localhost \
-Djava.security.policy=java.policy \
-cp build/classes mesrayons.FruitsEtLegumes
ou par la cible Ant suivante:
<target name="serveur" depends="rmic">
<java classname="mesrayons.FruitsEtLegumes" fork="true">
<sysproperty key="java.rmi.server.codebase" value="file:/chemin/vers/les/classes/"/>
<sysproperty key="java.rmi.server.hostname" value="localhost"/>
<sysproperty key="java.security.policy" value="java.policy"/>
<classpath>
<!-- rajoute un répertoire -->
<pathelement path="build/classes"/>
<!-- rajoute une archive jar -->
<pathelement location="/chemin/vers/archive.jar"/>
</classpath>
</java>
</target>
On peut également rajouter la cible ci-dessous pour effacer les fichiers générés:
<target name="clean">
<delete dir="build"/>
<delete>
<fileset dir="." includes="**/*~" defaultexcludes="no"/>
<fileset dir="." includes="**/*.jar" defaultexcludes="no"/>
</delete>
</target>
Il faut alors fermer la balise project pour clore le fichier Ant.
</project>
Pour génére le jar à partir de tous les fichiers source, il suffit de taper ant
>ant
Buildfile: build.xml
compile:
[mkdir] Created dir: /chemin/vers/build
[mkdir] Created dir: /chemin/vers/build/classes
[javac] Compiling 5 source files to /chemin/vers/build/classes
rmic:
[rmic] RMI Compiling 1 class to /chemin/vers/build/classes
BUILD SUCCESSFUL
Total time: 2 seconds
On remarque que c'est tout d'abord la cible compile
qui s'exécute, puis la cible rmic.
Si un fichier source *.java a été modifié, seul ce dernier sera compilé par une nouvelle invocation de ant. Pour effacer les fichiers générés taper ant clean.
Il faut tout d'abord lancer l'entité qui va permettre au serveur de s'enregistrer. Ceci se fait en invoquant sur la ligne de commande :
UNIX (bash):
export CLASSPATH=/chemin/vers/mes/point_class rmiregistry &
UNIX (csh):
setenv CLASSPATH /chemin/vers/mes/point_class rmiregistry &
Windows:
# fixer la variable d'environnement classpath start rmiregistry
Une autre alternative sous Unix ou Windows consiste à lançer rmiregistry dans le répertoire où sont les packages contenant les *.class.
ant serveurIl faut au préalable avoir créer le fichier java.policy dont voici un exemple possible:
grant {
permission java.net.SocketPermission "*:1024-65535",
"connect,accept,resolve";
};
ant client
Q1 Récupérer le source donné dans le cours à l'url : http://www.derepas.com/java/cours2_exercice_1.jar. Le décompresser à l'aide de la commande jar xvf cours2_exercice_1.jar. Mettre les bons chemins dans le fichier build.xml. Exécuter le client et le serveur, à l'aide de la commande ant.
Q2 On souhaite rendre l'interface boutique.Rayon plus générique. Créer une classe boutique.Article qui contient simplement une chaine de caractères. Transformer les méthodes de boutique.Rayon :
Object ajoutePomme(ChangeRayon t) throws RemoteException;
Object retirePomme(ChangeRayon t) throws RemoteException;
En :
Object ajouteArticle(ChangeRayon t) throws RemoteException;
Object retireArticle(ChangeRayon t) throws RemoteException;
La classe ChangeRayon étant alors tranformée en
public interface ChangeRayon extends Serializable {
Object nombreDArticles();
/**
* retourne le type d'article concerné par le changement.
*/
Object article();
}
Transformer les implémentations correspondantes.
Le but de l'exercice est de créer un mini serveur de chat.
Q1 Il faut tout d'abord définir les interfaces. On va disposer de deux classes :
Q2 Écrire une implémentation des interfaces précédentes permettant de réaliser le serveur de chat.