Skip to content

15. [TD] : création d'un client pour le service web

Mots clés : architecture multicouche, Spring, injection de dépendances, service web / jSON, client / serveur

15.1. Support

  

Les projets de ce chapitre seront trouvés dans le dossier [support / chap-15].

15.2. L'architecture client / serveur

Nous voulons créer l'architecture client / serveur suivante :

La couche [ui] sera celle déjà développée aux paragraphes 9 et 10. Cela sera possible parce que la couche [métier] ci-dessus implémentera la même interface [IElectionsMetier] que la couche [métier] du paragraphe 8 :

package elections.client.metier;

import elections.client.entities.ListeElectorale;

public interface IElectionsMetier {

    // obtenir les listes en compétition
    public ListeElectorale[] getListesElectorales();

    // le nombre de sièges à pourvoir
    public int getNbSiegesAPourvoir();

    // le seuil électoral
    public double getSeuilElectoral();

    // l'enregistrement des résultats
    public void recordResultats(ListeElectorale[] listesElectorales);

    // le calcul des sièges
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

}

Dans le paragraphe 7, la couche [DAO] échangeait des données avec un SGBD. Ici la couche [DAO] échange des données avec un serveur web / jSON.

Dans un premier temps, nous nous intéresserons à l'architecture suivante :

15.3. Le projet Eclipse

Le projet Eclipse est le suivant :

Cette structure reprend celle du projet exemple du paragraphe 13.6.1, page 245. Nous allons suivre la même démarche.

15.4. Configuration Maven

C'est celle décrite au paragraphe 13.6.2, page 246.

15.5. Implémentation de la couche [DAO]

  
  • le package [elections.client.config] contient la configation Spring de la couche [DAO] ;
  • le package [elections .client.dao] contient l'implémentation de la couche [DAO] ;
  • le package [elections .client.entities] contient les objets échangés avec le service web / jSON ;
  • le package [elections .client.metier] contient la couche [métier]
  • le package [elections .client.ui] contient la couche [UI]

15.5.1. Configuration de la couche [métier]

  

La classe [MetierConfig] fait la configuration Spring de la couche [métier]. Son code est le suivant :

package elections.client.config;

import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Scope;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;

@ComponentScan({ "elections.client.dao","elections.client.metier" })
public class MetierConfig {

    // constantes
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8080";

    @Bean
    public RestTemplate restTemplate(int timeout) {
        // création du composant RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // timeout des échanges
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // résultat
        return restTemplate;
    }

    @Bean
    public int timeout() {
        return TIMEOUT;
    }

    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }

    // mappeur jSON
    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
}

Ce code a été expliqué au paragraphe 13.6.3.1, page 248. Il est plus simple car il n'y a pas ici de filtres jSON à gérer.

15.5.2. Les entités

  

Les entités manipulées par les couches [DAO] et [métier] sont celles qu'elles échangent avec le service web / jSON. Ce sont les objets de type [ElectionsConfig] et [ListeElectorale]. Côté serveur, ces entités avaient des annotations de persistence JPA. Ici, ces annotations ont été enlevées. Nous redonnons le code des entités pour rappel :

[AbstractEntity]

package spring.webjson.client.entities;


public abstract class AbstractEntity {
    // propriétés
    protected Long id;
    protected Long version;

    // constructeurs
    public AbstractEntity() {

    }

    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }

    // redéfinition [equals] et [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }

    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return id != null && this.id == other.id.longValue();
    }

    // getters et setters
    ...
}

[ElectionsConfig]

package elections.webjson.client.entities;


public class ElectionsConfig extends AbstractEntity {

    // champs
    private int nbSiegesAPourvoir;
    private double seuilElectoral;

    // constructeurs
    public ElectionsConfig() {

    }

    public ElectionsConfig(int nbSiegesAPourvoir, double seuilElectoral) {
        this.nbSiegesAPourvoir = nbSiegesAPourvoir;
        this.seuilElectoral = seuilElectoral;
    }

    // getters et setters
    ...
}

[ListeElectorale]

package elections.webjson.client.entities;


public class ListeElectorale extends AbstractEntity {

    // champs
    private String nom;
    private int voix;
    private int sieges;
    private boolean elimine;

    // constructeurs
    public ListeElectorale() {
    }

    public ListeElectorale(String nom, int voix, int sieges, boolean elimine) {
        setNom(nom);
        setVoix(voix);
        setSieges(sieges);
        setElimine(elimine);
    }

    // getters et setters
    ...
}

15.5.3. L'interface de la couche [DAO]

  

La couche [DAO] présente l'interface [IClientDao] suivante :

1
2
3
4
5
6
7
8
package elections.client.dao;

public interface IClientDao {

    // requête générique
    String getResponse(String url, String jsonPost);

}

L'interface n'a qu'une unique méthode [getResponse] :

  • le 1er paramètre est l'URL du serveur à interroger ;
  • le second paramètre est la valeur jSON de la valeur à poster, null s'il n'y a rien à poster ;
  • le résultat est la chaîne jSON d'un objet [Response<T>] où la classe [Response] a été décrite au paragraphe 14.7, page 269 ;

15.5.4. Implémentation des échanges avec le service web / jSON

  

La classe [ClientDao] implémente l'interface [IClientDao] de la façon suivante :

package elections.client.dao;

import java.net.URI;
import java.net.URISyntaxException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

import elections.client.entities.ElectionsException;

@Component
public class ClientDao implements IClientDao {

    // data
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // requête générique
    @Override
    public String getResponse(String url, String jsonPost) {

        try {
            // url : URL à contacter
            // jsonPost : la valeur jSON à poster

            // exécution requête
            RequestEntity<?> request;
            if (jsonPost != null) {
                // requête POST
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
            } else {
                // requête GET
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            // on exécute la requête
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new ElectionsException(200, e1);
        } catch (RuntimeException e2) {
            throw new ElectionsException(201, e2);
        }
    }
}

Ce code a été décrit page 254.

15.6. Implémentation de la couche [métier]

 

Comme il a été dit, la couche [métier] présente la même interface [IElectionsMetier] que dans le paragraphe 8.4, page 128 :

package elections.client.metier;

import elections.client.entities.ListeElectorale;

public interface IElectionsMetier {

    // obtenir les listes en compétition
    public ListeElectorale[] getListesElectorales();

    // le nombre de sièges à pourvoir
    public int getNbSiegesAPourvoir();

    // le seuil électoral
    public double getSeuilElectoral();

    // l'enregistrement des résultats
    public void recordResultats(ListeElectorale[] listesElectorales);

    // le calcul des sièges
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales);

}

Cette interface est implémentée par la classe [ElectionsMetier] suivante :

package elections.client.metier;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import elections.client.dao.IClientDao;
import elections.client.entities.ElectionsConfig;
import elections.client.entities.ElectionsException;
import elections.client.entities.ListeElectorale;

@Component
public class ElectionsMetier implements IElectionsMetier {

    @Autowired
    private IClientDao dao;
    @Autowired
    private ApplicationContext context;

    // configuration de l'élection
    private ElectionsConfig electionsConfig;

    @PostConstruct
    public void init() {
        // mappeurs jSON
        ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
        try {
            // requête
            Response<ElectionsConfig> response = mapperResponse.readValue(dao.getResponse("/getElectionsConfig", null),
                    new TypeReference<Response<ElectionsConfig>>() {
                    });
            // erreur ?
            if (response.getStatus() != 0) {
                // on lance 1 exception
                throw new ElectionsException(response.getStatus(), response.getMessages());
            } else {
                electionsConfig = response.getBody();
            }
        } catch (ElectionsException e1) {
            throw e1;
        } catch (Exception e2) {
            throw new ElectionsException(100, getMessagesForException(e2));
        }
    }

    @Override
    public ListeElectorale[] getListesElectorales() {
        ...
    }

    @Override
    public int getNbSiegesAPourvoir() {
        return electionsConfig.getNbSiegesAPourvoir();
    }

    @Override
    public double getSeuilElectoral() {
        return electionsConfig.getSeuilElectoral();
    }

    @Override
    public void recordResultats(ListeElectorale[] listesElectorales) {
    ...
    }

    @Override
    public ListeElectorale[] calculerSieges(ListeElectorale[] listesElectorales) {
...
    }

    // liste des messages d'erreur d'une exception
    private List<String> getMessagesForException(Exception exception) {
        // on récupère la liste des messages d'erreur de l'exception
        Throwable cause = exception;
        List<String> erreurs = new ArrayList<String>();
        while (cause != null) {
            // on récupère le message seulement s'il est !=null et non blanc
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // cause suivante
            cause = cause.getCause();
        }
        return erreurs;
    }
}

Le type [Response] utilisé ligne 37 est la réponse du serveur web / jSON décrite au paragraphe 14.7, page 269 ;

Travail à faire : en suivant le paragraphe 13.6.3.7, page 255, complétez la classe [ElectionsMetier] ;

15.7. Le test Junit

Revenons à l'architecture client / serveur en cours de construction :

La couche [JUnit] [1] communique avec la couche [Métier] du serveur [5] au travers des couches [2-4]. En faisant en sorte que les couches [Métier] [2] et [5] aient la même interface, on rend les couches [2-4] transparentes. La couche [1] a l'impression de communiquer directement avec la couche [5]. Le point intéressant est qu'en [1] on va pouvoir utiliser le test JUnit qui avait été utilisé pour tester la couche [Métier] [5].

  

Travail à faire : passez le test JUnit du projet pour vérifier votre implémentation et du serveur et de son client.

15.8. Implémentation de la couche [UI]

Revenons à l'architecture que nous voulons construire :

Maintenant que la couche [métier] [2] a été construite et testée, nous pouvons construire la couche [ui] [1].

Comme l'interface [IElectionsMetier] de la couche [métier] est identique à celle du projet décrit au paragraphe 8, page 125, nous pouvons en [3], copier le projet de la couches [ui] du paragraphe 10, page 139. Ce projet était un projet Netbeans. Il suffit de faire un copier / coller des classes Java concernées de Netbeans vers Eclipse. Ceci fait, il y a des ajustements de packages et d'imports à faire.

On fera de même pour les classes exécutables du package [elections.client.boot] [4].

La classe [AbstractBootElections] est la suivante :

package elections.client.boot;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import elections.client.config.UiConfig;
import elections.client.entities.ElectionsException;
import elections.client.ui.IElectionsUI;

public abstract class AbstractBootElections {

    // récupération du contexte Spring
    protected AnnotationConfigApplicationContext ctx;

    public void run() {
        // instanciation couche [ui]
        IElectionsUI electionsUI = null;
        try {
            // récupération du contexte Spring
            ctx = new AnnotationConfigApplicationContext(UiConfig.class);
            // récupération de la couche [ui]
            electionsUI = getUI();

...
  • ligne 19 : le contexte Spring défini par la classe de configuration [UiConfig] est instancié. Cette classe est la suivante :
  

La classe [UiConfig] est la suivante :

1
2
3
4
5
6
7
8
9
package elections.client.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

@Import(MetierConfig.class)
@ComponentScan(basePackages = { "elections.client.ui" })
public class UiConfig {
}
  • ligne 6 : on importe les beans de la couche [métier] ;
  • ligne 7 : on indique qu'il y des beans Spring dans le package [elections.client.ui] ;

Travail à faire : vérifiez que les versions console et swing de la couche [ui] fonctionnent.