UP | HOME

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.

medias/soc_automata_ab-star.svg

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"

Etat

  • Session(n) : compteur des sessions
  • Execution(n, e) : table associant au numéro de session (appelé identifiant de session) un état de l'automate (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"

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 un service Web SOAP et par un service 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 SOAP avec état (stateful)

Créer un nouveau projet de type Dynamic Web Project, utilisant Tomcat 7. Y importer à la racine l'archive codeFourni_soapServeur.zip fournissant un paquet soap.

  • 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\))
  • adaptateur JAXB nécessité par les interfaces dont les valeurs sont traduites en documents XML

3.2.1 Serveur - Génération automatique ("code d'abord")

Engendrer à partir de l'interface Automate un service WS*.

  • Placer les curseurs au maximum, ce qui permet de tester dès la création.
  • Choisir la classe implémentant l'automate.
3.2.1.1 Commandes

Le générateur de services web produit un paquet soap.jaxws contenant différentes classes, comme Accepter et AccepterResponse.

A quoi correspondent-elles ? Elles correspondent à la partie "message" des requêtes et réponses impliquées dans les invocations de la méthode accepter. Elles représentent des commandes.

Pourquoi sont-elles annotées par @XmlRootElement ? 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.

A quoi sert l'annotation @XmlAccessorType ? Cette annotation est utilisée par JAXB. Elle décrit la manière dont JAXB extrait des informations pour produire un document XML.

A quoi sert l'annotation @XmlType ? Cette annotation est utilisée par JAXB. Elle décrit comment le type Java est transformé en un type XML.

3.2.1.2 WSDL

A quoi sert le fichier WebContent/wsdl/automate.wsdl ? Il définit le contrat réalisé par le service dans un langage appelé WSDL.

WSDL (Web Services Description Language) : version 1.0 en cxf, version 2.0 (la dernière) en cours d'implémentation

3.2.2 Client - Génération automatique ("contrat d'abord")

En pratique, la situation est la suivante. Un serveur déclare par un contrat de type WSDL les services qu'il rend. Le client récupère le contrat et engendre automatiquement un squelette d'application cliente. Il reste à le compléter.

Génération automatique à partir du contrat WSDL

  • Créer un nouveau projet client (de type Dynamic Web Project, utilisant Tomcat).
  • Recopier le répertoire wsdl du serveur dans le répertoire WebContent.
  • Sélectionner le contrat WSDL, et engendrer le code client (Web Services > Generate Client), curseur au maximum pour pouvoir tester.
  • Sélectionner comme destination le projet client et un paquet soap (si ce n'est déjà fait).
  • Importer à la racine l'archive codeFourni_soapClient.zip fournissant le paquet appli.
  • Remplacer la fonction principale de la classe soap.Automate_ParABParEtoilePort_Client par la fonction suivante :
    public static appli.Automate getServiceAutomate(){
      URL wsdlURL = ParABParEtoileService.WSDL_LOCATION;
      ParABParEtoileService ss = new ParABParEtoileService(wsdlURL, SERVICE_NAME);
      return new appli.AutomateClient(ss.getParABParEtoilePort());
    }
    

    Cette fonction permet de produire un automate de type appli.Automate en adaptant l'automate "stub" produit par CXF.

  • Dans le projet client, implémenter le scénario suivant dans la fonction principale de la classe appli.Test.
    • Créer un automate (en utilisant la fonction getServiceAutomate).
    • Initialiser un mot avec le tableau de caractères {'a', 'b', 'a', 'b', 'a', 'b'}.
    • Vérifier si ce mot est accepté.
    • Si c'est le cas, afficher un message de succès, sinon afficher un message d'échec.
    • Clore l'exécution.
  • Tester votre application.
  • De quel type sont les requêtes HTTP associées aux invocations des méthodes de l'interface "Automate" ? POST.
  • Décrire une requête correspondant à l'invocation de la méthode accepter avec les arguments suivants :
    • lettre : *'a'* (code 97)
    • session d'identifiant 3
méthode HTTP : POST
Adresse : http://localhost:8080/ProjetServeurSoap/services/Par_AB_Par_EtoilePort
Message (payload) : <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:accepter xmlns:ns2="http://soap/"><arg0>97</arg0><arg1><numero>3</numero></arg1></ns2:accepter></soap:Body></soap:Envelope>
  • Décrire la réponse, supposé positive.
réponse : <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body><ns2:accepterResponse xmlns:ns2="http://soap/"><return><id><numero>3</numero></id><valide>true</valide></return></ns2:accepterResponse></soap:Body></soap:Envelope> 

3.3 Service Restful sans état (stateless)

Créer un nouveau projet Dynamic Web Project pour le service Restful. Ajouter la bibliothèque cxf au projet. Y importer l'archive codeFourni_restServeur.zip fournissant un paquet restful.

  • interface Automate
  • interfaces Session et Resultat
  • implémentations de Session et Resultat
  • implémentation de Automate (pour reconnaître le langage (ab)*
  • deux adaptateurs JAXB nécessaires à cause du typage utilisant des interfaces dont les valeurs sont traduites en documents XML
  • une classe Lancement pour lancer le service

Relativement à la version soap, la seule différence réside dans la présence de l'état de l'automate 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.

  1. 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.
  2. Les chemins relatifs d'accès aux ressources correspondant aux deux méthodes sont les suivants :
    • initier : automate/etat/initial,
    • accepter : automate/etat/suivant.

    Ainsi, si l'URI de base est http://localhost:8080/Projet, une URI sera par exemple : http://localhost:8080/Projet/automate/etat/initial. (Pour indiquer un chemin C, on utilise l'annotation @Path("C").)

  3. Les sessions sont sérialisées au format application/xml. (Pour indiquer qu'un résultat est sérialisé suivant un certain format javax.ws.rs.core.MediaType.F, on utilise l'annotation @Produces(MediaType.F) après importation de MediaType.)

(Toutes ces annotations appartiennent au paquet javax.ws.rs.)

Pour représenter les invocations de la méthode, on complète ces chemins par des requêtes, représentant les arguments de la méthode. Déclarer la méthode accepter ainsi :

Session accepter(@QueryParam("lettre") char x, 
                 @QueryParam("") Session id)

Noter que l'annotation @QueryParam("") utilisant une chaîne vide ne fait pas partie du standard JAX-RS mais est propre à CXF. Elle permet de transformer un objet en une suite de couples clé-valeur, après une projection utilisant les méthodes publiques getX contenus dans l'objet, et ceci récursivement jusqu'à obtenir des types primitifs. Pour un objet de type Session, qui contient deux getters, un état et un numéro, on obtient une requête etatExecution=E&numero=n, où E et n sont deux valeurs primitives.

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 demande 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. On s'aperçoit que suivant la technologie utilisée (SOAP ou Rest), ce ne sont pas les mêmes classes qui sont traduites en des documents XML racines.

Vérifier la présence des deux fichiers de configuration web.xml et restful-beans.xml placés dans le répertoire WebContent/WEB-INF.

Déployer le service de deux manières, sur le serveur Tomcat ou sur un serveur dans CXF en utilisant la classe Lancement. Modifier l'adresse dans cette dernière classe pour qu'elle corresponde à celle utilisée par Tomcat.

Cf. http://cxf.apache.org/docs/jaxrs-services-configuration.html pour plus de détails.

3.3.2 Rappel sur le protocole HTTP et les URI

Cf. le cours.

3.3.3 Client

Créer un nouveau projet Java en y incluant la bibliothèque de CXF (Runtime). Y importer à la racine l'archive codeFourni_restClient.zip fournissant le paquet restful. Ce paquet côté client diffère de celui côté serveur par le retrait de la classe principale Lancement (lancement d'un serveur) et l'ajout de la classe Client d'une part, et par l'absence de l'implémentation de l'automate d'autre part.

Annoter l'interface Automate comme précédemment.

Dans la classe restful.Client, implémenter le scénario suivant dans la fonction principale.

  • Créer un automate (en utilisant la fabrique JAXRSClientFactory.create).
  • Initialiser un mot avec le tableau de caractères {'a', 'b', 'a', 'b', 'a', 'b'}.
  • Appeler le service 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 : GET
Adresse : http://localhost:8080/ProjetServeurRest/automate/etat/suivant?lettre=a&etatExecution=UN&numero=3 
Message (payload) : vide
Réponse : <?xml version="1.0" encoding="UTF-8" standalone="yes"?><resultat><id><etat>DEUX</etat><numero>3</numero></id><valide>true</valide></resultat>

Comment faire pour obtenir une réponse au format JSON ? Quelle est la réponse alors ? Il faut annoter la méthode Automate.accepter avec @Produces(MediaType.APPLICATION_JSON), côté serveur et client.

Réponse : {"resultat":{"id":{"etat":"DEUX","numero":3},"valide":true}} 

Petite variante pour le service Restful : actuellement, lorsqu'on fait une requête accepter, on utilise une URI de la forme suivante.

http://localhost:8080/ProjetServeurRest/automate/etat/suivant?lettre=a&etatExecution=UN&numero=3 

Il est possible d'intégrer une partie de la requête dans le chemin, même si les valeurs des clés évoluent.

Annoter la méthode accepter de l'interface Automate de la manière suivante.

@GET
@Path("automate/etat/suivant/{lettre}")
@Produces(MediaType.APPLICATION_JSON)
Resultat accepter(@PathParam("lettre") char x, @QueryParam("") Session id);

La valeur du paramètre de chemin lettre, correspondant à l'argument x, est utilisée dans le chemin, qui devient variable. Voici la nouvelle URI.

http://localhost:8080/ProjetServeurRest/automate/etat/suivant/a?numero=3&etatExecution=UN

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 Le service WS*

Dans le projet client de type WS*, créer une seconde classe de Test, TestConcurrence, contenant la fonction principale suivante.

public static void main(String[] args) {
  int MAX = 1000; // à augmenter possiblement
  Automate a = soap.Automate_ParABParEtoilePort_Client.getServiceAutomate();
  Session[] sessions = new Session[MAX];
  for(int i = 0; i < MAX ; i++){
    sessions[i]= a.initier();
    System.out.println(sessions[i].getNumero());
  }
  for(int i = 0; i < MAX ; i++){
    a.clore(sessions[i]);;
    System.out.println(sessions[i].getNumero());
  }
}

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 Par_AB_Par_EtoileConcurrent implémentant l'interface Automate.
  • 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.
  • A partir de l'interface Automate, engendrer un second fichier WSDL, automateConcurrent.wsdl, en choisissant la nouvelle classe d'implémentation Par_AB_Par_EtoileConcurrent. Celui-ci déclare le nouveau service utilisant l'automate concurrent.
  • Vérifier que le fichier de configuration WebContent/WEB-INF/cxf-beans.xml déclare les deux services à déployer (la version séquentielle et celle concurrente).

Il reste à tester le nouveau service. On procède comme précédemment en important le fichier WSDL dans l'application cliente et en engendrant le code dans un nouveau paquet, puis en développant une seconde fonction de test.

3.4.2 Le service Restful

On développe la classe Par_AB_Par_EtoileConcurrent, une seconde implémentation de l'automate avec contrôle de la concurrence. Les deux automates seront accessibles aux adresses suivantes :

  • automate/sequentiel et
  • automate/concurrent.

Modifier les chemins d'accès dans l'interface Automate ainsi.

public interface Automate {
  ...
  @Path("etat/initial")
  Session initier();
  ...
  @Path("etat/suivant")
  Resultat accepter(@QueryParam("lettre") char x, @QueryParam("") Session id);
}

Le chemin indiqué par l'annotation Path au-dessus d'une méthode donne le suffixe du chemin menant à la ressource. Cette annotation est définie dans la classe implémentant la ressource, ou à défaut dans un type parent comme l'interface Automate ici. Le préfixe est donné par le chemin indiqué par l'annotation Path au-dessus de la déclaration de la classe d'implémentation (ici, Par_AB_Par_Etoile ou Par_AB_Par_EtoileConcurrent), ou à défaut au-dessus de la déclaration d'un type parent (soit l'interface Automate ici).

Déclarer ainsi les classes d'implémentation.

@Path("automate/sequentiel")
public class Par_AB_Par_Etoile implements Automate { ... }

@Path("automate/concurrent")
public class Par_AB_Par_EtoileConcurrent implements Automate { ... }

Ajouter le contrôle de concurrence.

  • 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 + " *************");
    }
    

Déclarer les deux ressources dans le fichier de configuration WebContent/WEB-INF/restful-beans.xml.

<jaxrs:server id="testAutomatesRestfulStatelessConcurrent" address="/">
  <jaxrs:serviceBeans>
      <ref bean="automateABEtoile" />
      <ref bean="automateABEtoileConcurrent" />
  </jaxrs:serviceBeans>
  <jaxrs:features>
    <cxf:logging />
  </jaxrs:features>
</jaxrs:server>
<bean id="automateABEtoile" class="restful.Par_AB_Par_Etoile" />
<bean id="automateABEtoileConcurrent" class="restful.Par_AB_Par_EtoileConcurrent" />

A ce stade, il est possible d'accéder aux ressources, une fois déployées sur le serveur Tomcat.

POST on http://localhost:8080/ProjetServeurRest/automate/concurrent/etat/initial 
Status : 200 OK

POST on http://localhost:8080/ProjetServeurRest/automate/sequentiel/etat/initial
Status : 200 OK

Dans l'application cliente, mettre à jour l'interface Automate, précisément les chemins d'accès. Réaliser une première classe de test.

public class TestConcurrence {
  public static void main(String[] args) {
    int MAX = 1000; // à augmenter possiblement
    Automate a = JAXRSClientFactory.create(
                   "http://localhost:8080/TestTP1_restServeurAutomate/automate/sequentiel",
                   Automate.class);
    Session[] sessions = new Session[MAX];
    for(int i = 0; i < MAX ; i++){
      sessions[i]= a.initier();
      System.out.println(sessions[i].getNumero());
    }
  }
}

Réaliser une seconde classe de test en appelant l'automate concurrent. Tester et comparer.

Avec le contrôle de la concurrence, les pertes en écriture disparaissent.

Last Updated 2016-01-06T07:35+0100. Comments or questions: Send a mail.