13. [Cours] : Exposer une base de données sur le web avec Spring MVC
Mots clés : architecture multicouche, Spring, injection de dépendances, service web / jSON, client / serveur
13.1. Support
Les projets de ce chapitre seront trouvés dans le dossier [support / chap-13]. Le script SQL [dbintrospringdata.sql] permet de créer la base MySQL nécessaire aux tests.
13.2. La place de Spring MVC dans une application Web
Situons Spring MVC dans le développement d'une application Web. Le plus souvent, celle-ci sera bâtie sur une architecture multicouche telle que la suivante :
 |
- la couche [Web] est la couche en contact avec l'utilisateur de l'application Web. Celui-ci interagit avec l'application Web au travers de pages Web visualisées par un navigateur. C'est dans cette couche que se situe Spring MVC et uniquement dans cette couche ;
- la couche [métier] implémente les règles de gestion de l'application, tels que le calcul d'un salaire ou d'une facture. Cette couche utilise des données provenant de l'utilisateur via la couche [Web] et du SGBD via la couche [DAO] ;
- la couche [DAO] (Data Access Objects), la couche [ORM] (Object Relational Mapper) et le pilote JDBC gèrent l'accès aux données du SGBD. La couche [ORM] fait un pont entre les objets manipulés par la couche [DAO] et les lignes et les colonnes des tables d'une base de données relationnelle. La spécification JPA (Java Persistence API) permet de s'abstraire de l'ORM utilisé si celui-ci implémente ces spécifications. Ce sera le cas ici et nous appellerons désormais la couche ORM, la couche JPA ;
- l'intégration des couches est faite par le framework Spring ;
13.3. Le modèle de développement de Spring MVC
Spring MVC implémente le modèle d'architecture dit MVC (Modèle – Vue – Contrôleur) de la façon suivante :
Le traitement d'une demande d'un client se déroule de la façon suivante :
- demande - les URL demandées sont de la forme http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Le [Front Controller] utilise un fichier de configuration ou des annotations Java pour " router " la demande vers le bon contrôleur et la bonne action au sein de ce contrôleur. Pour cela, il utilise le champ [Action] de l'URL. Le reste de l'URL [/param1/param2/...] est formé de paramètres facultatifs qui seront transmis à l'action. Le C de MVC est ici la chaîne [Front Controller, Contrôleur, Action]. Si aucun contrôleur ne peut traiter l'action demandée, le serveur Web répondra que l'URL demandée n'a pas été trouvée.
- traitement
- l'action choisie peut exploiter les paramètres parami que le [Front Controller] lui a transmis. Ceux-ci peuvent provenir de plusieurs sources :
- du chemin [/param1/param2/...] de l'URL,
- des paramètres [p1=v1&p2=v2] de l'URL,
- de paramètres postés par le navigateur avec sa demande ;
- dans le traitement de la demande de l'utilisateur, l'action peut avoir besoin de la couche [métier] [2b]. Une fois la demande du client traitée, celle-ci peut appeler diverses réponses. Un exemple classique est :
- une page d'erreur si la demande n'a pu être traitée correctement
- une page de confirmation sinon
- l'action demande à une certaine vue de s'afficher [3]. Cette vue va afficher des données qu'on appelle le modèle de la vue. C'est le M de MVC. L'action va créer ce modèle M [2c] et demander à une vue V de s'afficher [3] ;
- réponse - la vue V choisie utilise le modèle M construit par l'action pour initialiser les parties dynamiques de la réponse HTML qu'elle doit envoyer au client puis envoie cette réponse.
Pour un service web / jSON, l'architecture précédente est légèrement modifiée :
- en [4a], le modèle qui est une classe Java est transformé en chaîne jSON par une bibliothèque jSON ;
- en [4b], cette chaîne jSON est envoyée au navigateur ;
Maintenant, précisons le lien entre architecture web MVC et architecture en couches. Selon la définition qu'on donne au modèle, ces deux concepts sont liés ou non. Prenons une application web Spring MVC à une couche :
Si nous implémentons la couche [Web] avec Spring MVC, nous aurons bien une architecture web MVC mais pas une architecture multicouche. Ici, la couche [web] s'occupera de tout : présentation, métier, accès aux données. Ce sont les actions qui feront ce travail.
Maintenant, considérons une architecture Web multicouche :
La couche [Web] peut être implémentée sans framework et sans suivre le modèle MVC. On a bien alors une architecture multicouche mais la couche Web n'implémente pas le modèle MVC.
Par exemple, dans le monde .NET la couche [Web] ci-dessus peut être implémentée avec ASP.NET MVC et on a alors une architecture en couches avec une couche [Web] de type MVC. Ceci fait, on peut remplacer cette couche ASP.NET MVC par une couche ASP.NET classique (WebForms) tout en gardant le reste (métier, DAO, ORM) à l'identique. On a alors une architecture en couches avec une couche [Web] qui n'est plus de type MVC.
Dans MVC, nous avons dit que le modèle M était celui de la vue V, c.a.d. l'ensemble des données affichées par la vue V. Une autre définition du modèle M de MVC est donnée :
Beaucoup d'auteurs considèrent que ce qui est à droite de la couche [Web] forme le modèle M du MVC. Pour éviter les ambigüités on peut parler :
- du modèle du domaine lorsqu'on désigne tout ce qui est à droite de la couche [Web]
- du modèle de la vue lorsqu'on désigne les données affichées par une vue V
Dans la suite, le terme " modèle M " désignera exclusivement le modèle d'une vue V.
13.4. Un projet web / jSON avec Spring MVC
Le site [http://spring.io/guides] offre des tutoriels de démarrage pour découvrir l'écosystème Spring. Nous allons suivre l'un d'eux pour découvrir la configuration Maven nécessaire à un projet Spring MVC.
13.4.1. Le projet de démonstration
- en [1], nous importons l'un des guides Spring ;
- en [2], nous choisissons l'exemple [Rest Service] ;
- en [3], on choisit le projet Maven ;
- en [4], on prend la version finale du guide ;
- en [5], on valide ;
- en [6], le projet importé ;
Les services web accessibles via des URL standard et qui délivrent du texte jSON sont souvent appelés des services REST (REpresentational State Transfer). Un service est dit Restful s'il respecte certaines règles.
Examinons maintenant le projet importé, d'abord sa configuration Maven.
13.4.2. Configuration Maven
Le fichier [pom.xml] est le suivant :
| <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>
</project>
|
- lignes 6-8 : les propriétés du projet Maven. Manque une balise [<packaging>] indiquant le type du fichier produit par la compilation Maven. En l'absence de celle-ci, c'est le type [jar] qui est utilisé. L'application est donc une application exécutable de type console, et non une application web où le packaging serait alors [war] ;
- lignes 10-14 : le projet Maven a un projet parent [spring-boot-starter-parent]. C'est lui qui définit l'essentiel des dépendances du projet. Elles peuvent être suffisantes, auquel cas on n'en rajoute pas, ou pas, auquel cas on rajoute les dépendances manquantes ;
- lignes 17-20 : l'artifact [spring-boot-starter-web] amène avec lui les bibliothèques nécessaires à un projet Spring MVC de type service web où il n'y a pas de vues générées. Cet artifact amène avec lui un très grand nombre de bibliothèques dont celles d'un serveur Tomcat embarqué. C'est sur ce serveur que l'application sera exécutée ;
Les bibliothèques amenées par cette configuration sont très nombreuses :
Ci-dessus on voit les trois archives du serveur Tomcat.
13.4.3. L'architecture d'un service Spring [web / jSON]
Pour un service web / jSON, Spring MVC implémente le modèle MVC de la façon suivante :
- en [4a], le modèle qui est une classe Java est transformé en chaîne jSON par une bibliothèque jSON ;
- en [4b], cette chaîne jSON est envoyée au navigateur ;
13.4.4. Le contrôleur C
L'application importée a le contrôleur suivant :
| package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
|
- ligne 9 : l'annotation [@RestController] fait de la classe [GreetingController] un contrôleur Spring, ç-à-d que ses méthodes sont enregistrées pour traiter des URL. Nous avons vu l'annotation similaire [@Controller]. Le résultat des méthodes de ce contrôleur était un type [String] qui était le nom de la vue à afficher. Ici c'est différent. Les méthodes d'un contrôleur de type [@RestController] rendent des objets qui sont sérialisés pour être envoyés au navigateur. Le type de sérialisation opérée dépend de la configuration de Spring MVC. Ici, ils seront sérialisés en jSON. C'est la présence d'une bibliothèque jSON dans les dépendances du projet qui fait que Spring Boot va, par autoconfiguration, configurer le projet de cette façon ;
- ligne 14 : l'annotation [@RequestMapping] indique l'URL que traite la méthode, ici l'URL [/greeting] ;
- ligne 15 : nous avons déjà expliqué l'annotation [@RequestParam]. Le résultat rendu par la méthode est un objet de type [Greeting].
- ligne 12 : un entier long de type atomique. Cela signifie qu'il supporte la concurrence d'accès. Plusieurs threads peuvent vouloir incrémenter la variable [counter] en même temps. Cela se fera proprement. Un thread ne peut lire la valeur du compteur que si le thread en train de le modifier a terminé sa modification.
13.4.5. Le modèle M
Le modèle M produit par la méthode précédente est l'objet [Greeting] suivant :
| package hello;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
|
La transformation jSON de cet objet créera la chaîne de caractères {"id":n,"content":"texte"}. Au final, la chaîne jSON produite par la méthode du contrôleur sera de la forme :
| {"id":2,"content":"Hello, World!"}
|
ou
| {"id":2,"content":"Hello, John!"}
|
13.4.6. Exécution
La classe [Application.java] est la classe exécutable du projet. Son code est le suivant :
| package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
|
Nous avons déjà rencontré et expliqué ce code dans l'exemple précédent.
13.4.7. Exécution du projet
Exécutons le projet :
On obtient les logs console suivants :
| . ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.1.9.RELEASE)
2014-11-28 15:22:55.005 INFO 3152 --- [ main] hello.Application : Starting Application on Gportpers3 with PID 3152 (started by ST in D:\data\istia-1415\spring mvc\dvp-final\gs-rest-service)
2014-11-28 15:22:55.046 INFO 3152 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@62e136d3: startup date [Fri Nov 28 15:22:55 CET 2014]; root of context hierarchy
2014-11-28 15:22:55.762 INFO 3152 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2014-11-28 15:22:56.567 INFO 3152 --- [ main] .t.TomcatEmbeddedServletContainerFactory : Server initialized with port: 8080
2014-11-28 15:22:56.738 INFO 3152 --- [ main] o.apache.catalina.core.StandardService : Starting service Tomcat
2014-11-28 15:22:56.740 INFO 3152 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/7.0.56
2014-11-28 15:22:56.869 INFO 3152 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2014-11-28 15:22:56.870 INFO 3152 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1827 ms
2014-11-28 15:22:57.478 INFO 3152 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2014-11-28 15:22:57.481 INFO 3152 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2014-11-28 15:22:57.685 INFO 3152 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.879 INFO 3152 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/greeting],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public hello.Greeting hello.GreetingController.greeting(java.lang.String)
2014-11-28 15:22:57.884 INFO 3152 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.885 INFO 3152 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2014-11-28 15:22:57.906 INFO 3152 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:57.907 INFO 3152 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2014-11-28 15:22:58.231 INFO 3152 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2014-11-28 15:22:58.318 INFO 3152 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080/http
2014-11-28 15:22:58.319 INFO 3152 --- [ main] hello.Application : Started Application in 3.788 seconds (JVM running for 4.424)
|
- ligne 13 : le serveur Tomcat démarre sur le port 8080 (ligne 12) ;
- ligne 17 : la servlet [DispatcherServlet] est présente ;
- ligne 20 : la méthode [GreetingController.greeting] a été découverte ;
Pour tester l'application web, on demande l'URL [http://localhost:8080/greeting] :
On reçoit bien la chaîne jSON attendue. Il peut être intéressant de voir les entêtes HTTP envoyés par le serveur. Pour cela, on va utiliser l'extension de Chrome appelée [Advanced Rest Client] (Chrome / Ctrl-T / Menu [Applications] / [Advanced Rest Client] - cf Annexes page 419) :
- en [1], l'URL demandée ;
- en [2], la méthode GET est utilisée ;
- en [3], la réponse jSON ;
- en [4], le serveur a indiqué qu'il envoyait une réponse au format jSON ;
- en [5], on demande la même URL mais cette fois-ci avec un POST ;
- en [7], les informations sont envoyées au serveur sous la forme [urlencoded] ;
- en [6], le paramètre name avec sa valeur ;
- en [8], le navigateur indique au serveur qu'il lui envoie des informations [urlencoded] ;
- en [9], la réponse jSON du serveur ;
13.4.8. Création d'une archive exécutable
Nous créons maintenant une archive exécutable :
- en [1] : on exécute une cible Maven ;
- en [2] : il y a deux cibles (goals) : [clean] pour supprimer le dossier [target] du projet Maven, [package] pour le régénérer ;
- en [3] : le dossier [target] généré, le sera dans ce dossier ;
- en [4] : on génère la cible ;
Dans les logs qui apparaissent dans la console, il est important de voir apparaître le plugin [spring-boot-maven-plugin]. C'est lui qui génère l'archive exécutable.
| [INFO] --- spring-boot-maven-plugin:1.1.0.RELEASE:repackage (default) @ gs-rest-service ---
|
Avec une console, on se place dans le dossier généré :
| D:\Temp\wksSTS\gs-rest-service\target>dir
...
11/06/2014 15:30 <DIR> classes
11/06/2014 15:30 <DIR> generated-sources
11/06/2014 15:30 11 073 572 gs-rest-service-0.1.0.jar
11/06/2014 15:30 3 690 gs-rest-service-0.1.0.jar.original
11/06/2014 15:30 <DIR> maven-archiver
11/06/2014 15:30 <DIR> maven-status
...
|
- ligne 5 : l'archive générée ;
Cette archive est exécutée de la façon suivante :
| D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.1.0.RELEASE)
2014-06-11 15:32:47.088 INFO 4972 --- [ main] hello.Application
: Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...
|
Maintenant que l'application web est lancée, on peut l'interroger avec un navigateur :
13.4.9. Déployer l'application sur un serveur Tomcat
Comme il a été fait pour le projet précédent, nous modifions le fichier [pom.xml] de la façon suivante :
| <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
...
</project>
|
- ligne 9 : il faut indiquer qu'on va générer une archive war (Web ARchive) ;
Il faut par ailleurs configurer l'application web. En l'absence de fichier [web.xml], cela se fait avec une classe héritant de [SpringBootServletInitializer] :
La classe [ApplicationInitializer] est la suivante :
| package hello;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ApplicationInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
|
- ligne 6 : la classe [ApplicationInitializer] étend la classe [SpringBootServletInitializer] ;
- ligne 9 : la méthode [configure] est redéfinie (ligne 8) ;
- ligne 10 : on fournit la classe qui configure le projet ;
Pour exécuter le projet, on peut procéder ainsi :
- en [1-2], on exécute le projet sur l'un des serveurs enregistrés dans l'IDE Eclipse ;
Ceci fait, on peut demander l'URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] dans un navigateur :
13.4.10. Conclusion
Nous avons introduit un type de projets Spring MVC où l'application web envoie un flux jSON au navigateur. Nous allons développer maintenant une application web / jSON pour exposer sur le web la base [dbintrospringdata] étudiée dans le tutoriel [Introduction à Spring Data].
13.5. Exposer la base [dbintrospringdata] sur le web
13.5.1. Architecture du service web / jSON
Nous allons mettre en place l'architecture suivante :
Les couches [DAO] et [JPA] sont implémentées par l'application écrite dans le tutoriel [Introduction à Spring Data].
13.5.2. Installation de la base de données
Le script SQL [dbintrospringdata.sql] permet de créer la base MySQL nécessaire aux tests.
13.5.3. Le projet Eclipse du service web / jSON
Le projet Eclipse du service web / jSON est le suivant :
C'est un projet Maven dont le fichier [pom.xml] est le suivant :
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.webjson</groupId>
<artifactId>intro-server-webjson01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>intro-server-webjson01</name>
<description>démo spring mvc</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>istia.st.springdata</groupId>
<artifactId>intro-spring-data-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
|
- lignes 11-15 : le projet Maven parent déjà utilisé pour la couche [DAO] ;
- lignes 18-22 : la dépendance sur la couche [DAO] ;
- lignes 23-26 : la dépendance sur l'artifact [spring-boot-starter-web]. Cet artifact amène avec lui toutes les dépendances nécessaires à la création d'un service web / jSON. Il amène aussi des bibliothèques inutiles. Une configuration plus précise serait donc nécessaire. Mais cette configuration est pratique pour démarrer ;
- lignes 28-30 : la dépendance sur l'artifact [spring-boot-starter] permet de gérer les annotation Spring Boot ;
Les dépendances amenées par cette configuration sont les suivantes :
- en [1], on voit qu'Eclipse a vu la dépendance sur l'archive du projet [intro-spring-data-01] ;
Les dépendances ci-dessus sont à la fois celles de la couche [DAO] et de la couche [web].
13.5.3.1. Configuration de la couche [web]
La couche [web] est configurée par un fichier [AppConfig] :
La classe [WebConfig] configure la couche [web] :
| package spring.webjson.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
// -------------------------------- configuration couche [web]
@Autowired
private ApplicationContext context;
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// filtres jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// mappeur jSON
ObjectMapper mapper = new ObjectMapper();
// filtres
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// résultat
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// mappeur jSON
ObjectMapper mapper = new ObjectMapper();
// filtres
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// résultat
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// mappeur jSON
ObjectMapper mapper = new ObjectMapper();
// filtres
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// résultat
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// mappeur jSON
ObjectMapper mapper = new ObjectMapper();
// filtres
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// résultat
return mapper;
}
}
|
- ligne 18 : l'annotation [@EnableWebMvc] induit des configurations automatiques pour le framework Spring MVC ;
- ligne 19 : la classe [WebConfig] étend la classe Spring [WebMvcConfigurerAdapter] pour en redéfinir certains beans (lignes 26-40) ;
- lignes 22-23 : injection du contexte Spring ;
- lignes 25-29 : définition de la servlet du framework Spring MVC, celle qui route les requêtes HTTP vers le bon contrôleur et la bonne méthode. [DispatcherServlet] est une classe de Spring ;
- lignes 31-34 : on indique que cette servlet traite toutes les URL ;
- lignes 36-39 : c'est la présence de ce bean qui va activer le serveur Tomcat présent dans les archives du projet. Il attendra les requêtes sur le port 8080 ;
- lignes 42-91 : des beans qui seront utilisés pour gérer des filtres jSON ;
- lignes 42-45 : un mappeur jSON sans filtres ;
- lignes 47-57 : le mappeur jSON qui permet d'avoir une catégorie avec ses produits. On notera que lorsqu'on demande une catégorie avec ses produits, il faut à la fois configurer le filtre jSON de la classe [Categorie] et celui de la classe [Produit]. Il en est toujours ainsi. Lorsqu'on sérialise / désérialise une classe en jSON, il faut configurer le filtre jSON de la classe et ceux de toutes les dépendances à inclure dans celle-ci ;
- lignes 59-69 : le mappeur jSON qui permet d'avoir un produit avec sa catégorie ;
- lignes 71-80 : le mappeur jSON qui permet d'avoir une catégorie sans ses produits ;
- lignes 82-91 : le mappeur jSON qui permet d'avoir un produit sans sa catégorie ;
La classe [AppConfig] configure l'ensemble de l'application, ç-à-d les couches [web] et [DAO] :
| package spring.webjson.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import spring.data.config.DaoConfig;
@ComponentScan(basePackages = { "spring.webjson" })
@Import({ DaoConfig.class, WebConfig.class})
public class AppConfig {
}
|
- ligne 9 : on importe les beans de la couche [DAO] et ceux de la couche [web] ;
- ligne 8 : indique dans quels packages trouver d'autres beans Spring ;
On notera que nulle part, nous n'avons utilisé l'annotation [@EnableAutoConfiguration]. Nous avons préféré contrôler la configuration nous-mêmes.
13.5.4. Le modèle de l'application
La classe [ApplicationModel] est la suivante :
| package spring.webjson.models;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
@Component
public class ApplicationModel implements IDao {
// la couche [DAO]
@Autowired
private IDao dao;
@Override
public void addProduits(List<Produit> produits) {
dao.addProduits(produits);
}
@Override
public void deleteAllProduits() {
dao.deleteAllProduits();
}
@Override
public void updateProduits(List<Produit> produits) {
dao.updateProduits(produits);
}
@Override
public List<Produit> getAllProduits() {
return dao.getAllProduits();
}
@Override
public void addCategories(List<Categorie> categories) {
dao.addCategories(categories);
}
@Override
public void deleteAllCategories() {
dao.deleteAllCategories();
}
@Override
public void updateCategories(List<Categorie> categories) {
dao.updateCategories(categories);
}
@Override
public List<Categorie> getAllCategories() {
return dao.getAllCategories();
}
@Override
public Produit getProduitByIdWithCategorie(Long idProduit) {
return dao.getProduitByIdWithCategorie(idProduit);
}
@Override
public Produit getProduitByNameWithCategorie(String nom) {
return dao.getProduitByNameWithCategorie(nom);
}
@Override
public Categorie getCategorieByIdWithProduits(Long idCategorie) {
return dao.getCategorieByIdWithProduits(idCategorie);
}
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
return dao.getCategorieByNameWithProduits(nom);
}
@Override
public Produit getProduitByIdWithoutCategorie(Long idProduit) {
return dao.getProduitByIdWithoutCategorie(idProduit);
}
@Override
public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
return dao.getCategorieByIdWithoutProduits(idCategorie);
}
@Override
public Produit getProduitByNameWithoutCategorie(String nom) {
return dao.getProduitByNameWithoutCategorie(nom);
}
@Override
public Categorie getCategorieByNameWithoutProduits(String nom) {
return dao.getCategorieByNameWithoutProduits(nom);
}
}
|
- ligne 12 : la classe est un singleton Spring ;
- ligne 13 : qui implémente l'interface [IDao] de la couche [DAO] ;
- lignes 16-17 : injection d'une référence sur la couche [DAO] ;
- lignes 19-99 : implémentation de l'interface [IDao] ;
L'architecture de la couche web évolue comme suit :
- en [2b], les méthodes du ou des contrôleurs communiquent avec le singleton [ApplicationModel] ;
Cette stratégie amène de la souplesse quant à la gestion d'un éventuel cache. La classe [ApplicationModel] peut servir à mémoriser des informations obtenues auprès de la couche [DAO] ou encore des données de configuration. Cela peut être utile lorsqu'on n'a pas la maîtrise de la couche [DAO]. Cette stratégie de cache peut évoluer au fil du temps. Les modifications n'auront aucun impact sur le code du ou des contrôleurs.
13.5.5. Le contrôleur
Nous n'avons ici qu'un contrôleur, la classe [MyController].
13.5.5.1. Les URL exposées
Les URL exposées par ce contrôleur sont les suivantes :
| @RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public String addProduits(HttpServletRequest request) {
...
}
|
| Ajoute des produits dans la base. Ceux-ci sont postés. La réponse est la chaîne jSON la liste des produits ajoutés avec leur clé primaire. |
| @RequestMapping(value = "/deleteAllProduits", method = RequestMethod.GET)
public String deleteAllProduits() {
..
}
|
| Supprime tous les produits de la base. |
| @RequestMapping(value = "/updateProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public String updateProduits(HttpServletRequest request) {
..
}
|
| Met à jour des produits dans la base. Ceux-ci sont postés. La réponse est chaîne jSON de la liste des produits mis à jour. |
| @RequestMapping(value = "/getAllProduits", method = RequestMethod.GET)
public String getAllProduits() {
..
}
|
| Obtient la chaîne jSON de tous les produits. |
| @RequestMapping(value = "/addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public String addCategories(HttpServletRequest request) {
..
}
|
| Ajoute des catégories dans la base. Ceux-ci sont postés. La réponse est la chaîne jSON de la liste des catégories ajoutées avec leur clé primaire. Si les catégories contiennent des produits, ceux-ci sont également ajoutés à la base. |
| @RequestMapping(value = "/deleteAllCategories", method = RequestMethod.GET)
public String deleteAllCategories() {
...
}
|
| Supprime toutes les catégories de la base ainsi que tous les produits de celles-ci. Après la base est vide. |
| @RequestMapping(value = "/updateCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public String updateCategories(HttpServletRequest request) {
...
}
|
| Met à jour des catégories dans la base. Ceux-ci sont postés. La réponse est la liste des catégories mises à jour. Si les catégories contiennent des produits, ceux-ci sont également mis à jour dans la base. Rend la chaîne jSON des catégories modifiées ; |
| @RequestMapping(value = "/getAllCategories", method = RequestMethod.GET)
public String getAllCategories() {
...
}
|
| Obtient la chaîne jSON de toutes les catégories. |
| @RequestMapping(value = "/getProduitByIdWithCategorie/{idProduit}", method = RequestMethod.GET)
public String getProduitByIdWithCategorie(@PathVariable("idProduit") Long idProduit) {
...
}
|
| Obtient la chaîne jSON d'un produit désigné par son id, avec sa catégorie. |
| @RequestMapping(value = "/getProduitByIdWithoutCategorie/{idProduit}", method = RequestMethod.GET)
public String getProduitByIdWithoutCategorie(@PathVariable("idProduit") Long idProduit) {
...
}
|
| Obtient la chaîne jSON d'un produit désigné par son id, sans sa catégorie. |
| @RequestMapping(value = "/getProduitByNameWithCategorie/{nom}", method = RequestMethod.GET)
public String getProduitByNameWithCategorie(@PathVariable("nom") String nom) {
...
}
|
| Obtient la chaîne jSON d'un produit désigné par son nom, avec sa catégorie. |
| @RequestMapping(value = "/getProduitByNameWithoutCategorie/{nom}", method = RequestMethod.GET)
public String getProduitByNameWithoutCategorie(@PathVariable("nom") String nom) {
...
}
|
| Obtient la chaîne jSON d'un produit désigné par son nom, sans sa catégorie. |
| @RequestMapping(value = "/getCategorieByIdWithProduits/{idCategorie}", method = RequestMethod.GET)
public String getCategorieByIdWithProduits(@PathVariable("idCategorie") Long idCategorie) {
...
}
|
| Obtient la chaîne jSON d'une catégorie désignée par son id, avec ses produits. |
| @RequestMapping(value = "/getCategorieByNameWithProduits/{nom}", method = RequestMethod.GET)
public String getCategorieByNameWithProduits(@PathVariable("nom") String nom) {
...
}
|
| Obtient la chaîne jSON d'une catégorie désignée par son nom, avec ses produits. |
| @RequestMapping(value = "/getCategorieByNameWithoutProduits/{nom}", method = RequestMethod.GET)
public String getCategorieByNameWithoutProduits(@PathVariable("nom") String nom) {
...
}
|
| Obtient la chaîne jSON d'une catégorie désignée par son nom, sans ses produits. |
| @RequestMapping(value = "/getCategorieByIdWithoutProduits/{idCategorie}", method = RequestMethod.GET)
public String getCategorieByIdWithoutProduits(@PathVariable("idCategorie") Long idCategorie) {
...
}
|
| Obtient la chaîne jSON d'une catégorie désignée par son id sans ses produits. |
Les URL exposées correspondent aux méthodes de l'interface [IDao] de la couche [DAO]. Les méthodes du service web / jSON sont toutes bâties sur le même modèle. Nous allons en examiner quelques unes.
13.5.5.2. Le squelette du contrôleur
Le squelette du contrôleur est le suivant :
| package spring.webjson.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;
import spring.data.dao.DaoException;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.webjson.models.ApplicationModel;
import spring.webjson.models.Response;
@Controller
public class MyController {
// dépendances Spring
@Autowired
private ApplicationModel application;
// filtres jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
// la classe [MyController] est un singleton et n'est instanciée qu'une fois le bean
public MyController() {
// System.out.println("MyController");
}
@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProduits(HttpServletRequest request) throws JsonProcessingException {
...
}
|
- ligne 28 : l'annotation [@Controller] fait de la classe un composant Spring ;
- lignes 32-33 : injection d'une référence sur la classe [ApplicationModel] ;
- lignes 36-50 : injections de références sur les mappeurs jSON ;
- ligne 58 : l'URL exposée est [/addProduits]. Le client doit utiliser une méthode [POST] pour faire sa requête (method = RequestMethod.POST). Il doit envoyer la valeur postée sous forme d'une chaîne jSON (consumes = "application/json; charset=UTF-8"). La méthode renvoie elle-même la réponse au client (ligne 59). Ce sera une chaîne de caractères (ligne 60). L'entête HTTP [Content-type : application/json; charset=UTF-8] sera envoyé au client pour lui indiquer qu'il va recevoir une chaîne jSON (ligne 58) ;
- ligne 60 : la méthode [addProduits] rend la chaîne jSON de la liste des produits ajoutés dans la base ;
13.5.5.3. La réponse des méthodes du contrôleur
Toutes les méthodes du contrôleur rendent la réponse de type [Response] suivant :
| package spring.webjson.service;
import java.util.List;
public class Response<T> {
// ----------------- propriétés
// statut de l'opération
private int status;
// les éventuels messages d'erreur
private List<String> messages;
// le corps de la réponse
private T body;
// constructeurs
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters et setters
...
}
|
- ligne 5 : la réponse encapsule un type T ;
- ligne 13 : la réponse de type T ;
- lignes 9-11 : il est possible qu'une méthode rencontre une exception. Dans ce cas, elle rendra une réponse avec :
- ligne 9 : status!=0 ;
- ligne 11 : la liste des erreurs rencontrées ;
13.5.5.4. L'URL [/addProduits]
L'URL [/addProduits] est traitée par la méthode suivante :
| @RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProduits(HttpServletRequest request) throws JsonProcessingException {
// réponse
Response<List<Produit>> response;
try {
// on récupère la valeur postée
String body = CharStreams.toString(request.getReader());
List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
});
// on rétablit le lien entre produits et catégories
for (Produit produit : produits) {
produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
}
// on persiste les produits
application.addProduits(produits);
response = new Respon se<List<Produit>>(0, null, produits);
} catch (DaoException e1) {
response = new Response<List<Produit>>(1000, e1.getErreurs(), null);
} catch (Exception e2) {
response = new Response<List<Produit>>(1000, getErreursForException(e2), null);
}
// réponse jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
|
- ligne 3 : la méthode admet pour paramètre [HttpServletRequest request] qui encapsule toutes les informations sur la requête du client ;
- ligne 5 : la réponse qui sera envoyée au client : une liste de produits ;
- ligne 8 : on récupère la valeur postée. La classe [CharStreams] appartient à la bibliothèque [Google Guava] dont on a ajouté la référence dans le fichier [pom.xml]. On obtient la chaîne jSON postée par le client. Il faut la désérialiser pour en faire quelque chose ;
- lignes 8-10 : la désérialisation est faite. On obtient une liste de produits où chaque produit a un champ [categorie=null] ;
- lignes 12-14 : on réinitialise le champ [categorie] de tous les produits de la liste. Pour cela, on utilise le champ [idCategorie] du produit qui lui, est initialisé ;
- ligne 16 : les produits sont insérés dans la base ;
- ligne 17 : l'objet [response] est initialisé avec la liste de produits ;
- lignes 18-19 : cas où la méthode rencontre une exception de la couche [DAO]. On initialise la réponse avec [status=1000] (code d'erreur) [messages=e1.getMessages()], ç-à-d qu'on transmet au client la liste des erreurs rencontrées côté serveur ;
- lignes 20-21 : cas où la méthode rencontre un autre type d'exception. On initialise la réponse avec [status=1000] (code d'erreur) [messages=getErreursForException(e)] où [getErreursForException] est une méthode privée de la classe qui rend la liste des erreurs associées aux exceptions de la pile d'exceptions de e, et [body=null] ;
- ligne 24 : on rend la chaîne jSON de la réponse ;
13.5.5.5. L'URL [/getAllProduits]
L'URL [/getAllProduits] est traitée par la méthode suivante :
| @RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllProduits() throws JsonProcessingException {
// réponse
Response<List<Produit>> response;
try {
response = new Response<List<Produit>>(0, null, application.getAllProduits());
} catch (DaoException e1) {
response = new Response<List<Produit>>(1003, e1.getErreurs(), null);
} catch (Exception e2) {
response = new Response<List<Produit>>(1003, getErreursForException(e2), null);
}
// réponse jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
|
- ligne 1 : l'URL [/getAllProduits] est demandée avec une opération [GET]. Elle produit du jSON ;
- ligne 2 : la méthode envoie elle-même la réponse jSON au client ;
- ligne 5 : la méthode envoie la chaîne jSON d'un type [Response<List<Produit>>] ;
- ligne 7 : les produits sont demandés sans leur catégorie ;
- lignes 8-12 : en cas d'erreur, la réponse est initialisée avec un code et des messages d'erreur ;
- ligne 14 : la réponse jSON de la réponse est envoyée au client ;
13.5.5.6. Conclusion
Nous n'allons pas présenter les autres méthodes du contrôleur. Elles ressemblent à l'une ou l'autre des deux méthodes que nous enons de présenter.
13.5.6. La classe d'exécution du service web / jSON
La classe [Boot] est la classe exécutable du projet :
| package spring.webjson.boot;
import org.springframework.boot.SpringApplication;
import spring.webjson.server.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
|
- ligne 10 : la méthode statique [SpringApplication.run] est exécutée. La classe [SpringApplication] est une classe du projet [Spring Boot] (ligne 3). On lui passe deux paramètres :
- [AppConfig.class] : la classe qui configure la totalité de l'application ;
- [args] : les éventuels arguments passés à la méthode [main] ligne 9. Ce paramètre n'est pas utilisé ici ;
Lorsqu'on exécute cette classe, on a les logs suivants :
| . ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.2.2.RELEASE)
2015-03-24 16:22:46.608 INFO 9492 --- [ main] spring.webjson.server.boot.Boot : Starting Boot on Gportpers3 with PID 9492 (D:\data\istia-1415\eclipse\intro-web-json\intro-webjson-server-02\target\classes started by ST in D:\data\istia-1415\eclipse\intro-web-json\intro-webjson-server-02)
2015-03-24 16:22:46.654 INFO 9492 --- [ main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1d7acb34: startup date [Tue Mar 24 16:22:46 CET 2015]; root of context hierarchy
2015-03-24 16:22:47.521 INFO 9492 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'beanNameViewResolver': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/ErrorMvcAutoConfiguration$WhitelabelErrorViewConfiguration.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter; factoryMethodName=beanNameViewResolver; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter.class]]
2015-03-24 16:22:47.569 INFO 9492 --- [ main] o.s.b.f.s.DefaultListableBeanFactory : Overriding bean definition for bean 'entityManagerFactory': replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=spring.data.config.DaoConfig; factoryMethodName=entityManagerFactory; initMethodName=null; destroyMethodName=(inferred); defined in class spring.data.config.DaoConfig] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=true; factoryBeanName=org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; factoryMethodName=entityManagerFactory; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfiguration.class]]
2015-03-24 16:22:48.137 INFO 9492 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration' of type [class org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration$$EnhancerBySpringCGLIB$$405db6ba] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.162 INFO 9492 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionAttributeSource' of type [class org.springframework.transaction.annotation.AnnotationTransactionAttributeSource] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.172 INFO 9492 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'transactionInterceptor' of type [class org.springframework.transaction.interceptor.TransactionInterceptor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.178 INFO 9492 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.transaction.config.internalTransactionAdvisor' of type [class org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2015-03-24 16:22:48.586 INFO 9492 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2015-03-24 16:22:48.850 INFO 9492 --- [ main] o.apache.catalina.core.StandardService : Starting service Tomcat
2015-03-24 16:22:48.852 INFO 9492 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.0.20
2015-03-24 16:22:48.992 INFO 9492 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2015-03-24 16:22:48.992 INFO 9492 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 2342 ms
2015-03-24 16:22:49.645 INFO 9492 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean : Mapping servlet: 'dispatcherServlet' to [/]
2015-03-24 16:22:49.650 INFO 9492 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2015-03-24 16:22:49.651 INFO 9492 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2015-03-24 16:22:50.380 INFO 9492 --- [ main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2015-03-24 16:22:50.392 INFO 9492 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [
name: default
...]
2015-03-24 16:22:50.478 INFO 9492 --- [ main] org.hibernate.Version : HHH000412: Hibernate Core {4.3.8.Final}
2015-03-24 16:22:50.480 INFO 9492 --- [ main] org.hibernate.cfg.Environment : HHH000206: hibernate.properties not found
2015-03-24 16:22:50.483 INFO 9492 --- [ main] org.hibernate.cfg.Environment : HHH000021: Bytecode provider name : javassist
2015-03-24 16:22:50.697 INFO 9492 --- [ main] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2015-03-24 16:22:50.806 INFO 9492 --- [ main] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MySQLDialect
2015-03-24 16:22:51.058 INFO 9492 --- [ main] o.h.h.i.ast.ASTQueryTranslatorFactory : HHH000397: Using ASTQueryTranslatorFactory
2015-03-24 16:22:52.581 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1d7acb34: startup date [Tue Mar 24 16:22:46 CET 2015]; root of context hierarchy
2015-03-24 16:22:52.654 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/addProduits],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.addProduits(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/updateProduits],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.updateProduits(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllProduits],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Produit>> spring.webjson.server.service.Controller.getAllProduits()
2015-03-24 16:22:52.655 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getAllCategories],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.getAllCategories()
2015-03-24 16:22:52.655 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/addCategories],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.addCategories(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.655 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/updateCategories],methods=[POST],params=[],headers=[],consumes=[application/json;charset=UTF-8],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.util.List<spring.data.entities.Categorie>> spring.webjson.server.service.Controller.updateCategories(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.656 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByNameWithoutProduits/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByNameWithoutProduits(java.lang.String)
2015-03-24 16:22:52.656 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByNameWithoutCategorie/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByNameWithoutCategorie(java.lang.String)
2015-03-24 16:22:52.656 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByNameWithCategorie/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByNameWithCategorie(java.lang.String)
2015-03-24 16:22:52.656 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByIdWithCategorie/{idProduit}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByIdWithCategorie(java.lang.Long)
2015-03-24 16:22:52.656 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByNameWithProduits/{nom}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByNameWithProduits(java.lang.String)
2015-03-24 16:22:52.657 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByIdWithProduits/{idCategorie}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByIdWithProduits(java.lang.Long)
2015-03-24 16:22:52.657 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/deleteAllCategories],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.lang.Void> spring.webjson.server.service.Controller.deleteAllCategories()
2015-03-24 16:22:52.657 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getCategorieByIdWithoutProduits/{idCategorie}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Categorie> spring.webjson.server.service.Controller.getCategorieByIdWithoutProduits(java.lang.Long)
2015-03-24 16:22:52.657 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/deleteAllProduits],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<java.lang.Void> spring.webjson.server.service.Controller.deleteAllProduits()
2015-03-24 16:22:52.658 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/getProduitByIdWithoutCategorie/{idProduit}],methods=[GET],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public spring.webjson.webjson.models.Response<spring.data.entities.Produit> spring.webjson.server.service.Controller.getProduitByIdWithoutCategorie(java.lang.Long)
2015-03-24 16:22:52.659 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.659 INFO 9492 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],methods=[],params=[],headers=[],consumes=[],produces=[text/html],custom=[]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest)
2015-03-24 16:22:52.691 INFO 9492 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:52.692 INFO 9492 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:52.742 INFO 9492 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2015-03-24 16:22:53.001 INFO 9492 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2015-03-24 16:22:53.106 INFO 9492 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2015-03-24 16:22:53.108 INFO 9492 --- [ main] spring.webjson.server.boot.Boot : Started Boot in 6.752 seconds (JVM running for 7.433)
|
- lignes 17-19 : démarrage du serveur Tomcat qui va exécuter le service web / jSON ;
- lignes 25-33 : construction de la couche [DAO] ;
- lignes 32-51 : les URL exposées sont découvertes ;
13.5.7. Tests du service web / jSON
Pour faire les tests, nous générons la base de données MySQL [dbintrospringdata] à partir du script SQL [dbintrospringdata.sql] :
Ceci fait, nous utilisons le client [Advanced Rest Client] (cf page 419) pour interroger les URL exposées par le service web / jSON (le service web / jSON doit être lancé).
- en [1-3], nous demandons l'URL [/getAllCategories] via une commande HTTP GET ;
Nous obtenons la réponse suivante :
- en [1], la requête HTTP du client ;
- en [2], la réponse HTTP du serveur ;
- en [3], le statut [200 OK] indique que le serveur a correctement traité la demande ;
- en [4], la réponse jSON du serveur ;
La réponse jSON complète est la suivante :
| {"status":0,"messages":null,"body":[{"id":415,"version":0,"nom":"categorie0","produits":[{"id":1849,"version":0,"nom":"produit00","idCategorie":415,"prix":100.0,"description":"desc00"},{"id":1850,"version":0,"nom":"produit01","idCategorie":415,"prix":101.0,"description":"desc01"},{"id":1851,"version":0,"nom":"produit02","idCategorie":415,"prix":102.0,"description":"desc02"},{"id":1852,"version":0,"nom":"produit03","idCategorie":415,"prix":103.0,"description":"desc03"},{"id":1853,"version":0,"nom":"produit04","idCategorie":415,"prix":104.0,"description":"desc04"}]},{"id":416,"version":0,"nom":"categorie1","produits":[{"id":1856,"version":0,"nom":"produit12","idCategorie":416,"prix":112.0,"description":"desc12"},{"id":1857,"version":0,"nom":"produit13","idCategorie":416,"prix":113.0,"description":"desc13"},{"id":1858,"version":0,"nom":"produit14","idCategorie":416,"prix":114.0,"description":"desc14"},{"id":1854,"version":0,"nom":"produit10","idCategorie":416,"prix":110.0,"description":"desc10"},{"id":1855,"version":0,"nom":"produit11","idCategorie":416,"prix":111.0,"description":"desc11"}]}]}
|
- status:0 signifie qu'il n'y a pas eu d'erreurs côté serveur ;
- messages : null signifie qu'il n'y a pas de messages d'erreur ;
- body : est le corps de la réponse, ici la liste des catégories avec leurs produits. Il y a deux catégories avec chacune 5 produits ;
Nous allons ajouter à la catégorie [categorie1], le produit [produit15]. Pour cela nous allons utiliser l'URL [/addCategories] qui a le code suivant :
| @RequestMapping(value = "/addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addCategories(HttpServletRequest request) throws JsonProcessingException {
Response<List<Categorie>> response;
ObjectMapper mapper = context.getBean(ObjectMapper.class);
// on persiste les catégories
try {
// on récupère la valeur postée
String body = CharStreams.toString(request.getReader());
mapper.setFilters(jsonFilterCategorieWithProduits);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// on rétablit le lien entre produits et catégories
for (Categorie categorie : categories) {
Set<Produit> produits = categorie.getProduits();
if (produits != null) {
for (Produit produit : categorie.getProduits()) {
produit.setCategorie(categorie);
}
}
}
// on persiste les catégories
application.addCategories(categories);
response = new Response<List<Categorie>>(0, null, categories);
} catch (Exception e) {
response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
}
// réponse jSON
return mapper.writeValueAsString(response);
}
|
- ligne 1 : le client doit faire un POST et la valeur postée doit être une chaîne jSON ;
- lignes 9-12 : la valeur postée doit être une liste de catégories avec leurs produits associés ;
Nous allons créer une catégorie [categorie2] avec un produit [produit21]. La chaîne jSON à envoyer est alors la suivante :
| [{"id":null,"version":0,"nom":"categorie2","produits":[{"id":null,"version":0,"nom":"produit21","idCategorie":null,"prix":111.0,"description":"desc21"}]}]
|
La requête au service web / jSON est faite de la façon suivante :
- en [1], l'URL demandée ;
- en [2], elle est demandée via une opération POST ;
- en [3], la chaîne jSON postée ;
- en [4], on indique au serveur qu'on va lui envoyer du jSON ;
La réponse du serveur est la suivante :
- en [1], on voit que et la catégorie et son produit ont maintenant une clé primaire montrant par là qu'ils ont probablement été insérés dans la base. Nous allons le vérifier en utilisant l'URL [/getCategorieByNameWithProduits/categorie2] :
Nous obtenons le résultat suivant :
Nous avons bien obtenu la catégorie [categorie2] avec son unique produit [produit21]. On peut aussi demander uniquement le produit. Utilisons pour cela l'URL [/getProduitByIdWithoutCategorie/1859] :
Nous obtenons le résultat suivant :
Toutes les opérations [GET] peuvent être faites dans un simple navigateur :
Le lecteur est invité à tester les autres URL du service web / json.
13.6. Un client programmé pour le service web / jSON
Maintenant que la base [dbintrospringdata] est disponible sur le web, nous allons écrire une application qui l'exploite. On aura alors l'architecture client / serveur suivante :
L'application cliente aura deux couches :
- une couche [DAO] [2] pour communiquer avec l'application web / jSON qui expose la base de données ;
- une couche de tests JUnit [1] pour vérifier que le client et le serveur font bien leur travail ;
13.6.1. Le projet Eclipse
Le projet Eclipse du client est le suivant :
- le dossier [src/main/java] implémente la couche [DAO] ;
- le dossier [src/test/java] implémente les tests JUnit ;
13.6.2. Configuration Maven du projet
Le projet est un projet Maven configuré par le fichier [pom.xml] suivant :
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.webjson</groupId>
<artifactId>intro-client-webjson-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Client console du serveur web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- librairie jSON utilisée par Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- composant utilisé par Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
<scope>test</scope>
</dependency>
<!-- bibliothèque de logs -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- plugins -->
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
<name>intro-client-webjson-01</name>
</project>
|
- lignes 14-18 : le projet Maven parent [spring-boot-starter-parent] qui nous permet de définir un certain nombre de dépendances sans leur versions, celle-ci étant définie dans le projet parent ;
- lignes 22-25 : bien que nous n'écrivions pas une application web, nous avons besoin de la dépendance [spring-web] qui amène avec elle la classe [RestTemplate] qui permet de s'interfacer aisément avec une application web / jSON ;
- lignes 27-34 : une bibliothèque jSON ;
- lignes 36-39 : une dépendance qui va nous permettre de fixer un timeout aux requêtes HTTP du client. Un timeout est un temps maximal d'attente de la réponse du serveur. Au-delà de ce temps, le client signale une erreur de timeout en jetant une exception ;
- lignes 41-46 : la bibliothèque Google Guava utilisée dans le test JUnit. Pour cette raison, nous avons mis sa portée à [test] (ligne 45). Cela signifie que cette dépendance n'est incluse que lors de l'exécution de codes de la branche [src/test/java] ;
- lignes 48-51 : la bibliothèque de logs ;
- lignes 52-63 : la dépendance pour les tests JUnit. Elle amène notamment la bibliothèque JUnit 4 nécessaire pour les tests. Ces dépendances ont l'attribut [<scope>test</scope>] indiquant qu'elles ne sont nécessaires que pour la phase de tests. Elles ne sont pas incluses dans l'archive finale du projet ;
13.6.3. Implémentation de la couche [DAO]
- le package [spring.client.config] contient la configation Spring de la couche [DAO] ;
- le package [spring.client.dao] contient l'implémentation de la couche [DAO] ;
- le package [spring.client.entities] contient les objets échangés avec le service web / jSON ;
13.6.3.1. Configuration
La classe [DaoConfig] fait la configuration Spring de la couche [DAO]. Son code est le suivant :
| package spring.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@ComponentScan({ "spring.client.dao" })
public class DaoConfig {
// 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;
}
// filtres jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// mappeur jSON
ObjectMapper mapper = new ObjectMapper();
// filtres
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// résultat
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// mappeur jSON
ObjectMapper mapper = new ObjectMapper();
// filtres
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// résultat
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// mappeur jSON
ObjectMapper mapper = new ObjectMapper();
// filtres
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// résultat
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// mappeur jSON
ObjectMapper mapper = new ObjectMapper();
// filtres
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// résultat
return mapper;
}
}
|
- ligne 13 : la classe est une classe de configuration Spring - des composants Spring sont à chercher dans le package [spring.client.dao] ;
- ligne 17 : on se fixe un timeout d'une seconde (1000 ms) ;
- lignes 32-35 : le bean qui rend cette valeur ;
- ligne 18 : l'URL du service web / jSON ;
- lignes 37-40 : le bean qui rend cette valeur ;
- lignes 20-30 : la configuration de la classe [RestTemplate] qui assure les échanges avec le service web / jSON. Lorsqu'on n'a pas à la configurer, on peut en disposer dans le code par un simple [new RestTemplate()]. Ici, nous voulons fixer le timeout des échanges avec le service web / jSON. Le bean [timeout] de la ligne 36 est passé en paramètre de la méthode [restTemplate] de la ligne 24 ;
- ligne 23 : le composant [HttpComponentsClientHttpRequestFactory] est le composant qui nous permet de fixer le timeout des échanges (lignes 29-30) ;
- ligne 24 : la classe [RestTemplate] est construite avec ce composant. Comme elle s'appuie sur celui-ci pour communiquer avec le service web / jSON, les échanges seront bien soumis au timeout ;
- le client et le serveur vont s'échanger des lignes de texte. Un convertisseur s'occupe de sérialiser un objet en texte et inversement de désérialiser un texte en objet. Il peut y avoir plusieurs convertisseurs associés à la classe [RestTemplate] et celui choisi à un moment donné dépend des entêtes HTTP envoyés par le serveur. Ici, nous n'aurons aucun convertisseur. Aussi, le composant [RestTemplate] ne cherchera pas à convertir d'une façon ou d'une autre les deux éléments suivants :
- le texte posté ;
- le texte reçu en réponse ;
Ces textes seront des chaînes jSON qui seront donc laissées en l'état par le composant [RestTemplate]. C'est nous développeur, qui ferons les séralisations / désérialisations jSON nécessaires. Ceci parce que les filtres à appliquer à la valeur postée et à la réponse reçue peuvent être différents et l'expérience montre qu'il est plus facile de les gérer soi-même que d'essayer de configurer le composant [RestTemplate] afin qu'il utilise le bon convertisseur jSON ;
- lignes 42-92 : définissent des filtres jSON. Ce sont les mêmes que ceux du serveur présentés et expliqués au paragraphe 13.5.3.1, page 228 ;
- lignes 43-46 : un mappeur jSON sans filtres ;
- lignes 64-68 : un mappeur jSON pour avoir une catégorie sans ses produits ;
- lignes 48-58 : un mappeur jSON pour avoir une catégorie avec ses produits ;
- lignes 83-92 : un mappeur jSON pour avoir un produit sans sa catégorie ;
- lignes 60-70 : un mappeur jSON pour avoir un produit avec sa catégorie ;
Tous ces beans vont être disponibles aux codes de la couche [DAO] ainsi qu'au test Junit.
13.6.3.2. Les entités
Les entités manipulées par la couche [DAO] sont celles qu'elle échange avec le service web / jSON. Ce sont les articles et les produits. 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.client.entities;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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();
}
// signature jSON
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// getters et setters
...
}
|
[Categorie]
| package spring.client.entities;
import java.util.HashSet;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {
// propriétés
private String nom;
// les produits associés
public Set<Produit> produits = new HashSet<Produit>();
// constructeurs
public Categorie() {
}
public Categorie(String nom) {
this.nom = nom;
}
// méthodes
public void addProduit(Produit produit) {
// on ajoute le produit
produits.add(produit);
// on fixe sa catégorie
produit.setCategorie(this);
}
// getters et setters
...
}
|
[Produit]
| package spring.webjson.client.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {
// le nom
private String nom;
// le n° de la catégorie
private Long idCategorie;
// le prix
private double prix;
// la description
private String description;
// la catégorie
private Categorie categorie;
// constructeurs
public Produit() {
}
public Produit(String nom, double prix, String description) {
this.nom = nom;
this.prix = prix;
this.description = description;
}
// getters et setters
...
}
|
13.6.3.3. La classe [DaoException]
Lorsque la couche [DAO] rencontrera une erreur, elle lancera une exception de type [DaoException]. Cette classe est celle utilisée côté serveur et décrite au paragraphe 11.3.7, page 191.
13.6.3.4. L'interface de la couche [DAO]
La couche [DAO] présente l'interface [IDao] décrite au paragraphe 11.3.7, page 191.
| package spring.client.dao;
import java.util.List;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
public interface IDao {
// insertion d'une liste de produits
public List<Produit> addProduits(List<Produit> produits);
// suppression de tous les produits
public void deleteAllProduits();
// mise à jour d'une liste de produits
public List<Produit> updateProduits(List<Produit> produits);
// obtention de tous les produits
public List<Produit> getAllProduits();
// insertion d'une liste de categories
public List<Categorie> addCategories(List<Categorie> categories);
// suppression de tous les categories
public void deleteAllCategories();
// mise à jour d'une liste de categories
public List<Categorie> updateCategories(List<Categorie> categories);
// obtention de tous les categories
public List<Categorie> getAllCategories();
// un produit particulier
public Produit getProduitByIdWithCategorie(Long idProduit);
public Produit getProduitByIdWithoutCategorie(Long idProduit);
public Produit getProduitByNameWithCategorie(String nom);
public Produit getProduitByNameWithoutCategorie(String nom);
// une catégorie particulière
public Categorie getCategorieByIdWithProduits(Long idCategorie);
public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
public Categorie getCategorieByNameWithProduits(String nom);
public Categorie getCategorieByNameWithoutProduits(String nom);
}
|
13.6.3.5. La réponse du service web / jSON
Nous avons vu que toutes les URL du service web / jSON rendaient un type [Response] défini au paragraphe 13.5.5.3, page 235. Nous reprenons ici cette classe :
| package spring.client.dao;
import java.util.List;
public class Response<T> {
// ----------------- propriétés
// statut de l'opération
private int status;
// les éventuels messages d'erreur
private List<String> messages;
// le corps de la réponse
private T body;
// constructeurs
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters et setters
...
}
|
13.6.3.6. Implémentation des échanges avec le service web / jSON
La classe [AbstractDao] implémente les échanges avec le service web / jSON :
| package spring.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.web.client.RestTemplate;
public abstract class AbstractDao {
// data
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// requête générique
protected String getResponse(String url, String jsonPost) {
// url : URL à contacter
// jsonPost : la valeur jSON à poster
try {
// 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 DaoException(20, e1);
} catch (RuntimeException e2) {
throw new DaoException(21, e2);
}
}
}
|
- lignes 15-16 : injection du composant [RestTemplate] qui assure la communication avec le serveur ;
- lignes 17-18 : injection de l'URL du service web / jSON ;
L'implémentation des méthodes de communication avec le serveur est factorisée dans la méthode [getResponse] :
- ligne 21 : la méthode reçoit 2 paramètres :
- [url] : l'URL demandée ;
- [jsonPost] : la chaîne jSON à poster, null sinon. Si [jsonPost==null], la requête de l'URL est faite avec un GET, sinon avec un POST ;
- ligne 38 : l'instruction qui fait la requête au serveur et reçoit sa réponse. Le composant [RestTemplate] offre un nombre important de méthodes d'échange avec le serveur. Nous avons choisi ici la méthode [exchange], mais il en existe d'autres ;
- lignes 27-36 : il nous faut construire la requête de type [RequestEntity]. Elle est différente selon que l'on utilise un GET ou un POST pour faire la requête ;
- lignes 30-31 : la requête pour un GET. La classe [RequestEntity] offre des méthodes statiques pour créer les requêtes GET, POST, HEAD,... La méthode [RequestEntity.get] permet de créer une requête GET en chaînant les différentes méthodes qui construisent celle-ci :
- la méthode [RequestEntity.get] admet pour paramètre l'URL cible sous la forme d'une instance URI,
- la méthode [accept] permet de définir les éléments de l'entête HTTP [Accept]. Ici, nous indiquons que nous acceptons le type [application/json] que va envoyer le serveur ;
- la méthode [build] utilise ces différentes informations pour construire le type [RequestEntity] de la requête ;
- lignes 34-35 : la requête pour un POST. La méthode [RequestEntity.post] permet de créer une requête POST en chaînant les différentes méthodes qui construisent celle-ci :
- la méthode [RequestEntity.post] admet pour paramètre l'URL cible sous la forme d'une instance URI,
- la méthode [header] définit un entête HTTP. Ici on envoie au serveur l'entête [Content-Type: application/json] pour lui indiquer que la valeur postée va lui arriver sous la forme d'une chaîne jSON ;
- la méthode [accept] permet d'indiquer que nous acceptons le type [application/json] que va envoyer le serveur ;
- la méthode [body] fixe la valeur postée. Celle-ci est le 4ième paramètre de la méthode générique [getResponse] (ligne 1) ;
- ligne 38 : la méthode [RestTemplate].exchange rend un type [ResponseEntity<String>] qui encapsule la totalité de la réponse du serveur : entêtes HTTP et corps du document. La méthode [ResponseEntity].getBody() permet d'avoir ce corps qui représente la réponse du serveur, ici une chaîne de caractères ;
13.6.3.7. Implémentation de l'interface [IDao]
La classe [Dao] implémente l'interface [IDao] :
| package spring.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
@Component
public class Dao extends AbstractDao implements IDao {
@Autowired
private ApplicationContext context;
// filtres jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Override
public List<Produit> addProduits(List<Produit> produits) {
// ----------- ajouter des produits (sans leur catégorie)
...
}
|
- ligne 17 : la classe [Dao] est un composant Spring dans lequel on peut donc injecter d'autres composants Spring ;
- ligne 18 : la classe [Dao] étend la classe [AbstractDao] que nous venons de voir et implémente l'interface [IDao] ;
- lignes 20-21 : on injecte le contexte Spring afin d'avoir accès à ses beans ;
- lignes 24-38 : injection des mappeurs jSON définis dans la classe [AppConfig] présentée au paragraphe 13.6.2, page 246 ;
Les implémentations des différentes méthodes de l'interface [IDao] suivent toutes le même schéma. Nous allons présenter deux méthodes, l'une s'appuyant sur une opération [POST], l'autre sur une opération [GET].
Un exemple de [GET] : [getCategorieByNameWithProduits]
| @Override
public Categorie getCategorieByNameWithProduits(String nom) {
// ----------- obtenir une catégorie désignée par son nom, avec ses produits
try {
// requête
Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
new TypeReference<Response<Categorie>>() {
});
// erreur ?
if (response.getStatus() != 0) {
// on lance 1 exception
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// on rend le coeur de la réponse du serveur
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(113, e2);
}
}
|
- ligne 7 : on appelle la méthode [getResponse] de la classe parent. C'est cette méthode qui assure les échanges avec le service web / jSON. Ses paramètres sont les suivants :
| getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null)
|
| * l'URL du service interrogée [/getCategorieByNameWithProduits/nom] ;
* la valeur postée. Ici il n'y en a pas ;
|
La méthode [getResponse] rend un type String qui est la réponse jSON envoyée par le serveur. On désérialise cette réponse jSON de la façon suivante :
| jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<Categorie>>() {
});
|
parce que la chaîne jSON est la sérialisation d'un type [Response<Categorie>] ;
- lignes 11-17 : on teste le statut de la réponse. Si le statut est différent de 0, alors c'est qu'il y a eu une erreur côté serveur. On lance alors une exception (ligne 13), en reprenant les informations contenues dans la réponse (statut et liste de messages d'erreur) ;
- ligne 16 : s'il n'y a pas eu d'erreur côté serveur, on rend le corps du type [Response<Categorie>], ç-à-d la catégorie demandée ;
- lignes 18-19 : on gère l'exception lancée ligne 16 ;
- lignes 20-22 : traitent toutes les autres exceptions ;
Un exemple de [POST] : [addCategories]
| @Override
public List<Categorie> addCategories(List<Categorie> categories) {
// ----------- ajouter des catégories (avec leurs produits)
try {
// requête
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
new TypeReference<Response<List<Categorie>>>() {
});
// erreur ?
if (response.getStatus() != 0) {
// on lance 1 exception
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// on rend le coeur de la réponse du serveur
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(104, e2);
}
}
|
- ligne 2 : la méthode [addCategories] sert à persister en base de données les catégories passées en paramètre. Elle rend ces mêmes catégories enrichies de leurs clés primaires. Si les catégories sont passées avec des produits, ceux-ci sont également persistés ;
- ligne 7 : on appelle la méthode [getResponse] du parent pour faire les échanges avec le service web / jSON ;
- le 1er paramètre est l'URL [/addCategories] ;
- le second paramètre est la valeur postée, ici la liste des catégories à persister ;
| getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories))
|
La chaîne jSON obtenue est ensuite désérialisée pour obtenir le type [Response<List<Categorie>] attendu :
| Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<List<Categorie>>>() {
});
|
- lignes 11-17 : gestion de la réponse du serveur (erreur ou pas) ;
- lignes 20-22 : gestion des exceptions ;
Toutes les autres méthodes suivent le canevas des deux méthodes présentées.
13.6.4. Le test JUnit
Revenons à l'architecture client / serveur en cours de construction :
Nous avons construit une couche [DAO] [2] avec la même interface que la couche [DAO] [4]. Pour tester la couche [DAO] [2], on peut donc utiliser le test JUnit qui a servi à tester la couche [DAO] [4]. Pour rappel, celui-ci est le suivant :
 | | |
| package spring.client.junit;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import spring.client.config.DaoConfig;
import spring.client.dao.DaoException;
import spring.client.dao.IDao;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// couche [DAO]
@Autowired
private IDao dao;
// filtres jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Before
public void cleanAndFill() {
// on nettoie la base avant chaque test
log("Vidage de la base de données", 1);
// on vide la table [CATEGORIES] - par cascade la table [PRODUITS] va être vidée
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// on remplit les tables
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < 2; i++) {
Categorie categorie = new Categorie(String.format("categorie%d", i));
for (int j = 0; j < 5; j++) {
categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
String.format("desc%d%d", i, j)));
}
categories.add(categorie);
}
// ajout de la catégorie - par cascade les produits vont eux aussi être insérés
categories = dao.addCategories(categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// liste des catégories
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories();
affiche(categories, jsonMapperCategorieWithoutProduits);
// liste des produits
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits();
affiche(produits, jsonMapperProduitWithoutCategorie);
// quelques vérifications
Assert.assertEquals(2, categories.size());
Assert.assertEquals(10, produits.size());
Categorie categorie = findCategorieByName("categorie0", categories);
Assert.assertNotNull(categorie);
Produit produit = findProduitByName("produit03", produits);
Assert.assertNotNull(produit);
Long idCategorie = produit.getIdCategorie();
Assert.assertEquals(categorie.getId(), idCategorie);
}
@Test
public void getCategorieByNameWithProduits() {
log("getCategorieByNameWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals(5, categorie1.getProduits().size());
}
@Test
public void getCategorieByNameWithoutProduits() {
log("getCategorieByNameWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals("categorie1", categorie1.getNom());
}
@Test
public void getCategorieByIdWithProduits() {
log("getCategorieByIdWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie2 = dao.getCategorieByIdWithProduits(categorie1.getId());
Assert.assertNotNull(categorie2);
Assert.assertEquals(categorie1.getId(), categorie2.getId());
Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
}
@Test
public void getCategorieByIdWithoutProduits() {
log("getCategorieByIdWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie2 = dao.getCategorieByIdWithoutProduits(categorie1.getId());
Assert.assertNotNull(categorie2);
Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
}
@Test
public void getProduitByNameWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Assert.assertNotNull(produit);
Assert.assertNotNull(produit.getCategorie());
}
@Test
public void getProduitByNameWithoutCategorie() {
log("getProduitByNameWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithoutCategorie("produit03");
Assert.assertNotNull(produit);
Assert.assertEquals("produit03", produit.getNom());
}
@Test
public void getProduitByIdWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
}
@Test
public void getProduitByIdWithoutCategorie() {
log("getProduitByIdWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
}
@Test
public void doInsertsInTransaction() {
log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
// on fait l'insertion
Categorie categorie = new Categorie("cat1");
categorie.addProduit(new Produit("x", 1.0, ""));
categorie.addProduit(new Produit("x", 1.0, ""));
// ajout de la catégorie - par cascade les produits vont eux aussi être insérés
try {
categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
} catch (DaoException e) {
show("Les erreurs suivantes se sont produites :", e.getErreurs());
}
// vérifications
List<Categorie> categories = dao.getAllCategories();
Assert.assertEquals(2, categories.size());
List<Produit> produits = dao.getAllProduits();
Assert.assertEquals(10, produits.size());
}
@Test
public void updateDataBase() {
log("Mise à jour du prix des produits de [categorie1]", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie1Saved = dao.getCategorieByNameWithProduits("categorie1");
Set<Produit> produits = categorie1.getProduits();
for (Produit produit : produits) {
produit.setPrix(1.1 * produit.getPrix());
}
List<Produit> produits2 = Lists.newArrayList(produits);
produits2 = dao.updateProduits(produits2);
// vérifications
List<Produit> produitsSaved = Lists.newArrayList(categorie1Saved.getProduits());
for (Produit produit2 : produits2) {
Produit produit = findProduitByName(produit2.getNom(), produitsSaved);
Assert.assertEquals(produit2.getPrix(), produit.getPrix() * 1.1, 1e-6);
}
}
@Test
public void addProduits() throws BeansException, JsonProcessingException {
log("Ajout de deux produits de catégorie [categorie0]", 1);
Categorie categorie0 = dao.getCategorieByNameWithoutProduits("categorie0");
Long idCategorie = categorie0.getId();
Produit p1 = new Produit("x", 1, "");
p1.setIdCategorie(idCategorie);
p1.setCategorie(categorie0);
Produit p2 = new Produit("y", 1, "");
p2.setIdCategorie(idCategorie);
p2.setCategorie(categorie0);
List<Produit> produits = new ArrayList<Produit>();
produits.add(p1);
produits.add(p2);
produits = dao.addProduits(produits);
// vérification
affiche(produits, jsonMapperProduitWithoutCategorie);
}
// -------------- méthodes privées
private Produit findProduitByName(String nom, List<Produit> produits) {
for (Produit produit : produits) {
if (produit.getNom().equals(nom)) {
return produit;
}
}
return null;
}
private Categorie findCategorieByName(String nom, List<Categorie> categories) {
for (Categorie categorie : categories) {
if (categorie.getNom().equals(nom)) {
return categorie;
}
}
return null;
}
// affichage d'un élément de type T
static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// affichage d'une liste d'éléments de type T
static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, jsonMapper);
}
}
private static void log(String message, int mode) {
// affiche message
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
private static void show(String title, List<String> messages) {
// titre
System.out.println(String.format("%s : ", title));
// messages
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
|
Son exécution réussit et donne les résultats suivants sur la console :
| Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout de deux produits de catégorie [categorie0] --------------------------------
{"id":6285,"version":0,"nom":"x","idCategorie":1319,"prix":1.0,"description":""}
{"id":6286,"version":0,"nom":"y","idCategorie":1319,"prix":1.0,"description":""}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Mise à jour du prix des produits de [categorie1] --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByIdWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
-- Liste des catégories
{"id":1337,"version":0,"nom":"categorie0"}
{"id":1338,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":6367,"version":0,"nom":"produit00","idCategorie":1337,"prix":100.0,"description":"desc00"}
{"id":6368,"version":0,"nom":"produit01","idCategorie":1337,"prix":101.0,"description":"desc01"}
{"id":6369,"version":0,"nom":"produit02","idCategorie":1337,"prix":102.0,"description":"desc02"}
{"id":6370,"version":0,"nom":"produit03","idCategorie":1337,"prix":103.0,"description":"desc03"}
{"id":6371,"version":0,"nom":"produit04","idCategorie":1337,"prix":104.0,"description":"desc04"}
{"id":6372,"version":0,"nom":"produit10","idCategorie":1338,"prix":110.0,"description":"desc10"}
{"id":6373,"version":0,"nom":"produit11","idCategorie":1338,"prix":111.0,"description":"desc11"}
{"id":6374,"version":0,"nom":"produit12","idCategorie":1338,"prix":112.0,"description":"desc12"}
{"id":6375,"version":0,"nom":"produit13","idCategorie":1338,"prix":113.0,"description":"desc13"}
{"id":6376,"version":0,"nom":"produit14","idCategorie":1338,"prix":114.0,"description":"desc14"}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites :
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
11:24:37.650 [Thread-1] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@f8c1ddd: startup date [Fri Nov 20 11:24:34 CET 2015]; root of context hierarchy
|