TP1 - Un automate comme service - Cas "stateful" et cas "stateless"
Table of Contents
1 Introduction : automate comme service, service comme automate
On cherche à décrire un service proposant la reconnaissance de langages réguliers fermés par préfixe : ce sont les langages reconnus par des automates dont tous les états sont finals. Précisément, on s'intéressera au langage régulier \((ab)^\ast + (ab)^\ast a\) formé d'une succession, éventuellement vide, de mots \(ab\), suivi possiblement d'un \(a\). Par exemple, les mots \(ab\), \(aba\) et \(abab\) sont reconnus alors que le mot \(abaa\) n'est pas reconnu. Pour reconnaître ce langage, on utilise un automate à deux états, UN et DEUX.
Automate reconnaissant le langage ((ab)∗ + (ab)∗ a)
Cet exemple simple repose sur une abstraction très commune : la modélisation du comportement d'un serveur par un automate, appelé aussi machine à états. Il est donc paradigmatique.
2 Automates : les règles chimiques
On spécifie les serveurs et les clients à l'aide de règles chimiques. Ce modèle permet une spécification non seulement concise mais aussi précise des échanges de messages.
2.1 Serveur "stateful" (avec état de session)
Etat
- Session(n) : compteur des sessions
- Execution(n, e) : élément de la table Execution associant au numéro de session n (appelé identifiant de session) un état de l'automate e (UN ou DEUX)
Etat initial
Session(0)
Canaux
- initier(ar) : initialise la session et renvoie sur le canal ar l'identifiant de session et un message OK.
- accepter(c, n, ar) : suivant l'état associé à l'identifiant de session n par la table Execution, accepte le caractère c et renvoie sur le canal ar le couple (n, OK), ou refuse le caractère c et renvoie (n, KO).
- clore(n) : termine la session d'identifiant n en retirant de la table l'entrée correspondant à n.
Règles
- initier(ar) & Session(n) -> ar(n, OK) & Session(n + 1) & Execution(n, UN) - accepter('a', n, ar) & Execution(n, UN) -> ar(n, OK) & Execution(n, DEUX) - accepter('b', n, ar) & Execution(n, DEUX) -> ar(n, OK) & Execution(n, UN) - accepter(x, n, ar) & Execution(n, e) & ((x, e) != ('a', UN) & (x, e) != ('b', DEUX)) -> ar(n, KO) & Execution(n, e) - accepter(x, n, ar) & (Execution(n, _) inactive)-> ar(n, KO) - clore(n) & Execution(n, e) ->
2.2 Client "stateful"
Etat
- quatre états internes : Debut, EnCours, Succes, Echec
- Mot(tab) : tableau de caractères tab formant le mot à transmettre à l'automate
Etat initial
Debut & Mot(['a', 'b', 'a', 'b'])
Canaux
- rep(n, msg) : reçoit en réponse un identifiant de session n et un message msg, soit OK soit KO.
Règles
- Debut -> initier(rep) & EnCours - EnCours & rep(n, OK) & Mot([tete, reste]) -> accepter(tete, n, rep) & Mot([reste]) & EnCours - EnCours & rep(n, OK) & Mot([]) -> clore(n) & Succes - EnCours & rep(n, KO) & Mot(m) -> Echec
2.3 Serveur "stateless" (sans état de session)
Etat
- Session(n) : compteur des sessions
Etat initial
Session(0)
Canaux
- initier(ar) : initialise la session et renvoie sur le canal ar l'identifiant de session et l'état initial de l'automate.
- accepter(c, n, e, ar) : suivant l'état e, accepte le caractère c et renvoie sur le canal ar le couple (n, f), où f est le nouvel état, ou refuse le caractère c et renvoie (n, KO).
Règles
- initier(ar) & Session(n) -> ar(n, UN) & Session(n + 1) - accepter('a', n, UN, ar) > ar(n, DEUX) - accepter('b', n, DEUX, ar) -> ar(n, UN) - accepter(x, n, e, ar) & ((x, e) != ('a', UN) & (x, e) != ('b', DEUX)) -> ar(n, KO)
2.4 Client "stateless"
Etat
- quatre états internes : Debut, EnCours, Succes, Echec
- Mot(tab) : tableau de caractères tab formant le mot à transmettre à l'automate
Etat initial
Debut & Mot(['a', 'b', 'a', 'b'])
Canaux
- rep(n, msg) : reçoit en réponse un identifiant de session n et un message msg, soit un état soit KO.
Règles
- Debut -> initier(rep) & EnCours - EnCours & rep(n, e) & Mot([tete, reste]) -> accepter(tete, n, e, rep) & Mot(reste) & EnCours - EnCours & rep(n, e) & Mot([]) & (e != KO) -> Succes - EnCours & rep(n, KO) & Mot(m) -> Echec
3 Exercice pratique : implémentation de l'automate
On cherche à implémenter l'automate précédent par des services Web Restful.
3.1 Modèle objet
Deux modèles objet sont fournis (cf. les archives associées). Ils diffèrent par la gestion des sessions, centralisée ou décentralisée, comme décrit par les modèles chimiques abstraits.
3.2 Service Restful avec état (stateful)
Créer un nouveau projet Dynamic web project pour le service Restful. Utiliser Tomcat 8.0 comme serveur. Noter que par défaut, le nom du projet sert de préfixe aux chemins des ressources ; pour changer le préfixe, voir : clic droit sur le projet > Properties > Web project settings. Transformer ensuite le projet en projet Maven.
Ajouter les dépendances suivantes dans le fichier pom.xml. La première permet d'importer Jersey, la seconde permet d'utiliser le format de données JSON. Par défaut, seul le format XML est géré.
<dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-servlet</artifactId> <version>2.23.2</version> </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-json-jackson</artifactId> <version>2.23.2</version> </dependency>
Dans le fichier WebContent/WEB-INF/web.xml, ajouter après l'élément welcome-file-list la référence de la servlet à utiliser.
<servlet> <servlet-name>Jersey</servlet-name> <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> </servlet> <servlet-mapping> <servlet-name>Jersey</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping>
Importer dans le projet l'archive codeFourni_ServeurAvecEtat.zip fournissant un paquet rest.
- Interface Automate
- Interfaces Session et Resultat
- Implémentations de Session et Resultat
- Implémentation de Automate (pour reconnaître le langage régulier \((ab)^\ast + (ab)^\ast a\))
- Un paquet jaxb contenant deux adaptateurs JAXB nécessaires pour la traduction en documents XML des objets dont le type (statique) est une interface et le fournisseur associé détaillant les classes d'implémentation des interfaces.
3.2.1 Annotations
Annoter les méthodes de l'interface Automate de manière à vérifier les propriétés suivantes. Toutes ces annotations appartiennent au paquet javax.ws.rs.
- Les méthodes correspondent à des requêtes http de type POST ou PUT. Précisément, deux méthodes ne sont ni pures ni idempotentes, tandis qu'une méthode n'est pas pure mais est idempotente.
Les chemins relatifs d'accès aux ressources correspondant aux méthodes sont les suivants :
- initier : etat/initial,
- accepter : etat/suivant.
- clore : fin
Ainsi, si l'URI de base est http://localhost:8080/Projet et si celle de la ressource implémentant l'automate est X, une URI sera par exemple : http://localhost:8080/Projet/X/etat/initial. Pour indiquer un chemin C, on utilise l'annotation @Path("C").
Le chemin d'accès à la méthode accepter est complété par un suffixe, noté {lettre}, où lettre est un paramètre de chemin associé à l'argument x.
@Path(".../{lettre}") Resultat accepter(@PathParam("lettre") char x, Session id);
- Les sessions et les résultats sont sérialisés au format application/xml ou application/json. Pour indiquer qu'un résultat est sérialisé suivant un certain format javax.ws.rs.core.MediaType.F, on utilise après importation de MediaType l'annotation @Produces(MediaType.F) pour les valeurs produites et @Consumes(MediaType.F) pour les valeurs consommées. On peut spécifier plusieurs valeurs dans un tableau de chaînes de caractères, le client choisissant le format à l'aide des en-têtes HTTP Accept et Content-Type respectivement.
Annoter la classe d'implémentation A_B_point_Etoile correspondant à la ressource par son chemin d'accès automate. Par défaut, une ressource nouvelle est créée à chaque requête. Pour l'éviter, annoter la classe par Singleton.
Pourquoi est-il nécessaire d'annoter l'interface Session par @XmlJavaTypeAdapter(TraductionSession.class) ?
Cette annotation est utilisée par le data binding JAXB. La fonction d'unmarshalling permettant de traduire un document en un objet est paramétrée par la classe de l'objet. Lorsqu'on utilise une interface pour type, JAXB ne peut déterminer la classe à utiliser pour réaliser la traduction de document à objet. On utilise alors un adaptateur, dont la classe est spécifiée par l'annotation. Cette classe implémente la classe abstraite javax.xml.bind.annotation.adapters.XmlAdapter<ImplemX, X> (X étant l'interface, ImplemX son implémentation) en définissant deux méthodes :
public ImplemX marshal(X x) throws Exception public X unmarshal(ImplemX i) throws Exception
La première méthode permet de construire un nouvel objet de type ImplemX à partir d'un objet initial de type X. La seconde méthode s'implémente de manière triviale, par conversion implicite. JAXB procède alors ainsi.
- document vers objet de type X : unmarshalling du document en un objet de type ImplemX, puis conversion implicite par application de la méthode unmarshall de l'adaptateur.
- objet de type X vers document ; application de la fonction marshal produisant un objet de type ImplemX puis marshalling de cet objet en un document.
Pourquoi est-il nécessaire d'annoter la classe ImplemSession par @XmlRootElement(name="session") ? Cette annotation est nécessaire pour le data binding JAXB : elle signifie que cette classe se traduit par un elément global dans le schéma associé par JAXB, et donc que les instances de cette classe peuvent être traduites en des documents XML racines, c'est-à-dire commençant par un élément global du schéma produit.
Remarque. Le pilotage de JAXB, utilisé pour la traduction en XML ou en JSON, n'est pas uniforme suivant les implémentations de la norme JAX-RS. Avec Jersey, il est également nécessaire d'annoter l'interface Session par @XmlRootElement(name="session"), alors qu'elle l'est déjà par l'adaptateur TraductionSession. Avec CXF, une autre implémentation de la norme, ce n'est pas nécessaire. La documentation est souvent insuffisante.
3.2.2 Déploiement et premier test
Dans le fichier web.xml, référencer la définition du service dans la classe rest.Service. Etudier le code de cette classe. Noter qu'en plus de la fonctionnalité de log, deux autres fonctionnalités sont ajoutées :
- la capacité à traiter JSON en utilisant l'outil Jackson,
- un fournisseur de traductions XML pour les résultats et les sessions.
Cette configuration est propre à Jersey.
<init-param> <param-name>javax.ws.rs.Application</param-name> <param-value>rest.Service</param-value> </init-param> <load-on-startup>1 </load-on-startup>
Déployer le service sur le serveur Tomcat. Tester avec Poster. Pour sélectionner le format du résultat, définir le champ accept. Pour spécifier le format du corps de la requête, initialiser le champ content-type.
3.2.3 Rappel sur le protocole HTTP et les URI
Cf. le cours.
3.2.4 Client
Créer un nouveau projet Java.
- Le transformer en projet Maven.
Ajouter les dépendances suivantes dans le fichier pom.xml. Les deux premières donnent les bibliothèques côté client, tandis que les suivantes permettent à Jersey d'utiliser les formats XML et JSON pour échanger des données.
<dependency> <groupId>org.glassfish.jersey.core</groupId> <artifactId>jersey-client</artifactId> <version>2.23.2</version> </dependency> <dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-proxy-client</artifactId> <version>2.23.2</version> </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-jaxb</artifactId> <version>2.23.2</version> </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-json-jackson</artifactId> <version>2.23.2</version> </dependency>
Y importer à la racine l'archive codeFourni_ClientServeurAvecEtat.zip fournissant deux paquets rest et client.
Paquet rest
- Le paquet diffère de celui côté serveur par le retrait de l'implémentation de l'automate. En effet, l'automate distant sera accédé par l'intermédiaire d'un proxy.
Paquet client
- Classe AppliCliente contenant la fonction principale
- Classe AutomateProxy définissant une implémentation de l'interface Automate par un proxy
Annoter l'interface Automate comme précédemment.
Dans la classe client.AppliCliente, implémenter le scénario suivant dans la fonction principale.
- Créer une cible de type WebTarget en appelant la fonction clientJAXRS, à étudier au préalable et à comparer avec le constructeur de la classe Service côté serveur.
- Créer un automate proxy en utilisant la fabrique WebResourceFactory, propre à Jersey.
- Appeler une fonction de test en passant l'automate en argument.
Implémenter la fonction de test ainsi.
- Initialiser un mot avec le tableau de caractères {'a', 'b', 'a', 'b', 'a', 'b'}.
- Initier une session.
- Appeler l'automate lettre par lettre pour vérifier si ce mot est accepté.
- Si c'est le cas, afficher un message de succès, sinon afficher un message d'échec. Dans tous les cas, clore la session.
Tester votre application.
Décrire une requête correspondant à l'invocation de la méthode accepter aves les arguments suivants :
- lettre : 'a',
- session d'identifiant 3
ainsi que sa réponse.
méthode Http : ... Adresse : ... En-tête : ... Message (payload) : ...
En-tête : ... Réponse : ...
Comment faire pour utiliser JSON ? Quelle est la réponse alors ?
Réponse : ...
Implémenter un automate proxy dans la classe client.AutomateProxy, en utilisant les cibles de type WebTarget.
Tester de la même manière cet automate proxy, avec le format XML puis JSON.
3.3 Service Restful sans état (stateless)
Créer un nouveau projet Dynamic Web Project pour le service Restful. Le configuer comme le précédent.
Y importer l'archive codeFourni_ServeurSansEtat.zip fournissant un paquet rest.
- Interface Automate
- Interfaces Session et Resultat
- Implémentations de Session et Resultat
- Implémentation de Automate (pour reconnaître le langage régulier \((ab)^\ast + (ab)^\ast a\))
- Un paquet jaxb contenant deux adaptateurs JAXB nécessaires pour la traduction en documents XML des objets dont le type (statique) est une interface et le fournisseur associé détaillant les classes d'implémentation des interfaces.
Relativement à la version précédente, deux différences apparaissent.
- L'interface Automate se simplifie.
- L'état de l'automate est agrégé dans chaque session.
3.3.1 Annotations
Annoter les méthodes de l'interface Automate de manière à vérifier les propriétés suivantes.
- Les deux méthodes correspondent à des requêtes http de type POST et GET. Précisément, comme la méthode initier n'est ni pure ni idempotente, elle doit être associée à une requête de type POST. Quant à la la méthode accepter, étant pure et idempotente, elle peut être associée à une requête de type GET.
Les chemins relatifs d'accès aux ressources correspondant aux méthodes sont les suivants :
- initier : etat/initial,
- accepter : etat/suivant.
Ainsi, si l'URI de base est http://localhost:8080/Projet et si celle de la ressource implémentant l'automate est X, une URI sera par exemple : http://localhost:8080/Projet/X/etat/initial. Pour indiquer un chemin C, on utilise l'annotation @Path("C").
Le chemin d'accès à la méthode accepter est complété par un suffixe, noté {lettre}, où lettre est un paramètre de chemin associé à l'argument x.
@Path(".../{lettre}") Resultat accepter(@PathParam("lettre") char x, Session id);
Le chemin d'accès à la méthode accepter est aussi complété par une requête, notée ?id=E-n, où E-n est une représentation de l'argument id, E étant l'état de la session, n son numéro. Cettre traduction bi-univoque d'une session en String est calculée par la méthode toString et par la fonction fromString de l'interface Session. Déclarer la méthode accepter ainsi :
Resultat accepter(@PathParam("lettre") char x, @QueryParam("id") Session id);
- Les sessions et les résultats sont sérialisés au format application/xml ou application/json. Pour indiquer qu'un résultat est sérialisé suivant un certain format javax.ws.rs.core.MediaType.F, on utilise après importation de MediaType l'annotation @Produces(MediaType.F) pour les valeurs produites et @Consumes(MediaType.F) pour les valeurs consommées. On peut spécifier plusieurs valeurs dans un tableau de chaînes de caractères, le client choisissant le format à l'aide des en-têtes HTTP Accept et Content-Type respectivement.
Annoter la classe d'implémentation A_B_point_Etoile correspondant à la ressource par son chemin d'accès automate. Comparer l'implémentation à celle du premier projet.
Par défaut, une ressource nouvelle est créée à chaque requête. Pour l'éviter, annoter la classe par Singleton.
3.3.2 Déploiement et premier test
Dans le fichier web.xml, référencer la définition du service dans la classe rest.Service. Etudier le code de cette classe. Noter que cette configuration est propre à Jersey.
<init-param> <param-name>javax.ws.rs.Application</param-name> <param-value>rest.Service</param-value> </init-param> <load-on-startup>1 </load-on-startup>
Déployer le service sur le serveur Tomcat. Tester avec Poster. Pour sélectionner le format du résultat, définir le champ accept.
3.3.3 Client
Créer un nouveau projet Java et le transformer en projet Maven. Le configurer comme le projet client précédent.
Y importer à la racine l'archive codeFourni_ClientServeurSansEtat.zip fournissant deux paquets rest et client.
Paquet rest
- Le paquet diffère de celui côté serveur par le retrait de l'implémentation de l'automate. En effet, l'automate distant sera accédé par l'intermédiaire d'un proxy.
Paquet client
- Classe AppliCliente contenant la fonction principale
- Classe AutomateProxy définissant une implémentation de l'interface Automate par un proxy
Annoter l'interface Automate comme précédemment.
Dans la classe client.AppliCliente, reprendre le scénario précédent.
- Créer une cible de type WebTarget en appelant la fonction clientJAXRS, à étudier au préalable et à comparer avec le constructeur de la classe Service côté serveur.
- Créer un automate proxy en utilisant la fabrique WebResourceFactory, propre à Jersey.
- Appeler une fonction de test en passant l'automate en argument.
Implémenter la fonction de test ainsi.
- Initialiser un mot avec le tableau de caractères {'a', 'b', 'a', 'b', 'a', 'b'}.
- Initier une session.
- Appeler l'automate lettre par lettre pour vérifier si ce mot est accepté.
- Si c'est le cas, afficher un message de succès, sinon afficher un message d'échec.
Tester votre application.
Décrire une requête correspondant à l'invocation de la méthode accepter aves les arguments suivants :
- lettre : 'a',
- session d'identifiant 3 et d'état UN
ainsi que sa réponse.
méthode Http : ... Adresse : ... En-tête : ... Message (payload) : ...
En-tête : ... Réponse : ...
Comment faire pour utiliser JSON ? Quelle est la réponse alors ?
Réponse : ...
Implémenter un automate proxy dans la classe client.AutomateProxy, en utilisant les cibles de type WebTarget.
Tester de la même manière cet automate proxy, avec le format XML puis JSON.
3.4 Contrôle de la concurrence
Les deux implémentations proposées suivent d'assez près la spécification chimique. Cependant, les règles chimiques ont une propriété fondamentale : elles s'exécutent (ou s'interprètent) de manière atomique (sans interruption). Ce n'est pas le cas des implémentations en Java. Il peut donc exister des accès concurrents, avec des risques de pertes en écriture.
3.4.1 Adapter à la concurrence le service avec état
Dans le premier projet client, créer une seconde classe de Test, TestConcurrence, contenant la fonction principale suivante.
public static void main(String[] args) { String adresse = "http://localhost:8080/AutomateAvecEtatSession/automate"; System.out.println("*************"); WebTarget cible = AppliCliente.clientJAXRS().target(adresse); Automate automateProxyJersey = WebResourceFactory.newResource(Automate.class, cible); int MAX = 100; // à augmenter possiblement Session[] sessions = new Session[MAX]; for (int i = 0; i < MAX; i++) { sessions[i] = automateProxyJersey.initier(); System.out.println(sessions[i].getNumero()); } for (int i = 0; i < MAX; i++) { automateProxyJersey.clore(sessions[i]); System.out.println(sessions[i].getNumero()); } System.out.println("*************"); }
Qu'observe-t-on lorsque plusieurs clients sont lancés simultanément (cinq à dix typiquement) ?
On observe des pertes en écriture : le numéro de session n'est pas incrémenté à chaque requête initier.
Remarque : suivant la machine utilisée, sa rapidité et son nombre de coeurs, des pertes en écriture peuvent apparaître avec plus ou moins d'itérations et plus ou moins de clients.
Pour garantir l'atomicité, il suffit de contrôler la concurrence dans le code Java en utilisant des verrous et des structures de données fonctionnant dans un contexte concurrent.
- Créer une nouvelle classe A_B_point_EtoileConcurrent implémentant l'interface Automate, par copie de la classe A_B_point_Etoile.
- Modifier le chemin d'accès en automate/concurrent.
- Qualifier la méthode initier par le mot-clé synchronized : celui garantit l'exécution atomique de la méthode.
- Remplacer les types Map et HashMap par les versions concurrentes ConcurrentMap et ConcurrentHashMap.
- Enregistrer la nouvelle ressource dans le constructeur de la classe Service.
- Tester la nouvelle ressource, après modification de l'adresse dans la fonction principale.
3.4.2 Adapter à la concurrence le service sans état
Procéder de même pour le second projet en adaptant le contrôle de la concurrence ainsi.
- Qualifier la méthode initier par le mot-clé synchronized : celui garantit l'exécution atomique de la méthode.
Entourer la section critique réalisant l'incrémentation du compteur par l'opérateur synchronized.
synchronized(this){ compteur++; System.out.println("************** requête accepter numéro " + compteur + " *************"); }