Skip to content

13. [Kurs]: Bereitstellung einer Datenbank im Web mit Spring MVC

Stichworte: mehrschichtige Architektur, Spring, Dependency Injection, Webservice / JSON, Client / Server

13.1. Support

 

Die Projekte zu diesem Kapitel finden Sie im Ordner [support / chap-13]. Das SQL-Skript [dbintrospringdata.sql] erstellt die für die Tests erforderliche MySQL-Datenbank.

13.2. Die Rolle von Spring MVC in einer Webanwendung

Betrachten wir Spring MVC im Kontext der Entwicklung einer Webanwendung. Meistens basiert diese auf einer mehrschichtigen Architektur wie der folgenden:

  • Die [Web]-Schicht ist die Schicht, die mit dem Benutzer der Webanwendung in Kontakt steht. Der Benutzer interagiert mit der Webanwendung über Webseiten, die in einem Browser angezeigt werden. Spring MVC befindet sich in dieser Schicht und nur in dieser Schicht;
  • Die [Business]-Schicht implementiert die Geschäftslogik der Anwendung, wie beispielsweise die Berechnung eines Gehalts oder einer Rechnung. Diese Schicht nutzt Daten vom Benutzer über die [Web]-Schicht und aus dem DBMS über die [DAO]-Schicht;
  • Die [DAO]-Schicht (Data Access Objects), die [ORM]-Schicht (Object Relational Mapper) und der JDBC-Treiber verwalten den Zugriff auf Daten im DBMS. Die [ORM]-Schicht fungiert als Brücke zwischen den von der [DAO]-Schicht verwalteten Objekten und den Zeilen und Spalten von Tabellen in einer relationalen Datenbank. Die JPA-Spezifikation (Java Persistence API) ermöglicht eine Abstraktion vom verwendeten ORM, sofern dieses die Spezifikationen implementiert. Dies ist hier der Fall, und wir werden die ORM-Schicht fortan als JPA-Schicht bezeichnen;
  • Die Integration der Schichten wird vom Spring-Framework übernommen;

13.3. Das Spring-MVC-Entwicklungsmodell

Spring MVC implementiert das MVC-Architekturmuster (Model–View–Controller) wie folgt:

Die Verarbeitung einer Client-Anfrage verläuft wie folgt:

  1. Anfrage – die angeforderten URLs haben die Form http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Der [Front Controller] nutzt eine Konfigurationsdatei oder Java-Annotationen, um die Anfrage an den richtigen Controller und die richtige Aktion innerhalb dieses Controllers weiterzuleiten. Dazu verwendet er das Feld [Action] der URL. Der Rest der URL [/param1/param2/...] besteht aus optionalen Parametern, die an die Aktion übergeben werden. Das C in MVC bezieht sich hier auf die Kette [Front Controller, Controller, Action]. Wenn kein Controller die angeforderte Aktion verarbeiten kann, antwortet der Webserver, dass die angeforderte URL nicht gefunden wurde.
  2. Verarbeitung
    • Die ausgewählte Aktion kann die Parameter verwenden, die der [Front Controller] an sie übergeben hat. Diese können aus verschiedenen Quellen stammen:
    • dem Pfad [/param1/param2/...] der URL,
    • die [p1=v1&p2=v2]-Parameter der URL,
    • aus Parametern, die der Browser mit seiner Anfrage gesendet hat;
    • Bei der Verarbeitung der Benutzeranfrage benötigt die Aktion möglicherweise die [Business]-Schicht [2b]. Sobald die Anfrage des Clients verarbeitet wurde, kann dies verschiedene Antworten auslösen. Ein klassisches Beispiel ist:
    • eine Fehlerseite, wenn die Anfrage nicht korrekt verarbeitet werden konnte
    • ansonsten eine Bestätigungsseite
    • die Aktion weist an, eine bestimmte Ansicht anzuzeigen [3]. Diese Ansicht zeigt Daten an, die als View-Modell bezeichnet werden. Dies ist das M in MVC. Die Aktion erstellt dieses M-Modell [2c] und weist an, eine V-Ansicht anzuzeigen [3];
  3. Antwort – die ausgewählte Ansicht V verwendet das von der Aktion erstellte Modell M, um die dynamischen Teile der HTML-Antwort zu initialisieren, die sie an den Client senden muss, und sendet dann diese Antwort.

Bei einem Webdienst / JSON wird die vorstehende Architektur leicht modifiziert:

  • In [4a] wird das Modell, bei dem es sich um eine Java-Klasse handelt, durch eine JSON-Bibliothek in eine JSON-Zeichenkette umgewandelt;
  • in [4b] wird diese JSON-Zeichenkette an den Browser gesendet;

Lassen Sie uns nun die Beziehung zwischen der MVC-Webarchitektur und der Schichtenarchitektur klären. Je nachdem, wie das Modell definiert ist, können diese beiden Konzepte miteinander in Verbindung stehen oder auch nicht. Betrachten wir eine einschichtige Spring-MVC-Webanwendung:

Wenn wir die [Web]-Schicht mit Spring MVC implementieren, erhalten wir zwar eine MVC-Webarchitektur, jedoch keine mehrschichtige Architektur. Hier übernimmt die [Web]-Schicht alles: Darstellung, Geschäftslogik und Datenzugriff. Diese Aufgaben werden von den Aktionen ausgeführt.

Betrachten wir nun eine mehrschichtige Webarchitektur:

Die [Web]-Schicht kann ohne Framework und ohne Befolgung des MVC-Musters implementiert werden. In diesem Fall haben wir zwar immer noch eine mehrschichtige Architektur, aber die Web-Schicht implementiert das MVC-Muster nicht.

In der .NET-Welt lässt sich die oben beschriebene [Web]-Schicht beispielsweise mit ASP.NET MVC implementieren, was zu einer mehrschichtigen Architektur mit einer [Web]-Schicht im MVC-Stil führt. Allerdings kann diese ASP.NET-MVC-Schicht durch eine klassische ASP.NET-Schicht (WebForms) ersetzt werden, während der Rest (Geschäftslogik, DAO, ORM) unverändert bleibt. Wir haben dann eine Schichtenarchitektur mit einer [Web]-Schicht, die nicht mehr MVC-basiert ist.

In MVC haben wir gesagt, dass das M-Modell das der V-Ansicht ist, d. h. die Menge der von der V-Ansicht angezeigten Daten. Es gibt eine weitere Definition des M-Modells in MVC:

Viele Autoren sind der Ansicht, dass das, was rechts von der [Web]-Schicht liegt, das M-Modell von MVC bildet. Um Mehrdeutigkeiten zu vermeiden, können wir uns auf Folgendes beziehen:

  • das Domänenmodell, wenn wir uns auf alles rechts von der [Web]-Schicht beziehen
  • das View-Modell, wenn wir uns auf die von einer Ansicht V angezeigten Daten beziehen

Im Folgenden bezieht sich der Begriff „M-Modell“ ausschließlich auf das Modell einer V-Ansicht.

13.4. Ein Web/JSON-Projekt mit Spring MVC

Die Website [http://spring.io/guides] bietet Einführungs-Tutorials zur Erkundung des Spring-Ökosystems. Wir werden einem davon folgen, um die für ein Spring-MVC-Projekt erforderliche Maven-Konfiguration zu ermitteln.

13.4.1. Das Demo-Projekt

  • In [1] importieren wir einen der Spring-Guides;
  • in [2] wählen wir das Beispiel [Rest Service] aus;
  • in [3] wählen wir das Maven-Projekt aus;
  • in [4] wählen wir die endgültige Version des Leitfadens aus;
  • in [5] bestätigen wir;
  • in [6] das importierte Projekt;

Webdienste, auf die über Standard-URLs zugegriffen werden kann und die JSON-Daten zurückgeben, werden oft als REST-Dienste (REpresentational State Transfer) bezeichnet. Ein Dienst gilt als RESTful, wenn er bestimmte Regeln befolgt.

Betrachten wir nun das importierte Projekt, beginnend mit seiner Maven-Konfiguration.

13.4.2. Maven-Konfiguration

Die Datei [pom.xml] sieht wie folgt aus:


<?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>
  • Zeilen 6–8: Die Maven-Projekteigenschaften. Ein [<packaging>]-Tag, das den vom Maven-Build erzeugten Dateityp angibt, fehlt. In diesem Fall wird der Typ [jar] verwendet. Die Anwendung ist daher eine konsolenbasierte ausführbare Anwendung und keine Webanwendung; in diesem Fall wäre die Verpackung [war];
  • Zeilen 10–14: Das Maven-Projekt hat ein übergeordnetes Projekt [spring-boot-starter-parent]. Dieses definiert die meisten Abhängigkeiten des Projekts. Diese können ausreichend sein – in diesem Fall werden keine zusätzlichen Abhängigkeiten hinzugefügt – oder auch nicht; in diesem Fall werden die fehlenden Abhängigkeiten hinzugefügt;
  • Zeilen 17–20: Das Artefakt [spring-boot-starter-web] enthält die Bibliotheken, die für ein Spring-MVC-Webservice-Projekt erforderlich sind, bei dem keine Ansichten generiert werden. Dieses Artefakt umfasst eine sehr große Anzahl von Bibliotheken, darunter auch solche für einen eingebetteten Tomcat-Server. Die Anwendung wird auf diesem Server ausgeführt;

Die in dieser Konfiguration enthaltenen Bibliotheken sind sehr zahlreich:

Oben sehen wir die drei Tomcat-Server-Archive.

13.4.3. Die Architektur eines Spring [Web/JSON]-Dienstes

Für einen Web-/JSON-Dienst implementiert Spring MVC das MVC-Modell wie folgt:

  • In [4a] wird das Modell – eine Java-Klasse – durch eine JSON-Bibliothek in eine JSON-Zeichenkette umgewandelt;
  • in [4b] wird diese JSON-Zeichenkette an den Browser gesendet;

13.4.4. Der C-Controller

  

Die importierte Anwendung verfügt über den folgenden Controller:


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));
    }
}
  • Zeile 9: Die Annotation [@RestController] macht die Klasse [GreetingController] zu einem Spring-Controller, was bedeutet, dass ihre Methoden für die Verarbeitung von URLs registriert sind. Wir haben bereits die ähnliche Annotation [@Controller] gesehen. Der Rückgabetyp der Methoden dieses Controllers war [String], also der Name der anzuzeigenden Ansicht. Hier ist das anders. Die Methoden eines [@RestController] geben Objekte zurück, die serialisiert und an den Browser gesendet werden. Die Art der durchgeführten Serialisierung hängt von der Spring-MVC-Konfiguration ab. Hier werden sie in JSON serialisiert. Das Vorhandensein einer JSON-Bibliothek in den Projektabhängigkeiten bewirkt, dass Spring Boot das Projekt automatisch auf diese Weise konfiguriert;
  • Zeile 14: Die Annotation [@RequestMapping] gibt die von der Methode bearbeitete URL an, in diesem Fall die URL [/greeting];
  • Zeile 15: Die Annotation [@RequestParam] haben wir bereits erläutert. Das von der Methode zurückgegebene Ergebnis ist ein Objekt vom Typ [Greeting].
  • Zeile 12: Eine Long-Integer-Variable vom Typ „atomic“. Das bedeutet, dass sie parallelen Zugriff unterstützt. Es kann vorkommen, dass mehrere Threads gleichzeitig die Variable [counter] inkrementieren wollen. Dies wird ordnungsgemäß gehandhabt. Ein Thread kann den Wert des Zählers erst lesen, wenn der Thread, der ihn gerade ändert, seine Änderung abgeschlossen hat.

13.4.5. Das M-Modell

Das durch die vorherige Methode erzeugte M-Modell ist das folgende [Greeting]-Objekt:

  

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;
    }
}

Die JSON-Umwandlung dieses Objekts erzeugt die Zeichenfolge {"id":n,"content":"text"}. Letztendlich hat die von der Controller-Methode erzeugte JSON-Zeichenfolge folgende Form:

{"id":2,"content":"Hello, World!"}

oder

{"id":2,"content":"Hello, John!"}

13.4.6. Ausführung

  

Die Klasse [Application.java] ist die ausführbare Klasse des Projekts. Ihr Code lautet wie folgt:


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);
    }
 
}

Wir sind diesem Code bereits im vorherigen Beispiel begegnet und haben ihn dort erklärt.

13.4.7. Das Projekt ausführen

Führen wir das Projekt aus:

 

Wir erhalten die folgenden Konsolenprotokolle:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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)
  • Zeile 13: Der Tomcat-Server startet auf Port 8080 (Zeile 12);
  • Zeile 17: Das Servlet [DispatcherServlet] ist vorhanden;
  • Zeile 20: Die Methode [GreetingController.greeting] wurde gefunden;

Um die Webanwendung zu testen, rufen wir die URL [http://localhost:8080/greeting] auf:

 

Wir erhalten die erwartete JSON-Zeichenkette. Es könnte interessant sein, die vom Server gesendeten HTTP-Header anzusehen. Dazu verwenden wir die Chrome-Erweiterung namens [Advanced Rest Client] (Chrome / Strg-T / Menü [Anwendungen] / [Advanced Rest Client] – siehe Anhang, Abschnitt 22.5):

  • in [1] die angeforderte URL;
  • in [2] wird die GET-Methode verwendet;
  • in [3] die JSON-Antwort;
  • in [4] hat der Server angegeben, dass er eine Antwort im JSON-Format sendet;
  • in [5] fordern wir dieselbe URL an, diesmal jedoch mit einer POST-Anfrage;
  • in [7] werden die Informationen im [urlencoded]-Format an den Server gesendet;
  • in [6] der Parameter „name“ und sein Wert;
  • in [8] teilt der Browser dem Server mit, dass er [urlencoded]-Informationen sendet;
  • in [9] die JSON-Antwort des Servers;

13.4.8. Erstellen eines ausführbaren Archivs

Wir erstellen nun ein ausführbares Archiv:

  • in [1]: Wir führen ein Maven-Target aus;
  • in [2]: Es gibt zwei Ziele: [clean], um den Ordner [target] aus dem Maven-Projekt zu löschen, und [package], um ihn neu zu generieren;
  • in [3]: Der generierte Ordner [target] befindet sich in diesem Ordner;
  • in [4]: Wir generieren das Ziel;

In den Protokollen, die in der Konsole angezeigt werden, ist es wichtig, das Plugin [spring-boot-maven-plugin] zu sehen. Dies ist das Plugin, das das ausführbare Archiv generiert.

[INFO] --- spring-boot-maven-plugin:1.1.0.RELEASE:repackage (default) @ gs-rest-service ---

Navigieren Sie über die Konsole zum generierten Ordner:


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
...
  • Zeile 5: das erstellte Archiv;

Dieses Archiv wird wie folgt ausgeführt:


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

Nachdem die Webanwendung nun ausgeführt wird, können Sie über einen Browser darauf zugreifen:

 

13.4.9. Bereitstellung der Anwendung auf einem Tomcat-Server

Wie bereits beim vorherigen Projekt ändern wir die Datei [pom.xml] wie folgt:


<?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>
  • Zeile 9: Sie müssen angeben, dass Sie eine WAR-Datei (Web Archive) erstellen möchten;

Außerdem müssen Sie die Webanwendung konfigurieren. Wenn keine [web.xml]-Datei vorhanden ist, erfolgt dies über eine Klasse, die [SpringBootServletInitializer] erweitert:

  

Die Klasse [ApplicationInitializer] sieht wie folgt aus:


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);
    }
 
}
  • Zeile 6: Die Klasse [ApplicationInitializer] erweitert die Klasse [SpringBootServletInitializer];
  • Zeile 9: Die Methode [configure] wird überschrieben (Zeile 8);
  • Zeile 10: Die Klasse, die das Projekt konfiguriert, wird bereitgestellt;

Um das Projekt auszuführen, gehen Sie wie folgt vor:

  • Führen Sie in [1-2] das Projekt auf einem der in der Eclipse-IDE registrierten Server aus;

Sobald dies geschehen ist, können Sie die URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] in einem Browser aufrufen:

 

13.4.10. Fazit

Wir haben eine Art von Spring-MVC-Projekt vorgestellt, bei dem die Webanwendung einen JSON-Stream an den Browser sendet. Wir werden nun eine Web-/JSON-Anwendung entwickeln, um die im Tutorial [Einführung in Spring Data] behandelte Datenbank [dbintrospringdata] im Web verfügbar zu machen.

13.5. Bereitstellung der Datenbank [dbintrospringdata] im Web

13.5.1. Web/JSON-Service-Architektur

Wir werden die folgende Architektur implementieren:

Die [DAO]- und [JPA]-Schichten werden durch die im Tutorial [Einführung in Spring Data] beschriebene Anwendung implementiert.

13.5.2. Installation der Datenbank

  

Das SQL-Skript [dbintrospringdata.sql] erstellt die für den Test erforderliche MySQL-Datenbank.

13.5.3. Das Eclipse-Projekt für den Webservice / JSON

Das Eclipse-Projekt für den Webservice / JSON sieht wie folgt aus:

  

Dies ist ein Maven-Projekt, dessen [pom.xml]-Datei wie folgt aussieht:


<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>
  • Zeilen 11–15: das übergeordnete Maven-Projekt, das bereits für die [DAO]-Schicht verwendet wurde;
  • Zeilen 18–22: die Abhängigkeit zur [DAO]-Schicht;
  • Zeilen 23–26: die Abhängigkeit vom Artefakt [spring-boot-starter-web]. Dieses Artefakt enthält alle Abhängigkeiten, die zum Erstellen eines Web-/JSON-Dienstes benötigt werden. Es enthält jedoch auch unnötige Bibliotheken. Eine präzisere Konfiguration wäre daher erforderlich. Diese Konfiguration ist jedoch für den Einstieg nützlich;
  • Zeilen 28–30: Die Abhängigkeit vom Artefakt [spring-boot-starter] ermöglicht die Verwaltung von Spring-Boot-Annotationen;

Die durch diese Konfiguration eingeführten Abhängigkeiten lauten wie folgt:

  • In [1] ist zu sehen, dass Eclipse die Abhängigkeit vom Projektarchiv [intro-spring-data-01] erkannt hat;

Die oben genannten Abhängigkeiten betreffen sowohl die [DAO]-Schicht als auch die [Web]-Schicht.

13.5.3.1. Konfiguration der [web]-Schicht

Die [web]-Schicht wird über eine [AppConfig]-Datei konfiguriert:

  

Die Klasse [WebConfig] konfiguriert die [Web]-Schicht:


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 {
 
    // -------------------------------- layer configuration [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);
    }
 
    // filters jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
 
    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
}
  • Zeile 18: Die Annotation [@EnableWebMvc] löst automatische Konfigurationen für das Spring MVC-Framework aus;
  • Zeile 19: Die Klasse [WebConfig] erweitert die Spring-Klasse [WebMvcConfigurerAdapter], um bestimmte Beans neu zu definieren (Zeilen 26–40);
  • Zeilen 22–23: Einbindung des Spring-Kontexts;
  • Zeilen 25–29: Definition des Servlets des Spring MVC-Frameworks, das HTTP-Anfragen an den richtigen Controller und die richtige Methode weiterleitet. [DispatcherServlet] ist eine Spring-Klasse;
  • Zeilen 31–34: Wir legen fest, dass dieses Servlet alle URLs verarbeitet;
  • Zeilen 36–39: Das Vorhandensein dieses Beans aktiviert den in den Projektarchiven enthaltenen Tomcat-Server. Er lauscht auf Anfragen am Port 8080;
  • Zeilen 42–91: Beans, die zur Verwaltung von JSON-Filtern verwendet werden;
  • Zeilen 42–45: ein JSON-Mapper ohne Filter;
  • Zeilen 47–57: der JSON-Mapper, mit dem Sie eine Kategorie zusammen mit ihren Produkten abrufen können. Beachten Sie, dass Sie bei der Anforderung einer Kategorie mit ihren Produkten sowohl den JSON-Filter für die Klasse [Category] als auch den für die Klasse [Product] konfigurieren müssen. Dies ist immer der Fall. Bei der Serialisierung/Deserialisierung einer Klasse in JSON müssen Sie den JSON-Filter für die Klasse und die für alle darin enthaltenen Abhängigkeiten konfigurieren;
  • Zeilen 59–69: Der JSON-Mapper, der es ermöglicht, ein Produkt zusammen mit seiner Kategorie anzuzeigen;
  • Zeilen 71–80: der JSON-Mapper, der es ermöglicht, eine Kategorie ohne ihre Produkte anzuzeigen;
  • Zeilen 82–91: der JSON-Mapper, mit dem Sie ein Produkt ohne seine Kategorie abrufen können;

Die Klasse [AppConfig] konfiguriert die gesamte Anwendung, d. h. die Schichten [web] und [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 {
 
}
  • Zeile 9: importiert die Beans aus der [DAO]-Schicht und aus der [Web]-Schicht;
  • Zeile 8: gibt die Pakete an, in denen andere Spring-Beans zu finden sind;

Beachten Sie, dass wir die Annotation [@EnableAutoConfiguration] nirgendwo verwendet haben. Wir haben es vorgezogen, die Konfiguration selbst zu steuern.

13.5.4. Das Anwendungsmodell

  

Die Klasse [ApplicationModel] sieht wie folgt aus:


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 {
 
    // the [DAO] layer
    @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);
    }
 
}
  • Zeile 12: Die Klasse ist ein Spring-Singleton;
  • Zeile 13: die die [IDao]-Schnittstelle der [DAO]-Schicht implementiert;
  • Zeilen 16–17: Injektion einer Referenz in die [DAO]-Schicht;
  • Zeilen 19–99: Implementierung der [IDao]-Schnittstelle;

Die Architektur der Webschicht entwickelt sich wie folgt:

  • In [2b] kommunizieren die Methoden des/der Controller(s) mit dem [ApplicationModel]-Singleton;

Diese Strategie bietet Flexibilität hinsichtlich der Verwaltung eines potenziellen Caches. Die Klasse [ApplicationModel] kann verwendet werden, um Informationen aus der [DAO]-Schicht oder Konfigurationsdaten zu speichern. Dies kann nützlich sein, wenn Sie keine Kontrolle über die [DAO]-Schicht haben. Diese Caching-Strategie kann sich im Laufe der Zeit weiterentwickeln. Änderungen haben keine Auswirkungen auf den Code des/der Controller(s).

13.5.5. Der Controller

  

Hier haben wir nur einen Controller, die Klasse [MyController].

13.5.5.1. Veröffentlichte URLs

Die von diesem Controller bereitgestellten URLs lauten wie folgt:


    @RequestMapping(value = "/addProducts",
method = RequestMethod.POST,
content-type = "application/json; charset=UTF-8")
    public String addProducts(HttpServletRequest request) {
...
    }
Fügt Produkte zur Datenbank hinzu. Diese werden per POST gesendet. Die Antwort ist eine JSON-Zeichenkette, die die Liste der hinzugefügten Produkte mit ihren Primärschlüsseln enthält.

    @RequestMapping(value = "/deleteAllProducts",
method = RequestMethod.GET)
    public String deleteAllProducts() {
..
    }
Löscht alle Produkte aus der Datenbank.

    @RequestMapping(value = "/updateProducts",
method = RequestMethod.POST,
content-type = "application/json; charset=UTF-8")
    public String updateProducts(HttpServletRequest request) {
..
    }

Aktualisiert Produkte in der Datenbank. Diese werden gepostet. Die Antwort ist eine JSON-Zeichenkette, die die Liste der aktualisierten Produkte enthält.

    @RequestMapping(value = "/getAllProducts",
method = RequestMethod.GET)
    public String getAllProducts() {
..
    }

Ruft die JSON-Zeichenkette für alle Produkte ab.

    @RequestMapping(value = "/addCategories",
method = RequestMethod.POST,
content-type = "application/json; charset=UTF-8")
    public String addCategories(HttpServletRequest request) {
..
    }

Fügt Kategorien zur Datenbank hinzu. Diese werden per POST gesendet. Die Antwort ist ein JSON-String, der die Liste der hinzugefügten Kategorien zusammen mit ihren Primärschlüsseln enthält. Wenn die Kategorien Produkte enthalten, werden diese ebenfalls zur Datenbank hinzugefügt.

    @RequestMapping(value = "/deleteAllCategories",
method = RequestMethod.GET)
    public String deleteAllCategories() {
...
    }

Löscht alle Kategorien aus der Datenbank sowie alle darin enthaltenen Produkte. Danach ist die Datenbank leer.

    @RequestMapping(value = "/updateCategories",
method = RequestMethod.POST,
content-type = "application/json; charset=UTF-8")
    public String updateCategories
(HttpServletRequest request) {
...
    }

Aktualisiert Kategorien in der Datenbank. Diese werden per POST gesendet. Die Antwort ist die Liste der aktualisierten Kategorien. Wenn die Kategorien Produkte enthalten, werden diese ebenfalls in der Datenbank aktualisiert. Gibt die JSON-Zeichenkette der geänderten Kategorien zurück;

    @RequestMapping(value = "/getAllCategories",
method = RequestMethod.GET)
    public String getAllCategories() {
...
    }

Ruft die JSON-Zeichenkette für alle Kategorien ab.

    @RequestMapping(value = "/getProductByIdWithCategory/{productId}",
method = RequestMethod.GET)
    public String getProductByIdWithCategory
(@PathVariable("productId") Long productId) {
...
    }

Ruft die JSON-Zeichenkette für ein Produkt ab, das anhand seiner ID identifiziert wird, zusammen mit seiner Kategorie.

    @RequestMapping(value = "/getProductByIdWithoutCategory/{productId}",
method = RequestMethod.GET)
    public String getProductByIdWithoutCategory
@PathVariable("productId") Long productId) {
...
    }

Ruft die JSON-Zeichenkette für ein Produkt ab, das anhand seiner ID identifiziert wird, ohne dessen Kategorie.

    @RequestMapping(value = "/getProductByNameWithCategory/{name}",
method = RequestMethod.GET)
    public String getProductByNameWithCategory(
@PathVariable("name") String name) {
...
    }

Ruft die JSON-Zeichenkette für ein Produkt ab, das anhand seines Namens identifiziert wird, zusammen mit seiner Kategorie.

    @RequestMapping(value = "/getProductByNameWithoutCategory/{name}",
method = RequestMethod.GET)
    public String getProductByNameWithoutCategory
(@PathVariable("name") String name) {
...
    }

Ruft die JSON-Zeichenkette für ein Produkt ab, das anhand seines Namens identifiziert wird, ohne dessen Kategorie.

    @RequestMapping(value = "/getCategoryByIdWithProducts/{categoryId}",
 method = RequestMethod.GET)
    public String getCategoryByIdWithProducts
@PathVariable("idCategorie") Long idCategorie) {
...
    }

Ruft die JSON-Zeichenkette für eine anhand ihrer ID angegebene Kategorie zusammen mit den dazugehörigen Produkten ab.

    @RequestMapping(value = "/getCategoryByNameWithProducts/{name}",
method = RequestMethod.GET)
    public String getCategoryByNameWithProducts
(@PathVariable("name") String name) {
...
    }

Ruft die JSON-Zeichenkette für eine anhand ihres Namens angegebene Kategorie zusammen mit den dazugehörigen Produkten ab.

    @RequestMapping(value = "/getCategoryByNameWithoutProducts/{name}",
method = RequestMethod.GET)
    public String getCategoryByNameWithoutProducts(
@PathVariable("name") String name) {
...
    }

Ruft die JSON-Zeichenkette für eine Kategorie ab, die durch ihren Namen angegeben wird, ohne deren Produkte.

    @RequestMapping(value = "/getCategoryByIdWithoutProducts/{categoryId}",
method = RequestMethod.GET)
    public String getCategoryByIdWithoutProducts(
@PathVariable("idCategorie") Long idCategorie) {
...
    }

Ruft die JSON-Zeichenkette für eine Kategorie ab, die durch ihre ID identifiziert wird, wobei ihre Produkte ausgeschlossen werden.

Die offengelegten URLs entsprechen den Methoden der [IDao]-Schnittstelle in der [DAO]-Schicht. Die Methoden des Webdienstes / JSON basieren alle auf demselben Modell. Wir werden einige davon näher betrachten.

13.5.5.2. Das Controller-Gerüst

Das Controller-Gerüst sieht wie folgt aus:


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 {
 
    // spring dependencies
    @Autowired
    private ApplicationModel application;
 
    // filters 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;
 
    // class [MyController] is a singleton and is instantiated only once the 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 {
        ...
    }
 
  • Zeile 28: Die Annotation [@Controller] macht die Klasse zu einer Spring-Komponente;
  • Zeilen 32–33: Einfügen einer Referenz auf die Klasse [ApplicationModel];
  • Zeilen 36–50: Injektion von Referenzen auf die JSON-Mapper;
  • Zeile 58: Die exponierte URL lautet [/addProducts]. Der Client muss eine [POST]-Methode verwenden, um seine Anfrage zu stellen (method = RequestMethod.POST). Er muss den gesendeten Wert als JSON-String übermitteln (content-type = "application/json; charset=UTF-8"). Die Methode selbst gibt die Antwort an den Client zurück (Zeile 59). Dies ist eine Zeichenkette (Zeile 60). Der HTTP-Header [Content-type: application/json; charset=UTF-8] wird an den Client gesendet, um anzuzeigen, dass er eine JSON-Zeichenkette erhält (Zeile 58);
  • Zeile 60: Die Methode [addProduits] gibt die JSON-Zeichenkette zurück, die die Liste der zur Datenbank hinzugefügten Produkte enthält;

13.5.5.3. Antworten von Controller-Methoden

Alle Controller-Methoden geben den folgenden Typ [Response] zurück:

  

package spring.webjson.service;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
    ...
}
  • Zeile 5: Die Antwort kapselt einen Typ T;
  • Zeile 13: die Antwort vom Typ T;
  • Zeilen 9–11: Eine Methode kann auf eine Ausnahme stoßen. In diesem Fall gibt sie eine Antwort zurück mit:
    • Zeile 9: status!=0;
    • Zeile 11: die Liste der aufgetretenen Fehler;

13.5.5.4. Die URL [/addProducts]

Die URL [/addProducts] wird von der folgenden Methode verarbeitet:


@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 {
        // answer
        Response<List<Produit>> response;
        try {
            // retrieve the posted value
            String body = CharStreams.toString(request.getReader());
            List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
            });
            // we re-establish the link between products and categories
            for (Produit produit : produits) {
                produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
            }
            // we persist products
            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);
        }
        // answer jSON
        return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
    }
  • Zeile 3: Die Methode nimmt [HttpServletRequest request] als Parameter entgegen, der alle Informationen zur Anfrage des Clients enthält;
  • Zeile 5: Die Antwort, die an den Client gesendet wird: eine Liste von Produkten;
  • Zeile 8: Wir rufen den gesendeten Wert ab. Die Klasse [CharStreams] gehört zur Bibliothek [Google Guava], deren Referenz wir der Datei [pom.xml] hinzugefügt haben. Wir erhalten die vom Client gesendete JSON-Zeichenkette. Wir müssen sie deserialisieren, um damit arbeiten zu können;
  • Zeilen 8–10: Die Deserialisierung wird durchgeführt. Wir erhalten eine Liste von Produkten, wobei jedes Produkt ein Feld [category=null] aufweist;
  • Zeilen 12–14: Wir setzen das Feld [category] für alle Produkte in der Liste zurück. Dazu verwenden wir das Feld [categoryId] des Produkts, das initialisiert ist;
  • Zeile 16: Die Produkte werden in die Datenbank eingefügt;
  • Zeile 17: Das [response]-Objekt wird mit der Liste der Produkte initialisiert;
  • Zeilen 18–19: Fall, in dem die Methode auf eine Ausnahme aus der [DAO]-Schicht stößt. Wir initialisieren die Antwort mit [status=1000] (Fehlercode) [messages=e1.getMessages()], d. h., wir senden dem Client die Liste der auf der Serverseite aufgetretenen Fehler;
  • Zeilen 20–21: Fall, in dem die Methode auf eine andere Art von Ausnahme stößt. Wir initialisieren die Antwort mit [status=1000] (Fehlercode) [messages=getErrorsForException(e)], wobei [getErrorsForException] eine private Methode der Klasse ist, die die Liste der Fehler zurückgibt, die mit den Ausnahmen im Ausnahmestapel von e verbunden sind, und [body=null];
  • Zeile 24: Die JSON-Zeichenkette der Antwort wird zurückgegeben;

13.5.5.5. Die URL [/getAllProducts]

Die URL [/getAllProducts] wird von der folgenden Methode verarbeitet:


    @RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllProduits() throws JsonProcessingException {
        // answer
        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);
        }
        // answer jSON
        return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
  • Zeile 1: Die URL [/getAllProduits] wird mit einer [GET]-Anfrage aufgerufen. Sie gibt JSON zurück;
  • Zeile 2: Die Methode selbst sendet die JSON-Antwort an den Client;
  • Zeile 5: Die Methode gibt eine JSON-Zeichenkette vom Typ [Response<List<Product>>] zurück;
  • Zeile 7: Produkte werden ohne ihre Kategorie angefordert;
  • Zeilen 8–12: Im Falle eines Fehlers wird die Antwort mit einem Fehlercode und Fehlermeldungen initialisiert;
  • Zeile 14: Die JSON-Antwort wird an den Client gesendet;

13.5.5.6. Fazit

Auf die anderen Methoden des Controllers werden wir nicht eingehen. Sie ähneln der einen oder anderen der beiden Methoden, die wir gerade vorgestellt haben.

13.5.6. Die Webdienst-/JSON-Ausführungsklasse

  

Die Klasse [Boot] ist die ausführbare Klasse des Projekts:


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);
    }
}
  • Zeile 10: Die statische Methode [SpringApplication.run] wird ausgeführt. Die Klasse [SpringApplication] ist eine Klasse aus dem [Spring Boot]-Projekt (Zeile 3). Ihr werden zwei Parameter übergeben:
    • [AppConfig.class]: die Klasse, die die gesamte Anwendung konfiguriert;
    • [args]: alle Argumente, die in Zeile 9 an die Methode [main] übergeben wurden. Dieser Parameter wird hier nicht verwendet;

Wenn diese Klasse ausgeführt wird, werden die folgenden Protokolleinträge generiert:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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)
  • Zeilen 17–19: Tomcat-Server-Start zum Ausführen des Web-/JSON-Dienstes;
  • Zeilen 25–33: Aufbau der [DAO]-Schicht;
  • Zeilen 32–51: Die exponierten URLs werden ermittelt;

13.5.7. Webservice-/JSON-Tests

Um die Tests durchzuführen, generieren wir die MySQL-Datenbank [dbintrospringdata] aus dem SQL-Skript [dbintrospringdata.sql]:

  

Sobald dies erledigt ist, verwenden wir den [Advanced Rest Client] (siehe Abschnitt 22.5), um die vom Webdienst / JSON bereitgestellten URLs abzufragen (der Webdienst / JSON muss dabei laufen).

  • In [1-3] rufen wir die URL [/getAllCategories] über eine HTTP-GET-Anfrage ab;

Wir erhalten die folgende Antwort:

  • In [1] die HTTP-Anfrage des Clients;
  • in [2] die HTTP-Antwort des Servers;
  • in [3] zeigt der Status [200 OK] an, dass der Server die Anfrage erfolgreich verarbeitet hat;
  • in [4] die JSON-Antwort des Servers;

Die vollständige JSON-Antwort lautet wie folgt:


{"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 bedeutet, dass keine serverseitigen Fehler aufgetreten sind;
  • messages: null bedeutet, dass keine Fehlermeldungen vorliegen;
  • body: ist der Hauptteil der Antwort, in diesem Fall die Liste der Kategorien mit ihren Produkten. Es gibt zwei Kategorien mit jeweils 5 Produkten;

Wir fügen das Produkt [product15] zur Kategorie [category1] hinzu. Dazu verwenden wir die URL [/addCategories], die folgenden Code enthält:


@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);
        // we persist categories
        try {
            // retrieve the posted value
            String body = CharStreams.toString(request.getReader());
            mapper.setFilters(jsonFilterCategorieWithProduits);
            List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
            });
            // we re-establish the link between products and categories
            for (Categorie categorie : categories) {
                Set<Produit> produits = categorie.getProduits();
                if (produits != null) {
                    for (Produit produit : categorie.getProduits()) {
                        produit.setCategorie(categorie);
                    }
                }
            }
            // we persist categories
            application.addCategories(categories);
            response = new Response<List<Categorie>>(0, null, categories);
        } catch (Exception e) {
            response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
        }
        // answer jSON
        return mapper.writeValueAsString(response);
    }
  • Zeile 1: Der Client muss eine POST-Anfrage senden, und der übermittelte Wert muss eine JSON-Zeichenkette sein;
  • Zeilen 9–12: Der übermittelte Wert muss eine Liste von Kategorien mit den zugehörigen Produkten sein;

Wir erstellen eine Kategorie [category2] mit einem Produkt [product21]. Die zu sendende JSON-Zeichenkette lautet dann wie folgt:

[{"id":null,"version":0,"nom":"categorie2","produits":[{"id":null,"version":0,"nom":"produit21","idCategorie":null,"prix":111.0,"description":"desc21"}]}]

Die Anfrage an den Webservice / JSON erfolgt wie folgt:

  • in [1] die angeforderte URL;
  • in [2] wird sie über einen POST-Vorgang angefordert;
  • in [3] die gesendete JSON-Zeichenkette;
  • in [4] wird dem Server mitgeteilt, dass JSON-Daten gesendet werden;

Die Antwort des Servers lautet wie folgt:

  • In [1] sehen wir, dass sowohl die Kategorie als auch ihr Produkt nun über einen Primärschlüssel verfügen, was darauf hindeutet, dass sie wahrscheinlich in die Datenbank eingefügt wurden. Wir werden dies anhand der URL [/getCategorieByNameWithProduits/categorie2] überprüfen:

Wir erhalten das folgende Ergebnis:

Wir haben tatsächlich die Kategorie [categorie2] mit ihrem einzigen Produkt [produit21] abgerufen. Wir können auch nur das Produkt abfragen. Dazu verwenden wir die URL [/getProduitByIdWithoutCategorie/1859]:

Wir erhalten das folgende Ergebnis:

Alle [GET]-Operationen können in einem Standard-Webbrowser ausgeführt werden:

 

Leser sind eingeladen, die anderen URLs des Webdienstes /json zu testen.

13.6. Ein für den Webdienst /json programmierter Client

Da die Datenbank [dbintrospringdata] nun im Web verfügbar ist, werden wir eine Anwendung schreiben, die sie nutzt. Wir werden dann die folgende Client-Server-Architektur haben:

Die Client-Anwendung wird zwei Schichten haben:

  • eine [DAO] [2]-Schicht zur Kommunikation mit der /json-Webanwendung, die die Datenbank bereitstellt;
  • eine JUnit-Testschicht [1], um zu überprüfen, ob Client und Server korrekt funktionieren;

13.6.1. Das Eclipse-Projekt

Das Eclipse-Projekt des Clients sieht wie folgt aus:

  
  • Der Ordner [src/main/java] implementiert die [DAO]-Schicht;
  • Der Ordner [src/test/java] enthält die JUnit-Tests;

13.6.2. Maven-Projektkonfiguration

Das Projekt ist ein Maven-Projekt, das durch die folgende [pom.xml]-Datei konfiguriert wird:


<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>
        <!-- jSON library used by 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>
        <!-- component used by 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>
        <!-- log library -->
        <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>
  • Zeilen 14–18: das übergeordnete Maven-Projekt [spring-boot-starter-parent], das es uns ermöglicht, eine Reihe von Abhängigkeiten zu definieren, ohne deren Versionen anzugeben, da diese im übergeordneten Projekt definiert sind;
  • Zeilen 22–25: Obwohl wir keine Webanwendung schreiben, benötigen wir die Abhängigkeit [spring-web], die die Klasse [RestTemplate] enthält, die eine einfache Anbindung an eine Web-/JSON-Anwendung ermöglicht;
  • Zeilen 27–34: eine JSON-Bibliothek;
  • Zeilen 36–39: eine Abhängigkeit, mit der wir ein Timeout für die HTTP-Anfragen des Clients festlegen können. Ein Timeout ist die maximale Wartezeit auf die Antwort des Servers. Nach Ablauf dieser Zeit signalisiert der Client einen Timeout-Fehler, indem er eine Ausnahme auslöst;
  • Zeilen 41–46: die Google Guava-Bibliothek, die im JUnit-Test verwendet wird. Aus diesem Grund haben wir ihren Geltungsbereich auf [test] gesetzt (Zeile 45). Das bedeutet, dass diese Abhängigkeit nur einbezogen wird, wenn Code aus dem Zweig [src/test/java] ausgeführt wird;
  • Zeilen 48–51: die Logging-Bibliothek;
  • Zeilen 52–63: Die Abhängigkeiten für JUnit-Tests. Dazu gehört insbesondere die für die Tests erforderliche JUnit-4-Bibliothek. Diese Abhängigkeiten sind mit dem Attribut [<scope>test</scope>] versehen, was darauf hinweist, dass sie nur für die Testphase benötigt werden. Sie werden nicht in das endgültige Projektarchiv aufgenommen;

13.6.3. Implementierung der [DAO]-Schicht

  
  • Das Paket [spring.client.config] enthält die Spring-Konfiguration für die [DAO]-Schicht;
  • Das Paket [spring.client.dao] enthält die Implementierung der [DAO]-Schicht;
  • Das Paket [spring.client.entities] enthält die Objekte, die mit dem Webservice / JSON ausgetauscht werden;

13.6.3.1. Konfiguration

  

Die Klasse [DaoConfig] übernimmt die Spring-Konfiguration der [DAO]-Schicht. Der Code lautet wie folgt:


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 {
 
    // constants
    static private final int TIMEOUT = 1000;
    static private final String URL_WEBJSON = "http://localhost:8080";
 
    @Bean
    public RestTemplate restTemplate(int timeout) {
        // creation of the RestTemplate component
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // exchange timeout
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // result
        return restTemplate;
    }
 
    @Bean
    public int timeout() {
        return TIMEOUT;
    }
 
    @Bean
    public String urlWebJson() {
        return URL_WEBJSON;
    }
 
    // filters jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }
 
    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // result
        return mapper;
    }
 
    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // filters
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // result
        return mapper;
    }
}
  • Zeile 13: Die Klasse ist eine Spring-Konfigurationsklasse – Spring-Komponenten befinden sich im Paket [spring.client.dao];
  • Zeile 17: Es wird ein Timeout von einer Sekunde (1000 ms) festgelegt;
  • Zeilen 32–35: Die Bean, die diesen Wert zurückgibt;
  • Zeile 18: Die URL des Webdienstes / JSON;
  • Zeilen 37–40: Die Bean, die diesen Wert zurückgibt;
  • Zeilen 20–30: Die Konfiguration der [RestTemplate]-Klasse, die die Kommunikation mit dem Webservice / JSON übernimmt. Wenn keine Konfiguration erforderlich ist, kann sie im Code mit einem einfachen [new RestTemplate()] instanziiert werden. Hier möchten wir das Timeout für die Kommunikation mit dem Webservice / JSON festlegen. Die [timeout]-Bean in Zeile 36 wird als Parameter an die [RestTemplate]-Methode in Zeile 24 übergeben;
  • Zeile 23: Die Komponente [HttpComponentsClientHttpRequestFactory] ermöglicht es uns, das Timeout für die Kommunikation festzulegen (Zeilen 29–30);
  • Zeile 24: Die Klasse [RestTemplate] wird mithilfe dieser Komponente instanziiert. Da sie für die Kommunikation mit dem Webdienst / JSON auf diese Komponente angewiesen ist, unterliegen die Datenaustausche tatsächlich dem Timeout;
  • Client und Server tauschen Textzeilen aus. Ein Konverter übernimmt die Serialisierung eines Objekts in Text und umgekehrt die Deserialisierung von Text in ein Objekt. Es können mehrere Konverter mit der [RestTemplate]-Klasse verknüpft sein, und welcher zu einem bestimmten Zeitpunkt ausgewählt wird, hängt von den vom Server gesendeten HTTP-Headern ab. Hier verwenden wir keinen Konverter. Daher wird die [RestTemplate]-Komponente nicht versuchen, die folgenden beiden Elemente in irgendeiner Weise zu konvertieren:
    • den gesendeten Text;
    • den als Antwort empfangenen Text;

Diese Texte sind JSON-Strings, die daher von der [RestTemplate]-Komponente unverändert belassen werden. Wir als Entwickler führen die erforderliche JSON-Serialisierung und -Deserialisierung selbst durch. Der Grund dafür ist, dass die Filter, die auf den gesendeten Wert und die empfangene Antwort angewendet werden sollen, unterschiedlich sein können, und die Erfahrung zeigt, dass es einfacher ist, diese selbst zu handhaben, als zu versuchen, die [RestTemplate]-Komponente so zu konfigurieren, dass sie den richtigen JSON-Konverter verwendet;

  • Zeilen 42–92: Definieren von JSON-Filtern. Diese entsprechen den serverseitigen Filtern, die in Abschnitt 13.5.3.1 vorgestellt und erläutert wurden;
  • Zeilen 43–46: ein JSON-Mapper ohne Filter;
  • Zeilen 64–68: ein JSON-Mapper zum Abrufen einer Kategorie ohne deren Produkte;
  • Zeilen 48–58: ein JSON-Mapper zum Abrufen einer Kategorie mit ihren Produkten;
  • Zeilen 83–92: ein JSON-Mapper zum Abrufen eines Produkts ohne dessen Kategorie;
  • Zeilen 60–70: ein JSON-Mapper zum Abrufen eines Produkts mit seiner Kategorie;

All diese Beans stehen sowohl dem Code der [DAO]-Schicht als auch dem JUnit-Test zur Verfügung.

13.6.3.2. Die Entitäten

  

Die Entitäten, die von der [DAO]-Schicht verarbeitet werden, sind diejenigen, die sie mit dem Webservice / JSON austauscht. Dabei handelt es sich um die Artikel und Produkte. Auf der Serverseite waren diese Entitäten mit JPA-Persistenz-Annotationen versehen. Hier wurden diese Annotationen entfernt. Wir fügen den Entitätscode zur Veranschaulichung noch einmal ein:

[AbstractEntity]


package spring.client.entities;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public abstract class AbstractEntity {
    // properties
    protected Long id;
    protected Long version;
 
    // manufacturers
    public AbstractEntity() {
 
    }
 
    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }
 
    // redefine [equals] and [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 and setters
...
}

[Kategorie]


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 {
 
    // properties
    private String nom;
 
    // related products
    public Set<Produit> produits = new HashSet<Produit>();
 
    // manufacturers
    public Categorie() {
 
    }
 
    public Categorie(String nom) {
        this.nom = nom;
    }
 
    // methods
    public void addProduit(Produit produit) {
        // we add the product
        produits.add(produit);
        // set your category
        produit.setCategorie(this);
    }
 
    // getters and setters
    ...
}

[Produkt]


package spring.webjson.client.entities;
 
import com.fasterxml.jackson.annotation.JsonFilter;
 
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {
 
    // the name
    private String nom;
   // category number
    private Long idCategorie;
   // the price
    private double prix;
   // the description
    private String description;
 
    // the category
    private Categorie categorie;
 
    // manufacturers
    public Produit() {
 
    }
 
    public Produit(String nom, double prix, String description) {
        this.nom = nom;
        this.prix = prix;
        this.description = description;
    }
 
    // getters and setters
...
}

13.6.3.3. Die Klasse [DaoException]

 

Wenn in der [DAO]-Schicht ein Fehler auftritt, wird eine [DaoException] ausgelöst. Diese Klasse wird auf der Serverseite verwendet und ist in Abschnitt 11.3.7 beschrieben.

13.6.3.4. Die Schnittstelle der [DAO]-Schicht

 

Die [DAO]-Schicht implementiert die in Abschnitt 11.3.7 beschriebene [IDao]-Schnittstelle.


package spring.client.dao;
 
import java.util.List;
 
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
 
public interface IDao {
 
    // insert product list
    public List<Produit> addProduits(List<Produit> produits);
 
    // removal of all products
    public void deleteAllProduits();
 
    // product list update
    public List<Produit> updateProduits(List<Produit> produits);
 
    // all products obtained
    public List<Produit> getAllProduits();
 
    // inserting a list of categories
    public List<Categorie> addCategories(List<Categorie> categories);
 
    // delete all categories
    public void deleteAllCategories();
 
    // updating a list of categories
    public List<Categorie> updateCategories(List<Categorie> categories);
 
    // obtaining all categories
    public List<Categorie> getAllCategories();
 
    // a special product
    public Produit getProduitByIdWithCategorie(Long idProduit);
 
    public Produit getProduitByIdWithoutCategorie(Long idProduit);
 
    public Produit getProduitByNameWithCategorie(String nom);
 
    public Produit getProduitByNameWithoutCategorie(String nom);
 
    // a special category
    public Categorie getCategorieByIdWithProduits(Long idCategorie);
 
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
 
    public Categorie getCategorieByNameWithProduits(String nom);
 
    public Categorie getCategorieByNameWithoutProduits(String nom);
 
}

13.6.3.5. Der Webservice / JSON-Antwort

  

Wir haben gesehen, dass alle URLs des Webdienstes / JSON einen in Abschnitt 13.5.5.3 definierten Typ [Response] zurückgeben. Wir geben diese Klasse hier wieder:


package spring.client.dao;
 
import java.util.List;
 
public class Response<T> {
 
    // ----------------- properties
    // operation status
    private int status;
    // any error messages
    private List<String> messages;
    // the body of the reply
    private T body;
 
    // manufacturers
    public Response() {
 
    }
 
    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }
 
    // getters and setters
    ...
}

13.6.3.6. Implementierung der Kommunikation mit dem Webdienst / JSON

  

Die Klasse [ AbstractDao] implementiert die Kommunikation mit dem Webservice / 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;
 
    // generic request
    protected String getResponse(String url, String jsonPost) {
 
        // url : URL to contact
        // jsonPost: the jSON value to be posted
        try {
            // request execution
            RequestEntity<?> request;
            if (jsonPost != null) {
                // query POST
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
            } else {
                // query GET
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            // execute the query
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new DaoException(20, e1);
        } catch (RuntimeException e2) {
            throw new DaoException(21, e2);
        }
    }
 
}
 
  • Zeilen 15–16: Einbindung der [RestTemplate]-Komponente, die die Kommunikation mit dem Server übernimmt;
  • Zeilen 17–18: Einbindung der Webservice-URL / JSON;

Die Implementierung der Methoden für die Kommunikation mit dem Server ist in der Methode [getResponse] zusammengefasst:

  • Zeile 21: Die Methode erhält 2 Parameter:
    • [url]: die angeforderte URL;
    • [jsonPost]: die zu sendende JSON-Zeichenkette oder andernfalls null. Wenn [jsonPost == null] ist, erfolgt die URL-Anfrage per GET; andernfalls per POST;
  • Zeile 38: Die Anweisung, die die Anfrage an den Server sendet und dessen Antwort empfängt. Die [RestTemplate]-Komponente bietet eine Vielzahl von Methoden für die Interaktion mit dem Server. Wir haben hier die [exchange]-Methode gewählt, es stehen jedoch auch andere zur Verfügung;
  • Zeilen 27–36: Wir müssen die [RequestEntity]-Anfrage erstellen. Diese unterscheidet sich je nachdem, ob wir eine GET- oder eine POST-Anfrage verwenden;
  • Zeilen 30–31: die Anfrage für einen GET. Die Klasse [RequestEntity] stellt statische Methoden zum Erstellen von GET-, POST-, HEAD- und anderen Anfragen bereit. Mit der Methode [RequestEntity.get] können Sie eine GET-Anfrage erstellen, indem Sie die verschiedenen Methoden, aus denen sie besteht, verketten:
    • Die Methode [RequestEntity.get] nimmt die Ziel-URL als Parameter in Form einer URI-Instanz entgegen;
    • die Methode [accept] ermöglicht es Ihnen, die Elemente des HTTP-Headers [Accept] zu definieren. Hier geben wir an, dass wir den Typ [application/json] akzeptieren, den der Server senden wird;
    • die Methode [build] verwendet diese Informationen, um den Typ [RequestEntity] der Anfrage zu erstellen;
  • Zeilen 34–35: die POST-Anfrage. Die Methode [RequestEntity.post] erstellt eine POST-Anfrage, indem sie die verschiedenen Methoden, aus denen sie besteht, miteinander verknüpft:
    • Die Methode [RequestEntity.post] nimmt die Ziel-URL als Parameter in Form einer URI-Instanz entgegen,
    • Die Methode [header] definiert einen HTTP-Header. Hier senden wir den Header [Content-Type: application/json] an den Server, um anzugeben, dass die übermittelten Daten in Form einer JSON-Zeichenkette ankommen werden;
    • Die Methode [accept] ermöglicht es uns anzugeben, dass wir den Typ [application/json] akzeptieren, den der Server senden wird;
    • Die Methode [body] legt den gesendeten Wert fest. Dies ist der vierte Parameter der generischen Methode [getResponse] (Zeile 1);
  • Zeile 38: Die Methode [RestTemplate].exchange gibt einen Typ [ResponseEntity<String>] zurück, der die gesamte Serverantwort kapselt: HTTP-Header und Dokumenttext. Die Methode [ResponseEntity].getBody() ruft diesen Text ab, der die Antwort des Servers darstellt – in diesem Fall eine Zeichenkette;

13.6.3.7. Implementierung der [IDao]-Schnittstelle

  

Die Klasse [Dao] implementiert die Schnittstelle [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;
 
    // filters 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) {
        // ----------- add products (without category)
        ...
}
  • Zeile 17: Die Klasse [Dao] ist eine Spring-Komponente, in die andere Spring-Komponenten injiziert werden können;
  • Zeile 18: Die Klasse [Dao] erweitert die soeben vorgestellte Klasse [AbstractDao] und implementiert die Schnittstelle [IDao];
  • Zeilen 20–21: Wir injizieren den Spring-Kontext, um auf dessen Beans zuzugreifen;
  • Zeilen 24–38: Injektion der JSON-Mapper, die in der in Abschnitt 13.6.2 vorgestellten [AppConfig]-Klasse definiert sind;

Die Implementierungen der verschiedenen Methoden der [IDao]-Schnittstelle folgen alle demselben Muster. Wir stellen zwei Methoden vor, eine basierend auf einer [POST]-Operation, die andere auf einer [GET]-Operation.

Ein Beispiel für [GET]: [getCategorieByNameWithProduits]


@Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        // ----------- obtain a category designated by its name, with its products
        try {
            // request
            Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
                    getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
                    new TypeReference<Response<Categorie>>() {
                    });
            // mistake?
            if (response.getStatus() != 0) {
                // 1 exception is thrown
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // render the core of the server response
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (RuntimeException | IOException e2) {
            throw new DaoException(113, e2);
        }
    }
  • Zeile 7: Die Methode [getResponse] der übergeordneten Klasse wird aufgerufen. Diese Methode übernimmt die Kommunikation mit dem Webdienst/JSON. Ihre Parameter lauten wie folgt:

getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null)
  • (Fortsetzung)
    • die URL des abgefragten Dienstes [/getCategoryByNameWithProducts/name];
    • der übermittelte Wert. Hier gibt es keinen;

Die Methode [getResponse] gibt einen String zurück, der die vom Server gesendete JSON-Antwort darstellt. Wir deserialisieren diese JSON-Antwort wie folgt:


jsonMapperCategorieWithProduits.readValue(
                    jsonResponse,
                    new TypeReference<Response<Categorie>>() {
});

da die JSON-Zeichenkette die Serialisierung eines Typs [Response<Category>] ist;

  • Zeilen 11–17: Wir prüfen den Antwortstatus. Ist der Status ungleich 0, lag ein serverseitiger Fehler vor. Wir lösen dann eine Ausnahme aus (Zeile 13) und verwenden dabei die in der Antwort enthaltenen Informationen (Status und Liste der Fehlermeldungen);
  • Zeile 16: Wenn kein serverseitiger Fehler aufgetreten ist, geben wir den Body vom Typ [Response<Category>] zurück, d. h. die angeforderte Kategorie;
  • Zeilen 18–19: Behandlung der in Zeile 16 ausgelösten Ausnahme;
  • Zeilen 20–22: Alle anderen Ausnahmen werden behandelt;

Ein Beispiel für [POST]: [addCategories]


@Override
    public List<Categorie> addCategories(List<Categorie> categories) {
        // ----------- add categories (with their products)
        try {
            // request
            Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
                    getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
                    new TypeReference<Response<List<Categorie>>>() {
                    });
            // mistake?
            if (response.getStatus() != 0) {
                // 1 exception is thrown
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // render the core of the server response
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (RuntimeException | IOException e2) {
            throw new DaoException(104, e2);
        }
    }
  • Zeile 2: Die Methode [addCategories] wird verwendet, um die als Parameter übergebenen Kategorien in der Datenbank zu speichern. Sie gibt dieselben Kategorien zurück, ergänzt um ihre Primärschlüssel. Wenn die Kategorien zusammen mit Produkten übergeben werden, werden diese ebenfalls gespeichert;
  • Zeile 7: Die [getResponse]-Methode des übergeordneten Objekts wird aufgerufen, um die Kommunikation mit dem Webservice / JSON zu handhaben;
    • Der erste Parameter ist die URL [/addCategories];
    • der zweite Parameter ist der übermittelte Wert, in diesem Fall die Liste der zu speichernden Kategorien;

getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories))

Die resultierende JSON-Zeichenkette wird anschließend deserialisiert, um den erwarteten Typ [Response<List<Category>>] zu erhalten:


Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
                    jsonResponse,
                    new TypeReference<Response<List<Categorie>>>() {
                    });
  • Zeilen 11–17: Verarbeitung der Serverantwort (Fehler oder nicht);
  • Zeilen 20–22: Ausnahmebehandlung;

Alle anderen Methoden folgen dem Muster der beiden vorgestellten Methoden.

13.6.4. Der JUnit-Test

Kehren wir nun zur derzeit in Entwicklung befindlichen Client/Server-Architektur zurück:

Wir haben eine [DAO]-Schicht [2] mit derselben Schnittstelle wie die [DAO]-Schicht [4] erstellt. Um die [DAO]-Schicht [2] zu testen, können wir daher den JUnit-Test verwenden, der bereits zum Testen der [DAO]-Schicht [4] verwendet wurde. Zur Erinnerung: Er sieht wie folgt aus:

  

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 {

    // layer [DAO]
    @Autowired
    private IDao dao;
 
    // filters 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() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // fill the 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);
        }
        // add the category - the products will be cascaded in as well
        categories = dao.addCategories(categories);
    }
 
    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // list of categories
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // product list
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // a few checks
        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);
        // we insert
        Categorie categorie = new Categorie("cat1");
        categorie.addProduit(new Produit("x", 1.0, ""));
        categorie.addProduit(new Produit("x", 1.0, ""));
        // add the category - the products will be cascaded in as well
        try {
            categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
        } catch (DaoException e) {
            show("Les erreurs suivantes se sont produites :", e.getErreurs());
        }
        // checks
        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);
        // checks
        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);
        // check
        affiche(produits, jsonMapperProduitWithoutCategorie);
    }
 
    // -------------- private methods
    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;
    }
 
    // display of a T-type element
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }
 
    // display a list of elements of 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) {
        // poster 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) {
        // title
        System.out.println(String.format("%s : ", title));
        // messages
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }
 
}

Der Befehl wird erfolgreich ausgeführt und gibt auf der Konsole folgende Ergebnisse aus:


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