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. Nous allons suivre la même démarche.
15.4. Configuration Maven
C'est celle décrite au paragraphe 13.6.2.
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. 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 :
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;
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 au paragraphe 13.6.3.6.
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 :
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 ;
Travail à faire : en suivant le paragraphe 13.6.3.7, 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 paragraphe8, nous pouvons en [3], copier le projet de la couches [ui] du paragraphe 10. 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 :
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.















