Java Rmi Workflow
Java Rmi Workflow
1.1 Rappels
1 2
1.2 Workflow
[Link] Définition
un objet distribué est un objet qui implémente une interface distante (i.e.,
l’interface [Link] ou n’importe quelle sous-interface personnalisée de
celle-ci) et qui éventuellement héritera de [Link]
(bonne pratique pour hériter le comportement d’exportation de l’objet).
Ceci nous permettra de référencer notre objet distant au sein d’un reg-
istre RMI qui sera ensuite “queriable” par un client via la méthode
[Link]#lookUp(String name).
Chaque méthode d’une interface distante doit déclarer l’exception RemoteException
dans sa clause throws lors de la déclaration de sa signature. Cette exception
permet de capturer les différentes erreurs pouvant survenir lors de l’exécution
Figure 4: Passage d’un paramètre d’une classe inconnue du serveur dans un environnement distribué. Par exemple, une exception retournée lors de
la coupure de la connexion entre les sockets utilisées pour établir une connexion
réseau, ou lors de l’accès distant à une référence non existante, . . .
3 4
/* SYNTAXE GÉNÉRIQUE D'UNE INTERFACE DISTANTE */ sera retourné par le registre RMI. Alors, on ne peut pas faire passer un objet
// dans le projet commons non distant par référence en Java RMI. On ne peut le faire que “par valeur”.
package [Link]; Pour ce faire, il faut alors le rendre sérialisable, et cet objet sérialisable ne sera
associé à aucun proxy le référençant.
import [Link];
import [Link];
[Link] Sérialisation vs. Désérialisation
public interface IObjetDistant extends Remote { Un objet sérialisable est un objet qui possède une représentation textuelle perme-
typeRetour methodeDistante([params]) throws RemoteException; ttant de le stocker sur des supports de persistance, mais aussi de le transmettre
... à travers un réseau. Désérialiser un objet consisterai ainsi à le reconstruire à par-
} tir de sa représentation textuelle. En Java, rendre un objet sérialisable signifie
qu’il doit implémenter l’interface [Link].
/* SYNTAXE GÉNÉRIQUE D'UNE CLASSE D'UN OBJET DISTANT */
// dans le projet serveur
package [Link]; [Link].1 Exemple
public class Person implements Serializable {
import [Link]; /* ATTRIBUTES */
import [Link]; private String name;
private int age;
import [Link];
/* CONSTRUCTORS */
public class ObjetDistantImpl extends UnicastRemoteObject { public Person() {
/* attributs */
/* méthodes */ }
@Override
typeRetour methodeDistante([params]) throws RemoteException{ public Person(String name, int age) {
// implémentation [Link] = name;
} [Link] = age;
... }
// autres méthodes : getters/setters, implémentations privées internes, etc.
} /* METHODS */
public String getName() {
[Link] Passage par référence et passage par valeur return name;
}
En général, un passage par valeur veut dire passage par copie de l’objet lui-même,
alors qu’un passage par référence veut dire passage de l’adresse de l’objet au lieu public int getAge() {
de l’objet lui-même. Dans le cadre de Java, ceci est abstrait du programmeur. return age;
Tous les types primitifs (char, byte, int, long, double, boolean, . . .) sont }
passés en valeur, alors que tous les objets sont passés par référence.
@Override
public String toString() {
[Link] Passage par référence vs passage par valeur en Java RMI
return "(Name: "+name+", Age: "+age+")";
Dans le cadre du Java RMI faire un passage par référence signifie faire un passage }
d’un objet distribué qui sera référencé par son objet proxy correspondant et c’est }
ce dernier qui sera utilisé par toute entité souhaitant utiliser l’objet distant et qui
5 6
[Link].2 En Java RMI * a RealInternet concrete class that plays the role of RealSubject
* in the Proxy design pattern.<br/>
Quand un client essaye d’invoquer une méthode sur l’objet distant, le proxy
* It implements the Internet interface to allow access to the Internet.
sérialise (ou marshalle) les arguments passés et l’envoie au serveur. Quand
* @author anonbnr
le serveur reçoit du client une invocation distante, il l’envoie au squelette qui
*
désérialise (ou démarshalle) les arguments passés, invoque la méthode sur
*/
l’objet distant réel, et retourne le résultat au serveur qui le renvoie au client. La
public class RealInternet implements Internet {
sérialisation et la désérialisation des arguments passés sont automatiquement
gérées par Java RMI.
/* METHODS */
Pour plus d’informations sur la sérialisation/désérialisation en Java : @Override
[Link] public void connectTo(String serverHost) {
[Link]("Standard Console: Connecting to " + serverHost);
}
[Link] Les proxys }
Proxy est un patron de conception (design pattern) dans lequel un objet peut
être référencé par un autre objet qui se comporte en tant que son intermédiaire. package [Link];
Les deux types d’objets sont conformes à une interface commune. L’avantage
de cette conception est de pouvoir effectuer des contrôles et/ou des calculs import [Link];
avant/après l’invocation actuelle d’une méthode ciblée d’un objet. Toute méth- import [Link];
ode ciblée fait partie ainsi de l’interface commune d’un objet et de son proxy.
/**
* a ProxyInternet concrete class that plays the role of Proxy
[Link].1 Exemple * in the Proxy design pattern.<br/>
package [Link]; * It provides a proxy to classes implementing Internet, particularly
* to ban Internet connections to some hosts.
/** * @author anonbnr
* an Internet interface that plays the role of Subject *
* in the Proxy design pattern.<br/> */
* It provides an interface for connecting to the Internet public class ProxyInternet implements Internet {
* that we want to limit access to using a proxy.
* @author anonbnr /* ATTRIBUTES */
*/ /**
public interface Internet { * The proxied Internet connection
*/
/* METHODS */ private Internet internet;
/**
* Connects to serverHost /**
* @param serverHost An Internet host to which we wish to connect * The list of banned sites
*/ */
void connectTo(String serverHost); private static List<String> bannedSites;
}
static {
package [Link]; bannedSites = new ArrayList<>();
[Link]("[Link]");
/** [Link]("[Link]");
[Link]("[Link]");
7 8
[Link]("[Link]"); En Java RMI, les objets distants sont référencés par des stubs (proxy), qui
} seront liés à des noms uniques et enregistrés dans un registre RMI. Quand un
client réclame un objet distant par son nom au registre RMI, celui-ci lui renvoie
/* METHODS */ le stub associé à l’objet demandé. Ce stub sera ensuite utilisé par le client
/** pour invoquer des méthodes sur l’objet distant. Le stub se chargera ainsi de
* Only allows the proxied Internet connection to connect to hosts sérialiser les paramètres des invocations, et faire les appels réseaux nécessaires
* that are not in the banned sites, otherwise denies access to the host. pour communiquer avec le serveur. Lorsque le serveur reçoit l’appel, il l’envoie
* It also creates the Internet connection, only if it hasn't already au squelette qui désérialise les paramètres, invoque la méthode sur l’objet distant
* been created réel, et retourne le résultat au serveur qui le renvoie au client.
*/
Les objets stubs, le squelette, et leurs comportements sont automatiquement
@Override
générés et contrôlés par Java RMI et on ne s’en soucie pas. Ainsi, on continuera
public void connectTo(String serverHost) {
à construire notre programme client comme si on était en train d’invoquer une
if ([Link](serverHost))
méthode sur un objet local, alors qu’on est en train d’invoquer une méthode sur
[Link]("Standard Error: Access Denied to " + serverHost);
l’objet proxy d’un objet distant.
else {
if (internet == null) [Link] Dans le TP
internet = new RealInternet();
Dans le cadre de ce TP, un animal est un objet distant qui devrait être ma-
[Link](serverHost); nipulé par le client, et donc il faut qu’il implémente une interface distante
} (IAnimal qui dérive de l’interface [Link]) et qu’il étend la classe
} [Link]. Par contre, son espèce est un objet
} qui ne devrait pas être manipulé directement par le client, mais qui devrait
quand même être reconnue par lui lorsque ce dernier va manipuler des objets de
package [Link]; type IAnimal (des objets proxy). Pour ce faire, l’espèce doit être ainsi déclarée
en tant qu’une classe sérialisable dont l’implémentation doit être accessible au
/** client et au serveur. C’est pour cette raison qu’on l’installe au sein du projet
* a Test class commons.
* @author anonbnr
* 1.2.3 Serveur
*/
public class Test { Un serveur doit effectuer les fonctionalités suivantes :
public static void main(String[] args) {
Internet internet = new ProxyInternet(); • éventuellement créer une instance d’un registre RMI dans le même proces-
[Link]("[Link]"); sus : Registry registry = [Link](RMI_REGISTRY_PORT);
[Link]("[Link]"); si l’outil rmiregistry a été lancé déjà via la ligne de commande dans un
} processus séparé.
} • récupérer une instance du registre RMI : Registry registry =
getRegistry(RMI_REGISTRY_PORT);
/* OUTPUT */
// Standard Error: Access Denied to [Link] • créer les objets distants ;
// Standard Console: Connecting to [Link] • lier les objets distants à des noms uniques au sein d’un registre RMI ;
• recevoir une requête du client consistant à invoquer une méthode distante
sur l’objet distant via son proxy, la traiter, et retourner le résultat.
[Link].2 En Java RMI
Le serveur doit être mis en place dans l’ordre suivant :
9 10
1. mise en place de son gestionnaire de sécurité (et de sa politique de sécu- comme un contrôleur, faisant le lien entre les fonctionnalités du client et les
rité). interactions de l’utilisateur (i.e., une CLI ou un GUI ).
2. éventuellement mise en place de son codebase.
// Exemple
3. mise en place de sa politique de sécurité.
/*
4. mise en place de l’instance du registre RMI.
* Classe modélisant le contrôleur
5. instanciation et distribution d’objets distants dans le registre RMI.
* qui gère l'interaction entre le client
* et le serveur.
[Link] Serveur et Main */
public class Main {
En général, la méthode main() (côté serveur) est ajouté à la classe du serveur public static void main(String[] args) {
directement. Toutefois, ceci peut devenir limitant pour l’extensibilité et la réutil- try {
isabilité, surtout si on souhaite éventuellement avoir une hiérarchie de serveurs
de différentes fonctionnalités et qui peuvent partager des fonctionnalités com- Client client = new Client();
munes par héritage. Pour éviter ces limitations, on peut introduire une classe [Link]();
Main dédiée, qui contiendra la méthode main() créant et mettant en place une [Link]("Animal Care");
instance du serveur. ICabinet cabinet = [Link]();
// Exemple [Link]("Objet proxy de l'objet cabinet: "+cabinet);
public class Main { /*
public static void main(String[] args) { * OUTPUT
Server server = new Server(); * ======
try { * Objet proxy de l'objet cabinet: Proxy[ICabinet,
[Link](); * RemoteObjectInvocationHandler [UnicastRef [liveRef:
} catch(RemoteException e) { * [endpoint:[[Link]:34553]
[Link](); * (remote),objID:[6cc201a2:17cc940e953:-7fff,
} * 604015115438757631]]]]]
} */
} browseCabinet(client, cabinet);
} catch (RemoteException e) {
1.2.4 Client [Link]();
} catch (NotBoundException e) {
Un client doit effectuer les fonctionnalités suivantes : [Link]();
• récupérer une instance du registre RMI. } catch (IOException e) {
• rechercher les objets distants d’intérêt. [Link]();
• invoquer des méthodes sur un objet distant pour accomplir une }
logique métier (e.g., afficher des informations sur un objet distant, }
créer/modifier/supprimer un objet distant, . . .) }
En général, la méthode main() (côté client) est ajoutée à la classe du client Un gestionnaire de sécurité pour une classe en Java permet de lui définir une
directement. Toutefois, pour les mêmes raisons susmentionnées, on peut in- politique de sécurité et de vérifier si cette politique de sécurité est bien respectée.
troduire une classe Main dédiée, qui contiendra la méthode main() créant et Par défaut, une classe n’a pas un gestionnaire de sécurité. Pour en créer un :
mettant en place une instance du client. De plus, cette classe se comportera [Link](new SecurityManager());
11 12
Une politique de sécurité permet de contrôler les interactions pos- Voici donc deux implémentations simples de ces deux méthodes au niveau de la
sibles avec une classe en spécifiant des permissions. Toute interac- classe CabinetMedicalImpl (côté serveur) implémentant ICabinetMedical :
tion non autorisée par la politique de sécurité engendre une excep-
@Override
tion de type [Link]. Par défaut, une
public boolean addAnimal(String name, String ownerName, String speciesName,
classe n’a ni un gestionnaire de sécurité, ni une politique de sécurité.
int speciesAverageLife, String race, String state)
Pour lier une classe à un fichier définissant sa politique de sécurité :
throws RemoteException {
[Link]("[Link]", "path/to/[Link]");
IAnimal patient = new AnimalImpl(name, ownerName, speciesName,
Dans le cadre de ce TP, on créera une politique de sécurité à titre indicatif en speciesAverageLife, race, state);
donnant toutes les permissions à tout le monde. Pour ce faire : return [Link](patient);
}
// path/to/[Link]
grant {
@Override
permission [Link];
public boolean addAnimal(String name, String ownerName, Species species,
};
String race, String state) throws RemoteException {
IAnimal patient = new AnimalImpl(name, ownerName, species, race, state);
1.2.6 Codebases return [Link](patient);
}
[Link] Motivation Voici les deux constructeurs de la classe AnimalImpl (côté serveur) implémen-
Dans une prochaine étape de l’exercice on introduit la notion d’un cabinet médi- tant IAnimal :
cal qui contiendra la liste des animaux suivis et qui sera le point d’interfaçage protected AnimalImpl(String name, String ownerName, String speciesName,
entre le client et le serveur. Le cabinet médical introduira des méthodes perme- int speciesAverageLife, String race, String state)
ttant de consulter les animaux suivis, mais aussi d’en ajouter. Ainsi, un cabinet throws RemoteException {
médical est un objet distant créé de la même manière que précédemment avec les [Link] = name;
objets animaux. On suppose que l’interface distante ICabinetMedical fournit [Link] = ownerName;
plusieurs méthodes dont les méthodes suivantes pour ajouter des animaux : [Link] = new Species(speciesName, speciesAverageLife); // création de l'espèce
boolean addAnimal(String name, String ownerName, String speciesName, [Link] = race;
int speciesAverageLife, String race, [Link] = new FollowUpFileImpl(state);
String state) throws RemoteException; }
boolean addAnimal(String name, String ownerName, Species species,
String race, String state) throws RemoteException; protected AnimalImpl(String name, String ownerName, Species species, String race,
String state) throws RemoteException {
La première permet d’ajouter un animal sans besoin de spécifier son espèce [Link] = name;
explicitement (via une instance de la classe Species). C’est-à-dire l’objet [Link] = ownerName;
Species est créé à l’intérieur du constructeur de l’animal et dont les paramètres [Link] = species; // affectation de l'espèce créée auparavant
String speciesName, int speciesAverageLife sont passés via la méthode [Link] = race;
addAnimal(). [Link] = new FollowUpFileImpl(state );
Par contre, la deuxième version de la méthode permet d’ajouter un animal en }
spécifiant explicitement son espèce via une instance de la classe Species. Ceci Maintenant supposons que le client souhaite ajouter au cabinet médical un
est possible parce que la classe Species est dans le projet commons, et donc le IAnimal dont l’espèce est une sous-classe de Species (e.g., une classe Dog).
client et le serveur la reconnaissent. Ainsi l’objet Species est créé et initialisé Le client peut déclarer ainsi une classe Dog de son côté qui étend Species et
du côté client avant d’être passé à la méthode addAnimal(). Le constructeur ensuite utiliser la deuxième version de addAnimal() en lui passant une instance
de l’animal ajouté se contentera ainsi d’affecter l’instance Species passée à de Dog pour ajouter l’animal au cabinet médical. Toutefois, le serveur va se
l’attribut correspondant au niveau de l’instance de l’animal créé. plaindre de son côté parce qu’il ne reconnaît pas la classe Dog. En effet, il ne
connaît que la classe Species dans le projet commmons qui est ajouté à son class-
13 14
path. Donc on aura une exception de type ClassNotFoundException quand on de chercher une classe du client qu’il ne reconnaît pas à partir du chemin du
essaye d’ajouter l’animal ayant une espèce Dog au cabinet médical. codebase, si on inclut le nom d’un package contenant la classe dans le chemin,
le serveur ne pourra jamais la trouver. Dans cette situation :
[Link] Solution : Les codebases 1. soit on spécifie le chemin du dossier bin/ du client comme codebase con-
tenant tous les fichiers bytecode de toutes les classes du projet client (ce
Pour résoudre ce problème, il faut que le serveur puisse reconnaître la classe qui peut être problématique si on ne souhaite pas exposer toutes les classes
Dog, notamment son bytecode (i.e., [Link]). Pour ce faire on utilise ce du client au serveur).
qu’on appelle un codebase désignant un mécanisme classpath distribué. Il 2. soit on peut copier les fichiers bytecode des classes que l’on souhaite ex-
s’agit simplement d’un dossier contenant des fichiers bytecode de classes. poser et les mettre dans un dossier parent (qui n’est pas un package) dans
Chaque entité pourra avoir son propre codebase où seront installés les fichiers le projet (ou ailleurs sur le système de fichiers). Ainsi, le chemin du
bytecode des classes qui devraient être accessibles aux autres composants de codebase désignera le chemin de ce dossier parent.
l’application. En gros, si le client définit et utilise des classes qui figurent
dans l’application et qui doivent être reconnues par le serveur, il installera leurs
[Link] Dernières remarques
fichiers bytecode dans son codebase, et le serveur ira pendant l’exécution consul-
ter le codebase du client, où il trouvera ces fichiers bytecode qui n’existent pas 1. Dans le cadre de ce TP nous avons créé un projet commons qui contiendra
directement dans son classpath. Bien sûr ça peut être l’inverse aussi ou le client les interfaces distantes et les classes non distantes accessibles au client et
ira chercher des classes qu’il ne reconnaît pas depuis le codebase du serveur. serveur, puis on a ajouté son classpath aux classpaths du client et serveur.
Cependant, ceci est une approche simplifiée pour le but du TP. En réalité,
Ce codebase peut être localisé sur une machine distante et servi par
ce projet sera un codebase dédié qui sera consulté par le client et le serveur
un serveur web via HTTP et qui pourra être consulté via son URL
pour récupérer les fichiers bytecode des interfaces distantes et les classes
(e.g., [Link] ), comme il peut être défini
non distantes qui leur sont accessibles.
comme dossier sur le système de fichiers de la même machine d’un
2. Le client ne se comportera pas comme un serveur que si le serveur a besoin
composant de l’application et pourra être consulté via son chemin (e.g.,
de manipuler explicitement des objets distants provenant du client, et dans
file:/home/someUser/path/to/codeBase).
ce cas là on peut parler d’un mode Peer-to-Peer (P2P). Si le serveur
Pour spécifier un/plusieurs codebases qui pourront être consultés par un com- se contente uniquement de récupérer le fichier bytecode d’une classe C
posant, on spécifie leurs URIs via la propriété "[Link]" du utilisée par le client et dont une instance est transmise au serveur sans
composant concernée de la manière suivante : [Link]("[Link]", que ce dernier puisse la reconnaître, ceci n’est pas du P2P. En effet, le
"URICodebase1 URICodebase2 ...");. Dans notre cas, il faut définir cette serveur ne manipule pas explicitement des variables de type C dans son
propriété pour le serveur, vu que c’est lui qui va aller chercher l’implémentation code.
de la classe Dog depuis le codebase du client. 3. Quand on a une application P2P, les interfaces des objets distants créés
par le client et qui seront manipulées explicitement par le serveur doivent
Il faut aussi faire attention en spécifiant le chemin du codebase; il faut que le
être dans le projet commons.
dossier du codebase ne soit pas un package contenant la classe qu’on souhaite
partager, parce que la classe est reconnue par son nom complètement qualifié
(Fully Qualified Name). 1.3 Erreurs fréquentes
Pour rappel, le nom complètement qualifié d’une classe désigne son nom et le
nom du package qui la contient (e.g., une classe Animal qui existe dans un pack- 1.3.1 Conflit entre les instances du serveur
age [Link]-package aura [Link]
comme nom complètement qualifié). Ceci est essentiel parce que deux classes Parfois vous faites tourner plusieurs instances du serveur par accident et vous
peuvent avoir le même nom tout en étant localisées dans deux packages tombez ainsi sur une erreur vous indiquant que le port de votre serveur est déjà
différents. Ainsi le seul moyen de les distinguer sera à travers leurs noms occupé, et qu’il ne peut pas être lancé. Dans cette situation, avant de relancer
complètement qualifiés. votre programme, vérifier bien dans votre console qu’aucune instance du serveur
n’est en train de tourner (i.e., elle est “terminated”). En gros, il faut arrêter
Dans un projet Java, tout dossier sous src/ ou bin/ est un package et donc ne toutes les instances du serveur avant de relancer une nouvelle instance.
peut pas être utilisé en tant qu’un dossier codebase. Alors si le serveur essaye
15 16
Figure 5: Consulter les programmes en cours d’exécution sous Eclipse
Parfois vous essayez de lancer le programme de votre client alors que le pro-
gramme de votre serveur ne l’est pas encore. Dans ce cas le client ne pourra pas
ainsi se connecter au serveur. Rappelez-vous de l’ordre d’exécution :
1. lancer une instance du registre RMI :
• soit en utilisant rmiregistry depuis la ligne de commandes pour un
processus séparé,
• soit en créant une instance du registre au sein du processus du serveur
en utilisant createRegistry(int port), et qui sera ainsi lancée
quand vous lanceriez votre serveur.
2. lancer une instance du serveur.
3. lancer une instance du client.
Pour lancer plusieurs instances de votre client, il faut que le programme de
votre client soit terminable manuellement via son interface fournie (e.g., via une
option quit pour une CLI ou un bouton fermer pour une GUI).
Quand vous créez votre projet Java, le wizard de création vous demandera si
vous voulez créer un fichier [Link] qui permet de rendre votre projet
modulaire. Si vous le faites, votre projet n’arrivera pas à localiser le module
contenant les classes du package [Link] pour qu’il puisse se lancer. Dans ce
cas, vous pouvez préciser les modules nécessaires dont votre projet dépend selon Figure 6: Le wizard pour la création de projets Java sous Eclipse
la syntaxe de description des modules en Java (au délà de la portée de ce TP).
Je vous invite ainsi à éviter de créer un projet Java modulaire (cf. [Link] ci-joint),
sauf si vous maîtrisez déjà tous ses facettes et que vous le souhaitez, mais ce
n’est pas le but du TP.
17 18
3. on peut les déclarer protected si on souhaite donner un accès directe aux
sous-classes de C.
19 20
1.4.2 Divers
Il faut imaginer votre système d’une manière orientée objet. Pensez bien aux
patrons de conception, aux principes de conception SOLID, et aux relations
entre vos objets. Réifier quand c’est possible et nécessaire. Imaginer une inter-
face utilisateur (CLI ou GUI) qui nous permettra de tester votre travail d’une
manière ergonomique.
N’oubliez pas de commiter votre code sur un dépôt distant après avoir implé-
menté chaque feature. De cette manière vous diviserez vos tâches convenable-
ment et vous laisserez des traces pour suivre l’évolution de votre performance.
21