Accueil > Non classé > Construire sa propre plateforme de services

Construire sa propre plateforme de services

Je vais vous décrire une petite plateforme de services que j’ai eu l’occasion de concevoir et de réaliser.

Parce que tout a un début…

Et en l’occurrence, le début est ici une besoin fonctionnel. Vous me direz tous que c’est normal, mais on a parfois tendance à oublier que la solution technique n’existe que pour répondre à un besoin fonctionnel.
Dans notre cas, nous avions besoin de mettre en place une plateforme permettant à des éléments d’un système informatique (que nous appellerons par la suite “consommateurs”) d’appeler des services, traitements ou ressources distantes (que nous appellerons systèmes externes).
Cette plateforme doit présenter un nombre limité de points d’entrées (d’interfaces) à ses consommateurs. Chacun de ces points d’entrées doit faire correspondre une requête du consommateur à un appel de processus métier. Ces processus peuvent alors orchestrer des appels de services distants puis traiter et agréger les données retournées par ces appels. Les processus et services doivent être faciles à ajouter dans la plateforme et doivent avoir un couplage faible avec leurs interfaces. De plus, les systèmes externes sollicités par la plateforme exposent des services et interfaces qui leurs sont propres et qui ne doivent pas impacter le format des requêtes des consommateurs.

Le tout, c’est d’avoir un plan

Tenant compte de tous les éléments énoncés ci-dessus, j’ai modélisé (dans un premier temps) notre plateforme de la façon suivante:



Jusque là, rien de transcendant me direz-vous. J’y ai représenté différents cas pouvant se produire :

  • Nous avons une boite noire qui reçoit une requête de la part d’un consommateur X et appelle la “source de données” A.
  • La requête du consommateur Y donne lieu à des appels aux sources B et C. La plateforme héberge un processus qui orchestre les deux appels.
  • La requête du consommateur X ne déclenche aucun appel de service (on pourrait imaginer que les services correspondants n’ont pas été écrits).

Par la suite, nous allons préciser peu à peu ce modèle pour faire apparaitre les différentes briques de notre plateforme et leurs responsabilités. Les fonctions à implémenter sont les suivantes :

Distribuer les appels de services

Une requête provenant d’un consommateur doit déclencher le processus métier adéquat. C’est pour cela que la première étape du traitement de la requête sera son analyse par un annuaire de services.

Traiter les processus métiers

Une fois le bon processus identifié, celui-ci est appelé. Son rôle est de contacter les différents services nécessaires à son fonctionnement, organiser leurs appels, rassembler et agréger les résultats pour les retourner au consommateur.

Traduire les paramètres

Les systèmes externes auxquels les processus métiers font appel exposent leurs propres services et APIs. Les paramètres transmis dans la requête d’un consommateur ne doivent en aucun cas être contraints par ces interfaces. C’est pour cela qu’un composant appelé “connecteur” propre à chaque service externe aura la responsabilité de transcrire les paramètres dans un format attendu par le service. Les classes de ces paramètres (transmis à la plateforme) seront définies dans l’API de la plateforme.

Contacter les services

Les paramètres transcrits et intégrés dans la requête du service (sous forme d’objets JAXB, chaîne de caractères, etc…), le connecteur a la charge d’établir la connexion vers le système distant et d’en appeler le service ciblé.

Traduire les réponses

Comme pour les paramètres, nous allons devoir traiter les réponses des appels de services en les transcrivant dans des objets transverses. La transcription aura lieu dans le connecteur associé au service. Les classes d’objets transverses seront définies dans l’API de notre plateforme.

Nous avons maintenant un découpage plus précis de notre plateforme qui la fait ressembler à ceci :


J’aime quand un plan se déroule sans accroc.


Maintenant que nous avons fait apparaître les différents composants de notre plateforme, nous devons choisir les solutions techniques qui vont intervenir dans leur réalisation. Différentes problématiques sont apparues :

L’accès à la plateforme

La plateforme doit être accessible d’une machine distante et la solution la plus évidente est de lui faire exposer des web services. Dans notre cas, j’ai eu une préférence pour des services RESTful. Cette solution nous permettra de simplifier la détection du processus à appeler en tenant compte de la méthode HTTP comme premier filtrage de la requête. De plus, l’entête HTTP peut nous permettre de transmettre des meta-données nécessaires à la désérialisation des objets paramètres transmis (par exemple). Pour cela, nous avons choisi d’utiliser la librairie Jersey (implémentation de référence de la JSR 311).
Parmi les formats de retour disponibles, nous avons retenu le JSON pour sa concision et sa légèreté. En effet, certains services devront retourner des quantités de données conséquentes qu’il est inutile d’alourdir avec des balises XML.

Instanciation, initialisation et découplage

Le découplage est ici essentiel puisque les composants tels que les connecteurs sont censés pouvoir être interchangés. Imaginons un processus de recherche de contenu qui interroge une GED via un connecteur conçu à cet effet. Si pour une raison quelconque on décidait de changer de GED, il faudrait ne réécrire que le composant connecteur.

La plupart des objets de notre plateforme ne devront être instanciés qu’une seule fois. Je pense en particulier aux clients Jersey définis dans les couches DAO de certains connecteurs et dont l’instanciation coûte cher.

Vous vous en doutez, la solution à ces deux problèmes est Spring Framework qui nous permet de contrôler l’instanciation de nos classes et d’assurer le découplage de nos composants. Entre autres bienfaits, le découplage par Spring nous permettra aussi de faciliter la mise en place de tests unitaires (en pratiquant la programmation par interface et pourquoi pas le TDD). Nous verrons par la suite que Spring nous permettra de résoudre d’autre problèmes plus spécifiques.

Implémenter l’annuaire de services

Nous avons besoin d’une implémentation souple permettant l’ajout (fréquent) de processus métiers supplémentaires identifiés par des “clés”. Ces clés nous permettront de faire correspondre une requête de l’un de nos consommateurs à un processus métier à exécuter.

La solution d’un bourrinisme achevé consisterait en un immonde gigantesque switch. Cette solution nous ferait modifier le code java et rajouter inutilement de la complexité cyclomatique à chaque ajout de nouveau processus métier dans notre plateforme.
M’inspirant de cet article, j’ai eu l’idée de définir mes processus métiers dans Spring (jusque là c’était prévu) et de les regrouper dans une map instanciée et gérée par ce même Spring. Ainsi, en une poignée de lignes de codes, je récupère et exécute le processus correspondant à ma requête et ces lignes ne changeront jamais pourvu que les interfaces de mes processus soient bien pensées avec des paramètres génériques (merci java 5.0 ^_^°). Voici à quoi ressemble la configuration de tout ceci dans l’applicationContext.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<beans xmlns="http://www.springframework.org/schema/beans"
   ...
   xmlns:util="http://www.springframework.org/schema/util"
   ...
   xsi:schemaLocation="... http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">

    <!-- Import des applicationContexts des connecteurs -->
    <import resource="classpath:applicationContext-customerConnector.xml" />
    <import resource="classpath:applicationContext-providererConnector.xml" />
    ...

    <!-- Point d'entree de la plateforme -->
    <bean id="serviceEntryPoint" class="chemin.vers.ma.plateforme.ServiceEntryPoint">
        <property name="loadProcessMap" ref="loadProcessMap"/>
        <property name="writeProcessMap" ref="writeProcessMap"/>
    </bean>
   
    <!-- map de processus de chargement/lecture de données-->
    <util:map id="loadProcessMap">
        <entry key="loadCustomerData"  value-ref="customerLoader"/>
        <entry key="loadProviderData"  value-ref="providerLoader"/>
        ...
    </util:map>

    <!-- map de processus d'écriture de données-->
    <util:map id="writeProcessMap">
        <entry key="writeCustomerData"  value-ref="customerLoader"/>
        ...
    </util:map>

    <!-- beans de processus -->
    <bean id="customerLoader" class="chemin.vers.mes.processus.SimpleLoadProcess">
        <property name="connector" ref="customerConnector"/>
    </bean>

    <bean id="providerLoader" class="chemin.vers.mes.processus.ProviderLoaderImpl">
        <property name="connector" ref="providerConnector"/>
    </bean>
   
    <bean id="customerWriter" class="chemin.vers.mes.processus.CustomerWriterImpl">
        <property name="connector" ref="customerConnector"/>
    </bean>
   
    ...
</beans>

Ainsi, l’implémentation du point d’entrée (avec Jersey) avec son annuaire de service est écrite de la façon suivante :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package chemin.vers.ma.plateforme;

@Path("/maPlateforme")
public class ServiceEntryPoint{

    private Map<String, LoadProcess<Item, Param>> loadProcessMap;
    private Map<String, WriteProcess<Item>> writeProcessMap;

    @POST
    @Consumes(MediaType.APPLICATION_XML)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("{processusName}")
    public Item search(
        @PathParam("processName") String processName,
        Param param){
        LoadProcess<Item, Param> process= loadProcessMap.get(processName);
        if(process==null){
            throw new IllegalArgumentException("the [processName] path parameter does not match an existing process.");
        }
        Item result = process.load(param);
        return result;
    }

    public void setLoadProcessMap(
        Map<String, Processus<Item, Param>> loadProcessMap) {
            this.loadProcessMap= loadProcessMap;
    }

    public void setWriteProcessMap(
        Map<String, Processus<Item>> writeProcessMap) {
            this.writeProcessMap= writeProcessMap;
    }
}

Une implémentation possible de processus métier pourrait avoir la forme suivante :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * Implementation d'un processus de lecture simple.
 *
 * @param <I> définit la classe de l'entité retournée.
 * @param <P> définit la classe du paramètre identifiant l'objet à retourner.
 */

public class SimpleLoadProcess<I extends Item, P extends Param> implements LoadProcess<I, P> {

    protected  LoaderConnector<I, P> connector;
 
    @Override
    public I load(P parametre) {
        I result  = connector.loadObject(parametre);
        return result;
    }

    public void setConnector(LoaderConnector<I, P> connector) {
        this.connector = connector;
    }
}
Construction d’un connecteur

Compte tenu du rôle joué par un connecteur, sa structure sera presque toujours articulée en 3 parties :

  • Un interpréteur de critères qui transcrit les entrées du service en un format que pourra traiter le service distant.
  • Un DAO qui gère la connexion au service et l’envoi de la requête.
  • Un constructeur de réponses qui initialise une structure d’objets à partir de la réponse du service appelé.

Regardons par exemple le fichier applicationContext-customerConnector.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    <!-- Connecteur -->
    <bean id="customerConnector" class="chemin.vers.CustomerConnectorImpl">
        <property name="idTranslator" ref="customerIdTranslator"/>
        <property name="customerTranslator" ref="customerTranslator"/>
        <property name="dao" ref="customerDao"/>
        <property name="responseBuilder" ref="customerResponseBuilder"/>
    </bean>
 
    <!-- DAO-->
    <bean id="customerDao" class="chemin.vers.CustomerDao">
        <property name="baseUrl" value="${customer.service.url.base}"/>
        <property name="login" value="${customer.service.login}"/>
        <property name="password" value="${customer.service.password}"/>
    </bean>
 
    <!-- interpeteurs de parametres -->
    <bean id="customerIdTranslator" class="chemin.vers.CustomerIdTranslator"/>
    <bean id="customerTranslator" class="chemin.vers.CustomerTranslator"/>
 
    <!-- constructeur de reponse -->
    <bean id="customerResponseBuilder" class="chemin.vers.CustomerResponseBuilder"/>

Avec pour implémentation du bean customerConnector :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class CustomerConnectorImpl implements LoaderConnector<Customer, CustomerId>, WriterConnector<Customer> {

    private ParamTranslator<CustomerId, CustomerIdDTO> idTranslator;
    private ParamTranslator<Customer, CustomerDTO> customerTranslator;
    private CustomerDaoImpl customerDao;
    private ResponseBuilder<CustomerDTO, Customer> responseBuilder;

    /**
     *@see chemin.vers.LoaderConnector.loadObject()
     */

    @Override
    public Customer loadObject(CustomerId id){
        CustomerIdDTO idDTO = idTranslator.translate(id);
        CustomerDTO customerDTO = customerDao.loadObject(idDTO);
        return responseBuilder.translate(customerDTO);
    }


    /**
      *@see chemin.vers.WriterConnector.writeObject()
      */

     @Override
    public void writeObject(Customer entite){
        CustomerDTO customerDTO = customerTranslator.translate(entite);
        customerDao.writeObject(customerDTO);
    }

    public void setIdTranslator(ParamTranslator<CustomerId, CustomerIdDTO> idTranslator){
        this.idTranslator = idTranslator;
    }

    public void setCustomerTranslator(ParamTranslator<Customer, CustomerDTO> customerTranslator){
        this.customerTranslator=customerTranslator;
    }

    public void setCustomerDao(CustomerDaoImpl customerDao){
        this.customerDao=customerDao;
    }

    public void setResponseBuilder(ResponseBuilder<CustomerDTO, Customer> responseBuilder){
        this.responseBuilder=responseBuilder;
    }
}
La transcription des paramètres et des réponses

Là encore la transcription des POJOs de l’API de la plateforme en POJOs de l’API d’un service externe peut être faite de façon peu subtile :

1
pojo1.setX(pojo2.getX());

Cette solution serait à la fois fastidieuse à écrire et à maintenir. C’est pour cela que nous préférons utiliser Dozer qui a le bon goût de s’intégrer simplement dans la configuration Spring de notre connecteur:

1
2
3
4
5
6
7
8
9
10
11
    <bean id="monMapper" class="org.dozer.DozerBeanMapper">
        <property name="mappingFiles">
            <list>
                <value>dozer.xml</value>
            </list>
        </property>
    </bean>

    <bean id="customerTranslator" class="chemin.vers.CustomerTranslator">
        <property name="dozerMapper" ref="monMapper"/>
    </bean>

Ce qui nous donne l’implémentation suivante :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CustomerTranslator implements
        ParamTranslator<Customer, CustomerDTO> {
   
    private DozerBeanMapper dozerMapper;
   
    @Override
    public CustomerDTO translate(Customer customer) {
        CustomerDTO dto= dozerMapper.map(customer, CustomerDTO.class);
        return dto;
    }

    public void setDozerMapper(DozerBeanMapper dozerMapper) {
        this.dozerMapper = dozerMapper;
    }
}

En conclusion


La flexibilité et l’ouverture de Spring nous permettent d’envisager les améliorations suivantes :

  • Tissage d’aspects à la compilation (par exemple pour générer des logs de statistiques).
  • Intégration de frameworks, ORM et webservices.

Ceci conclut cet article. Vous voilà maintenant prêts à monter votre propre ESB maison.

Share
  1. Mathieu GRENONVILLE
    03/04/2012 à 16:29 | #1

    Désolé de déterrer cet article, mais je me permets de partager une petite killer-feature de Spring. En effet, on peut s’abstenir de la configuration :

    ainsi que :

    en utilisant simplement l’annotation @Autowired sur le setter/champ de ServiceEntryPoint. Spring va alors fournir une map avec le nom du bean et l’implementation correspondante.

    Ça marche aussi pour les Collections.

    Merci spring :)

  2. Mathieu GRENONVILLE
    03/04/2012 à 16:33 | #2

    Je parlais évidemment de :

    <util:map id="writeProcessMap">
    <entry key="writeCustomerData" value-ref="customerLoader"/>

    </util:map>

    et de :
    <bean id="serviceEntryPoint" class="chemin.vers.ma.plateforme.ServiceEntryPoint">
    <property name="loadProcessMap" ref="loadProcessMap"/>
    <property name="writeProcessMap" ref="writeProcessMap"/>
    </bean>

  3. Stéphane LANDELLE
    19/06/2012 à 11:03 | #3

    Cool, j’avoue que j’avais complètement oublié cette feature et que j’ai enfin la 3ème réponse d’une certaine question dans un certain cours Spring dans un certain outil d’e-coaching…

  1. Pas encore de trackbacks