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 :
| 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 :
| 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.