Skip to content

13. [Cours]: Eine Datenbank mit Spring im Web bereitstellen MVC

Stichworte: mehrschichtige Architektur, Spring, Abhängigkeitsinjektion, Webdienst / jSON, Client/Server

13.1. Support

 

Die Projekte dieses Kapitels befinden sich im Ordner [support / chap-13]. Mit dem Skript SQL [dbintrospringdata.sql] lässt sich die für die Tests erforderliche Basis MySQL erstellen.

13.2. Die Rolle von Spring MVC in einer Webanwendung

Betrachten wir die Rolle von Spring MVC bei der Entwicklung einer Webanwendung. Meistens basiert diese auf einer mehrschichtigen Architektur wie der folgenden:

  • Die Schicht [Web] ist die Schicht, die mit dem Benutzer der Webanwendung in Kontakt steht. Dieser interagiert mit der Webanwendung über Webseiten, die in einem Browser angezeigt werden. In dieser Schicht befindet sich Spring MVC und ausschließlich in dieser Schicht;
  • Die Schicht [métier] implementiert die Geschäftsregeln der Anwendung, wie beispielsweise die Berechnung eines Gehalts oder einer Rechnung. Diese Schicht nutzt Daten, die vom Benutzer über die Schicht [Web] und von SGBD über die Schicht [DAO] bereitgestellt werden;
  • die Schicht [DAO] (Data Access Objects), die Schicht [ORM] (Object Relational Mapper) und der Treiber JDBC verwalten den Zugriff auf die Daten der Schicht SGBD. Die Schicht [ORM] bildet eine Brücke zwischen den von der Schicht [DAO] verarbeiteten Objekten und den Zeilen und Spalten der Tabellen einer relationalen Datenbank. Die Spezifikation JPA (Java Persistence API) ermöglicht es, vom verwendeten ORM abstrahieren, sofern dieser die Spezifikationen implementiert. Dies ist hier der Fall, und wir werden die Schicht ORM fortan als Schicht JPA bezeichnen;
  • die Integration der Schichten erfolgt durch das Spring-Framework;

13.3. Das Entwicklungsmodell von Spring MVC

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

Die Bearbeitung einer Client-Anfrage läuft wie folgt ab:

  1. Anfrage – die angeforderten URL haben die Form http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&.... Der [Front Controller] verwendet eine Konfigurationsdatei oder Java-Annotationen, um die Anfrage an den richtigen Controller und die richtige Aktion innerhalb dieses Controllers weiterzuleiten. Dazu nutzt er das Feld [Action] des URL. Der Rest des URL [/param1/param2/...] besteht aus optionalen Parametern, die an die Aktion übergeben werden. Das C von MVC ist hier die Zeichenkette [Front Controller, Contrôleur, 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 parami nutzen, die ihr von [Front Controller] übergeben wurden. Diese können aus verschiedenen Quellen stammen:
    • aus dem Pfad [/param1/param2/...] des URL,
    • aus den Parametern [p1=v1&p2=v2] von URL,
    • aus Parametern, die der Browser mit seiner Anfrage übermittelt hat;
    • Bei der Bearbeitung der Benutzeranfrage benötigt die Aktion möglicherweise die Schichten [métier] und [2b]. Sobald die Client-Anfrage bearbeitet wurde, kann sie verschiedene Antworten auslösen. Ein klassisches Beispiel ist:
    • eine Fehlerseite, wenn die Anfrage nicht korrekt verarbeitet werden konnte
    • ansonsten eine Bestätigungsseite
    • die Aktion fordert die Anzeige einer bestimmten Ansicht an: [3]. Diese Ansicht zeigt Daten an, die als Modell der Ansicht bezeichnet werden. Das ist das M in MVC. Die Aktion erstellt dieses Modell M [2c] und fordert eine Ansicht V auf, sich anzuzeigen [3];
  3. Antwort – die ausgewählte Ansicht V verwendet die von der Aktion erstellte Vorlage M, um die dynamischen Teile der Antwort HTML zu initialisieren, die sie an den Client senden muss, und sendet diese Antwort anschließend.

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 Bibliothek jSON in die Zeichenkette jSON umgewandelt;
  • in [4b] wird diese Zeichenkette jSON an den Browser gesendet;

Lassen Sie uns nun den Zusammenhang zwischen der Webarchitektur MVC und der Schichtenarchitektur näher erläutern. Je nachdem, wie man das Modell definiert, stehen diese beiden Konzepte in einem Zusammenhang oder auch nicht. Nehmen wir eine einschichtige Spring-Webanwendung MVC als Beispiel:

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

Betrachten wir nun eine mehrschichtige Webarchitektur:

Die Schicht [Web] kann ohne Framework und ohne Befolgung des Modells MVC implementiert werden. Man hat dann zwar eine mehrschichtige Architektur, aber die Webschicht implementiert das Modell MVC nicht.

Beispielsweise kann in der Welt .NET die oben genannte Schicht [Web]oben mit ASP.NET und MVC implementiert werden, und man erhält somit eine Schichtenarchitektur mit einer Schicht [Web] vom Typ MVC. Ist dies geschehen, kann man diese Schicht ASP.NET MVC durch eine klassische Schicht ASP.NET (WebForms) ersetzen, während der Rest (Geschäftslogik, DAO, ORM) unverändert beibehalten. Man erhält somit eine Schichtenarchitektur mit einer Schicht [Web], die nicht mehr vom Typ MVC ist.

In MVC haben wir festgelegt, dass das Modell M das der Ansicht V, c.a.d, ist. Die Gesamtheit der von der Ansicht V angezeigten Daten. Eine weitere Definition des Modells M von MVC lautet:

Viele Autoren sind der Ansicht, dass das, was sich rechts von der Ebene [Web] befindet, das Modell M von MVC bildet. Um Mehrdeutigkeiten zu vermeiden, kann man sprechen von:

  • vom Domänenmodell, wenn man alles bezeichnet, was rechts von der Schicht [Web] liegt
  • vom Modell der Ansicht, wenn man die von einer Ansicht V angezeigten Daten bezeichnet

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

13.4. Ein Webprojekt / jSON mit Spring MVC

Die Website [http://spring.io/guides] bietet Einführungs-Tutorials, um das Spring-Ökosystem kennenzulernen. Wir werden einem davon folgen, um die für ein Spring-Projekt MVC erforderliche Maven-Konfiguration kennenzulernen.

13.4.1. Das Demonstrationsprojekt

  • in [1] importieren wir eine der Spring-Anleitungen;
  • 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 der Anleitung aus;
  • in [5] bestätigen wir;
  • in [6] das importierte Projekt;

Webdienste, die über Standard-URL erreichbar sind und Text im Format jSON liefern, werden oft als REST-Dienste (REpresentational State Transfer) bezeichnet. Ein Dienst wird als RESTful bezeichnet, wenn er bestimmte Regeln einhält.

Sehen wir uns nun das importierte Projekt an, zunächst seine 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 Eigenschaften des Maven-Projekts. Es fehlt ein Tag [<packaging>], das den Typ der durch die Maven-Kompilierung erzeugten Datei angibt. Fehlt dieses Tag, wird der Typ [jar] verwendet. Die Anwendung ist also eine ausführbare Konsolenanwendung und keine Webanwendung, bei der das Packaging dann [war] lauten würde;
  • Zeilen 10–14: Das Maven-Projekt hat ein übergeordnetes Projekt mit dem Typ [spring-boot-starter-parent]. Dieses definiert den Großteil der Projektabhängigkeiten. Entweder sind diese ausreichend – in diesem Fall werden keine weiteren hinzugefügt – oder nicht, in welchem Fall die fehlenden Abhängigkeiten hinzugefügt werden;
  • Zeilen 17–20: Das Artefakt [spring-boot-starter-web] enthält die Bibliotheken, die für ein Spring-Projekt MVC vom Typ Webservice erforderlich sind, bei dem keine Ansichten generiert werden. Dieses Artefakt enthält eine sehr große Anzahl von Bibliotheken, darunter auch die eines eingebetteten Tomcat-Servers. Auf diesem Server wird die Anwendung ausgeführt;

Die von dieser Konfiguration bereitgestellten Bibliotheken sind sehr zahlreich:

Oben sind die drei Archive des Tomcat-Servers zu sehen.

13.4.3. Die Architektur eines Spring-Dienstes [web / jSON]

Für einen Webdienst / jSON implementiert Spring MVC das Modell MVC wie folgt:

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

13.4.4. Der Controller C

  

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, d. h., ihre Methoden werden zur Verarbeitung von URL registriert. Wir haben bereits die ähnliche Annotation [@Controller] gesehen. Das Ergebnis der Methoden dieses Controllers war ein Typ [String], der den Namen der anzuzeigenden Ansicht darstellte. Hier ist das anders. Die Methoden eines Controllers vom Typ [@RestController] geben Objekte zurück, die serialisiert werden, um an den Browser gesendet zu werden. Der Typ der durchgeführten Serialisierung hängt von der Spring-Konfiguration MVC ab. Hier werden sie in jSON serialisiert. Das Vorhandensein einer Bibliothek jSON in den Projektabhängigkeiten führt dazu, dass Spring Boot das Projekt durch Autokonfiguration auf diese Weise einrichtet;
  • Zeile 14: Die Annotation [@RequestMapping] gibt den URL an, den die Methode verarbeitet, in diesem Fall den 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 atomare Long-Ganzzahl. Das bedeutet, dass sie parallelen Zugriff unterstützt. Mehrere Threads können gleichzeitig versuchen, die Variable [counter] zu inkrementieren. Dies erfolgt korrekt. 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 Modell M

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

  

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 Transformation jSON dieses Objekts erzeugt die Zeichenkette {"id":n,"content":"text"}. Letztendlich hat die von der Controller-Methode erzeugte Zeichenkette jSON 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);
    }

}

Diesen Code haben wir bereits im vorherigen Beispiel kennengelernt und erläutert.

13.4.7. Ausführung des Projekts

Führen wir das Projekt aus:

 

Es werden die folgenden Konsolenprotokolle ausgegeben:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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/**] auf den Handler vom Typ [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 vom Typ [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 erkannt;

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

 

Man erhält tatsächlich die erwartete Zeichenfolge jSON. Es kann interessant sein, sich die vom Server gesendeten Header HTTP anzusehen. Dazu verwenden wir die Chrome-Erweiterung namens [Advanced Rest Client] (Chrome / Strg-T / Menü [Applications] / [Advanced Rest Client] – siehe Anhang, Abschnitt 22.5):

  • in [1], das angeforderte URL;
  • in [2] wird die Methode GET verwendet;
  • in [3] die Antwort jSON;
  • bei [4] hat der Server angegeben, dass er eine Antwort im Format jSON sendet;
  • in [5] wird dieselbe URL angefordert, diesmal jedoch mit einer POST;
  • In [7] werden die Informationen in der Form [urlencoded] an den Server gesendet;
  • in [6] den Parameter „name“ mit seinem Wert;
  • bei [8] teilt der Browser dem Server mit, dass er ihm Informationen in der Form [urlencoded] sendet;
  • in [9] die Antwort jSON des Servers;

13.4.8. Erstellung eines ausführbaren Archivs

Wir erstellen nun ein ausführbares Archiv:

  • in [1]: Wir führen ein Maven-Ziel aus;
  • in [2]: Es gibt zwei Ziele (Goals): [clean] zum Löschen des Ordners [target] aus dem Maven-Projekt und [package] zum erneuten Erstellen dieses Ordners;
  • in [3]: Der generierte Ordner [target] wird in diesem Ordner erstellt;
  • in [4]: Das Ziel wird generiert;

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

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

Öffnen Sie in einer Konsole den erstellten 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 generierte 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 gestartet ist, kann man sie mit einem Browser aufrufen:

 

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: Es muss angegeben werden, dass ein WAR-Archiv (Web ARchive) generiert werden soll;

Außerdem muss die Webanwendung konfiguriert werden. Da die Datei [web.xml] fehlt, erfolgt dies über eine Klasse, die von [SpringBootServletInitializer] erbt:

  

Die Klasse [ApplicationInitializer] lautet wie folgt:


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 neu definiert (Zeile 8);
  • Zeile 10: Hier wird die Klasse angegeben, die das Projekt konfiguriert;

Um das Projekt auszuführen, kann man wie folgt vorgehen:

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

Anschließend kann man die Seite URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] in einem Browser aufrufen:

 

13.4.10. Fazit

Wir haben eine Art von Spring-Projekten vorgestellt, bei denen die Webanwendung einen Datenstrom an den Browser sendet. Wir werden nun eine Webanwendung / jSON entwickeln, um die im Tutorial [Introduction à Spring Data] behandelte Datenbank [dbintrospringdata] im Web bereitzustellen.

13.5. Die Datenbank [dbintrospringdata] im Web bereitstellen

13.5.1. Architektur des Webdienstes / jSON

Wir werden die folgende Architektur einrichten:

Die Schichten [DAO] und [JPA] werden durch die im Tutorial [Introduction à Spring Data] beschriebene Anwendung implementiert.

13.5.2. Installation der Datenbank

  

Mit dem Skript SQL [dbintrospringdata.sql] lässt sich die für die Tests erforderliche Datenbank MySQL anlegen.

13.5.3. Das Eclipse-Projekt des Webdienstes / jSON

Das Eclipse-Projekt des Webdienstes / jSON sieht wie folgt aus:

  

Es handelt sich um ein Maven-Projekt, dessen Datei [pom.xml] wie folgt lautet:


<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 Schicht [DAO] verwendet wurde;
  • Zeilen 18–22: die Abhängigkeit von der Schicht [DAO];
  • Zeilen 23–26: die Abhängigkeit vom Artefakt [spring-boot-starter-web]. Dieses Artefakt enthält alle Abhängigkeiten, die für die Erstellung eines Webdienstes / jSON erforderlich sind. Es enthält jedoch auch unnötige Bibliotheken. Eine genauere Konfiguration wäre daher erforderlich. Für den Einstieg ist diese Konfiguration jedoch praktisch;
  • Zeilen 28–30: Die Abhängigkeit vom Artefakt [spring-boot-starter] ermöglicht die Verwaltung der Spring-Boot-Annotationen;

Die durch diese Konfiguration mitgelieferten Abhängigkeiten sind folgende:

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

Die oben genannten Abhängigkeiten gelten sowohl für die Ebene [DAO] als auch für die Ebene [web].

13.5.3.1. Konfiguration der Ebene [web]

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

  

Die Klasse [WebConfig] konfiguriert die Schicht [web]:


package spring.webjson.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {

    // -------------------------------- Konfiguration der Ebene [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);
    }

    // Filter jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // Mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // Filter
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // Ergebnis
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // Mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // Filter
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // Ergebnis
        return mapper;
    }

    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // Mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // Filter
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // Ergebnis
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // Mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // Filter
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // Ergebnis
        return mapper;
    }
}
  • Zeile 18: Die Annotation [@EnableWebMvc] führt zu automatischen Konfigurationen für das Spring-Framework MVC;
  • Zeile 19: Die Klasse [WebConfig] erweitert die Spring-Klasse [WebMvcConfigurerAdapter], um bestimmte Beans neu zu definieren (Zeilen 26–40);
  • Zeilen 22–23: Injektion des Spring-Kontexts;
  • Zeilen 25–29: Definition des Servlets des Spring-Frameworks MVC, das die Anfragen HTTP an den richtigen Controller und die richtige Methode weiterleitet. [DispatcherServlet] ist eine Spring-Klasse;
  • Zeilen 31–34: Hier wird festgelegt, dass dieses Servlet alle URL-Anfragen verarbeitet;
  • Zeilen 36–39: Das Vorhandensein dieser Bean aktiviert den Tomcat-Server, der sich im Projektarchiv befindet. Er wartet auf Anfragen am Port 8080;
  • Zeilen 42–91: Beans, die zur Verwaltung von Filtern jSON verwendet werden;
  • Zeilen 42–45: Ein Mapper jSON ohne Filter;
  • Zeilen 47–57: der Mapper jSON, der es ermöglicht, eine Kategorie mit ihren Produkten anzuzeigen. Es ist zu beachten, dass bei der Abfrage einer Kategorie mit ihren Produkten sowohl der Filter jSON der Klasse [Categorie] als auch der der Klasse [Produit] konfiguriert werden müssen. Dies ist immer der Fall. Wenn eine Klasse in jSON serialisiert/deserialisiert wird, müssen der Filter jSON der Klasse sowie die Filter aller darin einzubeziehenden Abhängigkeiten konfiguriert werden;
  • Zeilen 59–69: der Mapper jSON, der es ermöglicht, ein Produkt mit seiner Kategorie darzustellen;
  • Zeilen 71–80: Der Mapper jSON, der es ermöglicht, eine Kategorie ohne ihre Produkte anzuzeigen;
  • Zeilen 82–91: Der Mapper jSON, der es ermöglicht, ein Produkt ohne seine Kategorie anzuzeigen;

Die Klasse [AppConfig] konfiguriert die gesamte Anwendung, d. h. die Ebenen [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: Hier werden die Beans der Schicht [DAO] und der Schicht [web] importiert;
  • Zeile 8: gibt an, in welchen Paketen weitere Spring-Beans zu finden sind;

Es ist zu beachten, dass wir die Annotation [@EnableAutoConfiguration] an keiner Stelle 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 {

    // die Ebene [DAO]
    @Autowired
    private IDao dao;

    @Override
    public void addProduits(List<Produit> produits) {
        dao.addProduits(produits);
    }

    @Override
    public void deleteAllProduits() {
        dao.deleteAllProduits();
    }

    @Override
    public void updateProduits(List<Produit> produits) {
        dao.updateProduits(produits);
    }

    @Override
    public List<Produit> getAllProduits() {
        return dao.getAllProduits();
    }

    @Override
    public void addCategories(List<Categorie> categories) {
        dao.addCategories(categories);
    }

    @Override
    public void deleteAllCategories() {
        dao.deleteAllCategories();
    }

    @Override
    public void updateCategories(List<Categorie> categories) {
        dao.updateCategories(categories);
    }

    @Override
    public List<Categorie> getAllCategories() {
        return dao.getAllCategories();
    }

    @Override
    public Produit getProduitByIdWithCategorie(Long idProduit) {
        return dao.getProduitByIdWithCategorie(idProduit);
    }

    @Override
    public Produit getProduitByNameWithCategorie(String nom) {
        return dao.getProduitByNameWithCategorie(nom);
    }

    @Override
    public Categorie getCategorieByIdWithProduits(Long idCategorie) {
        return dao.getCategorieByIdWithProduits(idCategorie);
    }

    @Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        return dao.getCategorieByNameWithProduits(nom);
    }

    @Override
    public Produit getProduitByIdWithoutCategorie(Long idProduit) {
        return dao.getProduitByIdWithoutCategorie(idProduit);
    }

    @Override
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
        return dao.getCategorieByIdWithoutProduits(idCategorie);
    }

    @Override
    public Produit getProduitByNameWithoutCategorie(String nom) {        
        return dao.getProduitByNameWithoutCategorie(nom);
    }

    @Override
    public Categorie getCategorieByNameWithoutProduits(String nom) {
        return dao.getCategorieByNameWithoutProduits(nom);
    }

}
  • Zeile 12: Die Klasse ist ein Spring-Singleton;
  • Zeile 13: Sie implementiert die Schnittstelle [IDao] der Schicht [DAO];
  • Zeilen 16–17: Injektion einer Referenz auf die Schicht [DAO];
  • Zeilen 19–99: Implementierung der Schnittstelle [IDao];

Die Architektur der Webschicht entwickelt sich wie folgt:

  • in [2b] kommunizieren die Methoden des oder der Controller mit dem Singleton [ApplicationModel];

Diese Strategie bietet Flexibilität bei der Verwaltung eines möglichen Caches. Die Klasse [ApplicationModel] kann dazu dienen, Informationen zu speichern, die von der Schicht [DAO] abgerufen wurden, oder auch Konfigurationsdaten. Dies kann nützlich sein, wenn man keine Kontrolle über die Schicht [DAO] hat. Diese Cache-Strategie kann sich im Laufe der Zeit weiterentwickeln. Die Änderungen haben keinerlei Auswirkungen auf den Code des oder der Controller.

13.5.5. Der Controller

  

Wir haben hier nur einen Controller, die Klasse [MyController].

13.5.5.1. Die von diesem Controller bereitgestellten

Die von diesem Controller bereitgestellten URL sind folgende:


    @RequestMapping(value = "/addProduits",
method = RequestMethod.POST,
consumes = "application/json; charset=UTF-8")
    public String addProduits(HttpServletRequest request) {
...
    }
Fügt Produkte zur Datenbank hinzu. Diese werden gepostet. Die Antwort ist die Zeichenfolge jSON, die Liste der hinzugefügten Produkte mit ihren Primärschlüsseln.

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

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

Aktualisiert Produkte in der Datenbank. Diese werden veröffentlicht. Die Antwort ist die Zeichenfolge jSON aus der Liste der aktualisierten Produkte.

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

Ermittelt die Zeichenfolge jSON aus allen Produkten.

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

Fügt Kategorien in die Datenbank ein. Diese werden veröffentlicht. Die Antwort ist die Zeichenfolge jSON aus der Liste der hinzugefügten Kategorien mit ihren Primärschlüsseln. Wenn die Kategorien Produkte enthalten, werden diese ebenfalls in die Datenbank eingefü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,
consumes = "application/json; charset=UTF-8")
    public String updateCategories
(HttpServletRequest request) {
...
    }

Aktualisiert Kategorien in der Datenbank. Diese werden übermittelt. Die Antwort ist die Liste der aktualisierten Kategorien. Wenn die Kategorien Produkte enthalten, werden diese ebenfalls in der Datenbank aktualisiert. Gibt die Zeichenfolge jSON der geänderten Kategorien zurück;

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

Ermittelt die Zeichenfolge jSON aus allen Kategorien.

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

Ruft die Zeichenfolge jSON für ein Produkt ab, das durch seine ID bezeichnet wird, einschließlich seiner Kategorie.

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

Ruft die Zeichenfolge jSON für ein Produkt ab, das durch seine ID bezeichnet wird, ohne dessen Kategorie.

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

Ruft die Zeichenfolge jSON für ein Produkt ab, das durch seinen Namen und seine Kategorie bezeichnet wird.

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

Ruft die Zeichenfolge jSON für ein Produkt ab, das durch seinen Namen bezeichnet wird, ohne dessen Kategorie.

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

Ruft die Zeichenfolge jSON für eine durch ihre ID angegebene Kategorie mit den dazugehörigen Produkten ab.

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

Ruft die Zeichenfolge jSON für eine durch ihren Namen bezeichnete Kategorie samt deren Produkten ab.

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

Ruft die Zeichenfolge jSON für eine durch ihren Namen bezeichnete Kategorie ab, ohne deren Produkte.

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

Ruft die Zeichenfolge jSON einer durch ihre ID bezeichneten Kategorie ohne deren Produkte ab.

Die aufgeführten URL entsprechen den Methoden der Schnittstelle [IDao] der Schicht [DAO]. Die Methoden des Webdienstes /jSON basieren alle auf demselben Muster. Wir werden uns einige davon genauer ansehen.

13.5.5.2. Das Grundgerüst des Controllers

Das Grundgerüst des Controllers 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-Abhängigkeiten
    @Autowired
    private ApplicationModel application;

    // Filter 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;

    // Die Klasse [MyController] ist ein Singleton und wird erst nach der Bean instanziiert
    
    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: Injektion einer Referenz auf die Klasse [ApplicationModel];
  • Zeilen 36–50: Injektion von Referenzen auf die Mapper jSON;
  • Zeile 58: Die exponierte URL ist [/addProduits]. Der Client muss eine Methode [POST] verwenden, um seine Anfrage zu stellen (method = RequestMethod.POST). Er muss den über POST übermittelten Wert in Form einer Zeichenkette jSON senden (consumes = "application/json; charset=UTF-8"). Die Methode selbst sendet die Antwort an den Client zurück (Zeile 59). Dabei handelt es sich um eine Zeichenkette (Zeile 60). Der Header HTTP [Content-type : application/json; charset=UTF-8] wird an den Client gesendet, um ihm mitzuteilen, dass er eine Zeichenkette jSON erhalten wird (Zeile 58);
  • Zeile 60: Die Methode [addProduits] gibt die Zeichenkette jSON aus der Liste der in die Datenbank aufgenommenen Produkte zurück;

13.5.5.3. Die Antwort der Controller-Methoden

Alle Methoden des Controllers geben die folgende Antwort vom Typ [Response] zurück:

  

package spring.webjson.service;

import java.util.List;

public class Response<T> {

    // ----------------- Eigenschaften
    // Status der Operation
    private int status;
    // etwaige Fehlermeldungen
    private List<String> messages;
    // der Antworttext
    private T body;

    // Konstruktoren
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // Getter und Setter
    ...
}
  • Zeile 5: Die Antwort kapselt einen Typ T;
  • Zeile 13: Die Antwort vom Typ T;
  • Zeilen 9–11: Es ist möglich, dass eine Methode auf eine Ausnahme stößt. In diesem Fall gibt sie eine Antwort mit folgenden Merkmalen zurück:
    • Zeile 9: status!=0;
    • Zeile 11: die Liste der aufgetretenen Fehler;

13.5.5.4. L'URL [/addProduits]

L'URL [/addProduits] 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 {
        // Antwort
        Response<List<Produit>> response;
        try {
            // Der übermittelte Wert wird abgerufen
            String body = CharStreams.toString(request.getReader());
            List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
            });
            // Die Verbindung zwischen Produkten und Kategorien wird wiederhergestellt
            for (Produit produit : produits) {
                produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
            }
            // Die Produkte werden gespeichert
            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);
        }
        // Antwort jSON
        return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
    }
  • Zeile 3: Die Methode akzeptiert als Parameter [HttpServletRequest request], das alle Informationen zur Anfrage des Kunden enthält;
  • Zeile 5: Die Antwort, die an den Kunden gesendet wird: eine Produktliste;
  • Zeile 8: Der über POST übermittelte Wert wird abgerufen. Die Klasse [CharStreams] gehört zur Bibliothek [Google Guava], deren Referenz in der Datei [pom.xml] hinzugefügt wurde. Man erhält die vom Kunden gesendete Zeichenkette jSON. Diese muss deserialisiert werden, um sie weiterverarbeiten zu können;
  • Zeilen 8–10: Die Deserialisierung erfolgt. Man erhält eine Produktliste, in der jedes Produkt ein Feld [categorie=null] enthält;
  • Zeilen 12–14: Das Feld „[categorie]“ aller Produkte in der Liste wird zurückgesetzt. Dazu wird das Feld „[idCategorie]“ des Produkts verwendet, das bereits initialisiert ist;
  • Zeile 16: Die Produkte werden in die Datenbank eingefügt;
  • Zeile 17: Das Objekt [response] wird mit der Produktliste initialisiert;
  • Zeilen 18–19: Fall, in dem die Methode auf eine Ausnahme der Schicht [DAO] stößt. Die Antwort wird mit [status=1000] (Fehlercode) [messages=e1.getMessages()] initialisiert, d. h., es wird die Liste der serverseitig aufgetretenen Fehler an den Client übermittelt;
  • Zeilen 20–21: Fall, in dem die Methode auf eine andere Art von Ausnahme stößt. Die Antwort wird mit [status=1000] (Fehlercode) [messages=getErreursForException(e)], wobei [getErreursForException] eine private Methode der Klasse ist, die die Liste der Fehler zurückgibt, die mit den Ausnahmen im Ausnahmestapel von e verbunden sind, sowie [body=null];
  • Zeile 24: Die Zeichenkette jSON wird in der Antwort zurückgegeben;

13.5.5.5. URL [/getAllProduits]

URL [/getAllProduits] wird mit der folgenden Methode verarbeitet:


    @RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String getAllProduits() throws JsonProcessingException {
        // Antwort
        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);
        }
        // Antwort jSON
        return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
  • Zeile 1: URL [/getAllProduits] wird mit einer Operation [GET] angefordert. Sie erzeugt jSON;
  • Zeile 2: Die Methode sendet die Antwort jSON selbst an den Client;
  • Zeile 5: Die Methode sendet die Zeichenfolge jSON vom Typ [Response<List<Produit>>];
  • Zeile 7: Die Produkte werden ohne ihre Kategorie abgefragt;
  • Zeilen 8–12: Im Fehlerfall wird die Antwort mit einem Code und Fehlermeldungen initialisiert;
  • Zeile 14: Die Antwort „jSON“ wird an den Client gesendet;

13.5.5.6. Conclusion

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

13.5.6. Die Ausführungsklasse des Webdienstes / jSON

  

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 des Projekts [Spring Boot] (Zeile 3). Ihr werden zwei Parameter übergeben:
    • [AppConfig.class]: die Klasse, die die gesamte Anwendung konfiguriert;
    • [args]: eventuelle Argumente, die an die Methode [main] in Zeile 9 übergeben wurden. Dieser Parameter wird hier nicht verwendet;

Wenn man diese Klasse ausführt, erhält man die folgenden Protokolle:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: 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/**] auf den Handler vom Typ [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 [/**] auf den Handler vom Typ [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: Start des Tomcat-Servers, der den Webdienst /jSON ausführen wird;
  • Zeilen 25–33: Aufbau der Schicht [DAO];
  • Zeilen 32–51: Die exponierten URL werden erkannt;

13.5.7. Tests des Webdienstes / jSON

Um die Tests durchzuführen, generieren wir die Datenbank MySQL [dbintrospringdata] anhand des Skripts SQL [dbintrospringdata.sql]:

  

Anschließend verwenden wir den Client [Advanced Rest Client] (siehe Abschnitt 22.5), um die vom Webservice / jSON bereitgestellten URL abzufragen bereitgestellt werden (der Webdienst / jSON muss gestartet sein).

  • In [1-3] fordern wir die URL und [/getAllCategories] über einen Befehl HTTP und GET an;

Wir erhalten folgende Antwort:

  • in [1] die Anfrage HTTP des Clients;
  • in [2], die Antwort HTTP des Servers;
  • in [3] gibt der Status [200 OK] an, dass der Server die Anfrage korrekt verarbeitet hat;
  • in [4] die Antwort jSON des Servers;

Die vollständige Antwort jSON 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 serverseitig keine Fehler aufgetreten sind;
  • messages: null bedeutet, dass keine Fehlermeldungen vorliegen;
  • body: ist der Hauptteil der Antwort, hier die Liste der Kategorien mit ihren Produkten. Es gibt zwei Kategorien mit jeweils 5 Produkten;

Wir werden der Kategorie [categorie1] das Produkt [produit15] hinzufügen. Dazu verwenden wir URL [/addCategories], das den folgenden Code hat:


@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);
        // werden die Kategorien beibehalten
        try {
            // der gespeicherte Wert wird abgerufen
            String body = CharStreams.toString(request.getReader());
            mapper.setFilters(jsonFilterCategorieWithProduits);
            List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
            });
            // Die Verknüpfung zwischen Produkten und Kategorien wird wiederhergestellt
            for (Categorie categorie : categories) {
                Set<Produit> produits = categorie.getProduits();
                if (produits != null) {
                    for (Produit produit : categorie.getProduits()) {
                        produit.setCategorie(categorie);
                    }
                }
            }
            // Die Kategorien werden gespeichert
            application.addCategories(categories);
            response = new Response<List<Categorie>>(0, null, categories);
        } catch (Exception e) {
            response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
        }
        // Antwort jSON
        return mapper.writeValueAsString(response);
    }
  • Zeile 1: Der Kunde muss ein POST erstellen, und der übermittelte Wert muss eine Zeichenkette jSON sein;
  • Zeilen 9–12: Der übermittelte Wert muss eine Liste von Kategorien mit den zugehörigen Produkten sein;

Wir erstellen eine Kategorie [categorie2] mit einem Produkt [produit21]. Die zu sendende Zeichenfolge jSON 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 Webdienst /jSON erfolgt wie folgt:

  • in [1], das angeforderte URL;
  • in [2] wird sie über eine Operation POST angefordert;
  • in [3] wird die Zeichenfolge jSON gesendet;
  • in [4] wird dem Server mitgeteilt, dass man ihm jSON senden wird;

Die Antwort des Servers lautet wie folgt:

  • In [1] sieht man, dass sowohl die Kategorie als auch das Produkt nun einen Primärschlüssel haben, was darauf hindeutet, dass sie wahrscheinlich in die Datenbank eingefügt wurden. Wir werden dies anhand von URL und [/getCategorieByNameWithProduits/categorie2] überprüfen:

Wir erhalten folgendes Ergebnis:

Wir haben tatsächlich die Kategorie [categorie2] mit ihrem einzigen Produkt [produit21] erhalten. Man kann auch nur nach dem Produkt fragen. Verwenden wir dazu die URL [/getProduitByIdWithoutCategorie/1859]:

Wir erhalten folgendes Ergebnis:

Alle Vorgänge mit [GET] können in einem einfachen Browser durchgeführt werden:

 

Der Leser ist eingeladen, die anderen URL des Webdienstes / JSON zu testen.

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

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

Die Client-Anwendung wird zwei Schichten haben:

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

13.6.1. Das Eclipse-Projekt

Das Eclipse-Projekt des Clients sieht wie folgt aus:

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

13.6.2. Maven-Konfiguration des Projekts

Das Projekt ist ein Maven-Projekt, das durch die folgende Datei [pom.xml] 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>
        <!-- Von Spring verwendete Bibliothek jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- von Spring verwendete Komponente 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>
        <!-- Protokollbibliothek -->
        <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], mit dem wir eine Reihe von Abhängigkeiten ohne Angabe ihrer Versionen definieren können, da diese im übergeordneten Projekt festgelegt sind;
  • Zeilen 22–25: Obwohl wir keine Webanwendung schreiben, benötigen wir die Abhängigkeit [spring-web], die die Klasse [RestTemplate] mit sich bringt, welche eine einfache Anbindung an eine Webanwendung / jSON ermöglicht;
  • Zeilen 27–34: eine Bibliothek jSON;
  • Zeilen 36–39: eine Abhängigkeit, die es uns ermöglicht, den Client-Anfragen vom Typ HTTP ein timeout zuzuweisen. Ein timeout ist eine maximale Wartezeit auf die Antwort des Servers. Nach Ablauf dieser Zeit meldet der Client einen Fehler vom Typ timeout, indem er eine Ausnahme auslöst;
  • Zeilen 41–46: Die im Test verwendete Google Guava-Bibliothek JUnit. Aus diesem Grund haben wir ihren Geltungsbereich auf [test] festgelegt (Zeile 45). Das bedeutet, dass diese Abhängigkeit nur bei der Ausführung von Code aus dem Zweig [src/test/java] einbezogen wird;
  • Zeilen 48–51: die Logging-Bibliothek;
  • Zeilen 52–63: die Abhängigkeit für die Tests JUnit. Sie enthält insbesondere die für die Tests erforderliche Bibliothek JUnit 4. Diese Abhängigkeiten tragen das Attribut [<scope>test</scope>], das angibt, dass sie nur für die Testphase benötigt werden. Sie werden nicht in das endgültige Projektarchiv aufgenommen;

13.6.3. Implementierung der Schicht [DAO]

  
  • Das Paket [spring.client.config] enthält die Spring-Konfiguration der Schicht [DAO];
  • Das Paket [spring.client.dao] enthält die Implementierung der Schicht [DAO];
  • Das Paket [spring.client.entities] enthält die mit dem Webdienst / jSON ausgetauschten Objekte;

13.6.3.1. Configuration

  

Die Klasse [DaoConfig] übernimmt die Spring-Konfiguration der Schicht [DAO]. Ihr 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 {

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

    @Bean
    public RestTemplate restTemplate(int timeout) {
        // Erstellung der Komponente RestTemplate
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        RestTemplate restTemplate = new RestTemplate(factory);
        // Zeitlimit für den Datenaustausch
        factory.setConnectTimeout(timeout);
        factory.setReadTimeout(timeout);
        // Ergebnis
        return restTemplate;
    }

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

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

    // Filter jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // Mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // Filter
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // Ergebnis
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // Mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // Filter
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // Ergebnis
        return mapper;
    }

    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // Mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // Filter
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // Ergebnis
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // Mapper jSON
        ObjectMapper mapper = new ObjectMapper();
        // Filter
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // Ergebnis
        return mapper;
    }
}
  • Zeile 13: Die Klasse ist eine Spring-Konfigurationsklasse – Spring-Komponenten sind im Paket [spring.client.dao] zu finden;
  • Zeile 17: Es wird ein timeout von einer Sekunde (1000 ms) festgelegt;
  • Zeilen 32–35: Das Bean, das diesen Wert zurückgibt;
  • Zeile 18: der URL des Webdienstes / jSON;
  • Zeilen 37–40: Die Bean, die diesen Wert zurückgibt;
  • Zeilen 20–30: die Konfiguration der Klasse [RestTemplate], die den Datenaustausch mit dem Webdienst / jSON gewährleistet. Wenn keine Konfiguration erforderlich ist, kann sie im Code einfach durch [new RestTemplate()] angegeben werden. Hier möchten wir den timeout für den Datenaustausch mit dem Webservice / jSON festlegen. Die Bean [timeout] aus Zeile 36 wird als Parameter an die Methode [restTemplate] aus Zeile 24 übergeben;
  • Zeile 23: Die Komponente [HttpComponentsClientHttpRequestFactory] ermöglicht es uns, den timeout für den Datenaustausch festzulegen (Zeilen 29–30);
  • Zeile 24: Die Klasse [RestTemplate] wird mit dieser Komponente erstellt. Da sie sich auf diese stützt, um mit dem Webdienst /jSON zu kommunizieren, unterliegen die Datenaustausche tatsächlich der timeout;
  • Client und Server tauschen Textzeilen aus. Ein Konverter ist dafür zuständig, ein Objekt in Text zu serialisieren und umgekehrt einen Text in ein Objekt zu deserialisieren. Der Klasse [RestTemplate] können mehrere Konverter zugeordnet sein, und welcher zu einem bestimmten Zeitpunkt ausgewählt wird, hängt von den vom Server gesendeten Headern HTTP ab. In diesem Fall wird kein Konverter verwendet. Daher wird die Komponente [RestTemplate] nicht versuchen, die beiden folgenden Elemente in irgendeiner Weise zu konvertieren:
    • den geposteten Text;
    • den als Antwort empfangenen Text;

Diese Texte sind jSON-Zeichenketten, die daher von der Komponente [RestTemplate] unverändert belassen werden. Wir als Entwickler werden die erforderlichen Serialisierungen und Deserialisierungen (jSON) vornehmen. Dies liegt daran, dass die Filter, die auf den gesendeten Wert und die empfangene Antwort angewendet werden müssen, unterschiedlich sein können, und die Erfahrung zeigt, dass es einfacher ist, diese selbst zu verwalten, als zu versuchen, die Komponente [RestTemplate] so zu konfigurieren, dass sie den richtigen Konverter jSON verwendet;

  • Zeilen 42–92: Definieren Filter jSON. Es handelt sich um dieselben wie die des Servers, die in Abschnitt 13.5.3.1 vorgestellt und erläutert wurden;
  • Zeilen 43–46: ein Mapper jSON ohne Filter;
  • Zeilen 64–68: ein Mapper jSON, um eine Kategorie ohne ihre Produkte zu erhalten;
  • Zeilen 48–58: ein Mapper jSON, um eine Kategorie mit ihren Produkten zu erhalten;
  • Zeilen 83–92: ein Mapper jSON, um ein Produkt ohne seine Kategorie zu erhalten;
  • Zeilen 60–70: ein Mapper jSON, um ein Produkt mit seiner Kategorie zu erhalten;

Alle diese Beans werden in den Codes der Schicht [DAO] sowie im Junit-Test verfügbar sein.

13.6.3.2. Die Entitäten

  

Die von der Schicht [DAO] verarbeiteten Entitäten sind diejenigen, die sie mit dem Webdienst /jSON austauscht. Dabei handelt es sich um Artikel und Produkte. Auf der Serverseite wiesen diese Entitäten Persistenz-Annotationen JPA auf. Hier wurden diese Annotationen entfernt. Zur Erinnerung geben wir den Code der Entitäten noch einmal wieder:

[AbstractEntity]


package spring.client.entities;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public abstract class AbstractEntity {
    // Eigenschaften
    protected Long id;
    protected Long version;

    // Konstruktoren
    public AbstractEntity() {

    }

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

    // Neudefinition von [equals] und [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();
    }

    // Signatur jSON
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }

    // Getter und Setter
...
}

[Categorie]


package spring.client.entities;

import java.util.HashSet;
import java.util.Set;

import com.fasterxml.jackson.annotation.JsonFilter;

@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {

    // Eigenschaften
    private String nom;

    // verwandte Produkte
    public Set<Produit> produits = new HashSet<Produit>();

    // Konstruktoren
    public Categorie() {

    }

    public Categorie(String nom) {
        this.nom = nom;
    }

    // Methoden
    public void addProduit(Produit produit) {
        // Das Produkt hinzufügen
        produits.add(produit);
        // Kategorie festlegen
        produit.setCategorie(this);
    }

    // Getter und Setter
    ...
}

[Produit]


package spring.webjson.client.entities;

import com.fasterxml.jackson.annotation.JsonFilter;

@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {

    // der Name
    private String nom;
   // die Kategorie-Nr.
    private Long idCategorie;
   // den Preis
    private double prix;
   // die Beschreibung
    private String description;

    // die Kategorie
    private Categorie categorie;

    // Hersteller
    public Produit() {

    }

    public Produit(String nom, double prix, String description) {
        this.nom = nom;
        this.prix = prix;
        this.description = description;
    }

    // Getter und Setter
...
}

13.6.3.3. Die Klasse [DaoException]

 

Wenn in der Schicht [DAO] ein Fehler auftritt, löst sie eine Ausnahme vom Typ [DaoException] aus. Diese Klasse wird serverseitig verwendet und ist in Abschnitt 11.3.7 beschrieben.

13.6.3.4. Die Schnittstelle der Schicht [DAO]

 

Die Schicht [DAO] stellt die in Abschnitt 11.3.7 beschriebene Schnittstelle [IDao] bereit.


package spring.client.dao;

import java.util.List;

import spring.client.entities.Categorie;
import spring.client.entities.Produit;

public interface IDao {

    // Einfügen einer Produktliste
    public List<Produit> addProduits(List<Produit> produits);

    // Löschen aller Produkte
    public void deleteAllProduits();

    // Aktualisierung einer Produktliste
    public List<Produit> updateProduits(List<Produit> produits);

    // Abruf aller Produkte
    public List<Produit> getAllProduits();

    // Einfügen einer Kategorieliste
    public List<Categorie> addCategories(List<Categorie> categories);

    // Löschen aller Kategorien
    public void deleteAllCategories();

    // Aktualisierung einer Kategorieliste
    public List<Categorie> updateCategories(List<Categorie> categories);

    // Abruf aller Kategorien
    public List<Categorie> getAllCategories();

    // ein bestimmtes Produkt
    public Produit getProduitByIdWithCategorie(Long idProduit);

    public Produit getProduitByIdWithoutCategorie(Long idProduit);

    public Produit getProduitByNameWithCategorie(String nom);

    public Produit getProduitByNameWithoutCategorie(String nom);

    // eine bestimmte Kategorie
    public Categorie getCategorieByIdWithProduits(Long idCategorie);

    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);

    public Categorie getCategorieByNameWithProduits(String nom);

    public Categorie getCategorieByNameWithoutProduits(String nom);

}

13.6.3.5. Die Antwort des Webdienstes / jSON

  

Wir haben gesehen, dass alle URL des Webdienstes / jSON einen Typ [Response] zurückgeben, der in Abschnitt 13.5.5.3 definiert ist. Wir greifen diese Klasse hier noch einmal auf:


package spring.client.dao;

import java.util.List;

public class Response<T> {

    // ----------------- Eigenschaften
    // Status des Vorgangs
    private int status;
    // etwaige Fehlermeldungen
    private List<String> messages;
    // der Antworttext
    private T body;

    // Konstruktoren
    public Response() {

    }

    public Response(int status, List<String> messages, T body) {
        this.status = status;
        this.messages = messages;
        this.body = body;
    }

    // Getter und Setter
    ...
}

13.6.3.6. Implementierung des Datenaustauschs mit dem Webdienst / jSON

  

Die Klasse [AbstractDao] implementiert den Datenaustausch mit dem Webdienst / 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 {

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

    // generische Anfrage
    protected String getResponse(String url, String jsonPost) {

        // URL: URL – Kontakt
        // jsonPost: Der Wert jSON soll gesendet werden
        try {
            // Anfrage ausführen
            RequestEntity<?> request;
            if (jsonPost != null) {
                // Anfrage POST
                request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
            } else {
                // Anfrage GET
                request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON).build();
            }
            // Die Abfrage wird ausgeführt
            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 Komponente [RestTemplate], die die Kommunikation mit dem Server gewährleistet;
  • Zeilen 17–18: Einbindung der Komponente URL des Webdienstes / jSON;

Die Implementierung der Methoden zur Kommunikation mit dem Server ist in der Methode [getResponse] zusammengefasst:

  • Zeile 21: Die Methode erhält zwei Parameter:
    • [url]: die angeforderte URL;
    • [jsonPost]: die zu sendende Zeichenkette jSON, andernfalls null. Wenn [jsonPost==null], erfolgt die Anfrage an URL mit einem GET, andernfalls mit einem POST;
  • Zeile 38: Die Anweisung, die die Anfrage an den Server sendet und dessen Antwort empfängt. Die Komponente [RestTemplate] bietet eine Vielzahl von Methoden für den Austausch mit dem Server. Wir haben hier die Methode [exchange] gewählt, es gibt jedoch auch andere;
  • Zeilen 27–36: Wir müssen die Anfrage vom Typ [RequestEntity] erstellen. Diese unterscheidet sich je nachdem, ob ein GET oder ein POST für die Anfrage verwendet wird;
  • Zeilen 30–31: Die Abfrage für ein GET. Die Klasse [RequestEntity] bietet statische Methoden zum Erstellen der Abfragen GET, POST, HEAD, ... Die Methode [RequestEntity.get] ermöglicht es, eine Abfrage vom Typ GET zu erstellen, indem die verschiedenen Methoden, aus denen diese besteht, miteinander verkettet werden:
    • Die Methode [RequestEntity.get] akzeptiert als Parameter die Ziel-URL in Form einer Instanz von URI,
    • die Methode [accept] ermöglicht die Definition der Elemente des Headers HTTP [Accept]. Hier geben wir an, dass wir den Typ [application/json] akzeptieren, den der Server senden wird;
    • die Methode [build] verwendet diese verschiedenen Informationen, um den Typ [RequestEntity] der Anfrage zu erstellen;
  • Zeilen 34–35: die Anfrage für einen POST. Die Methode [RequestEntity.post] ermöglicht es, eine Anfrage vom Typ POST zu erstellen, indem die verschiedenen Methoden, aus denen diese besteht, miteinander verkettet werden:
    • Die Methode [RequestEntity.post] akzeptiert als Parameter das Ziel-URL in Form einer Instanz von URI,
    • die Methode [header] definiert einen Header HTTP. Hier wird der Header [Content-Type: application/json] an den Server gesendet, um ihm mitzuteilen, dass der übermittelte Wert in Form einer Zeichenkette jSON bei ihm eintreffen wird;
    • die Methode [accept] ermöglicht es, anzugeben, dass wir den Typ [application/json] akzeptieren, den der Server senden wird;
    • Die Methode [body] legt den gesendeten Wert fest. Dieser 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 Antwort des Servers kapselt: die Header HTTP und den Dokumenttext. Die Methode [ResponseEntity].getBody() ermöglicht den Zugriff auf diesen Hauptteil, der die Serverantwort darstellt, in diesem Fall eine Zeichenkette;

13.6.3.7. Implementierung der Schnittstelle [IDao]

  

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;

    // Filter 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) {
        // ----------- Produkte hinzufügen (ohne deren Kategorie)
        ...
}
  • Zeile 17: Die Klasse [Dao] ist eine Spring-Komponente, in die daher andere Spring-Komponenten injiziert werden können;
  • Zeile 18: Die Klasse [Dao] erweitert die soeben betrachtete Klasse [AbstractDao] und implementiert die Schnittstelle [IDao];
  • Zeilen 20–21: Wir injizieren den Spring-Kontext, um Zugriff auf dessen Beans zu erhalten;
  • Zeilen 24–38: Einbindung der Mapper jSON, die in der in Abschnitt 13.6.2 vorgestellten Klasse [AppConfig] definiert sind;

Die Implementierungen der verschiedenen Methoden der Schnittstelle [IDao] folgen alle demselben Schema. Wir stellen zwei Methoden vor, von denen sich die eine auf eine Operation [POST] stützt, die andere auf eine Operation [GET].

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


@Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        // ----------- eine bestimmte Kategorie anhand ihres Namens abrufen, zusammen mit den dazugehörigen Produkten
        try {
            // Abfrage
            Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
                    getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
                    new TypeReference<Response<Categorie>>() {
                    });
            // Fehler?
            if (response.getStatus() != 0) {
                // Es wird 1 Ausnahme ausgelöst
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // Der Kern der Serverantwort wird zurückgegeben
                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 sorgt für den Austausch mit dem Webdienst / jSON. Ihre Parameter lauten wie folgt:

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

Die Methode [getResponse] gibt einen String-Typ zurück, bei dem es sich um die vom Server gesendete Antwort jSON handelt. Diese Antwort jSON wird wie folgt deserialisiert:


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

da die Zeichenkette jSON die Serialisierung eines Typs [Response<Categorie>] ist;

  • Zeilen 11–17: Der Status der Antwort wird geprüft. Ist der Status ungleich 0, liegt ein serverseitiger Fehler vor. In diesem Fall wird eine Ausnahme ausgelöst (Zeile 13), wobei die in der Antwort enthaltenen Informationen (Status und Liste der Fehlermeldungen) übernommen werden;
  • Zeile 16: Wenn kein Fehler auf Serverseite aufgetreten ist, wird der Hauptteil des Typs [Response<Categorie>] zurückgegeben, d. h. die angeforderte Kategorie;
  • Zeilen 18–19: Die in Zeile 16 ausgelöste Ausnahme wird behandelt;
  • Zeilen 20–22: Alle anderen Ausnahmen werden behandelt;

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


@Override
    public List<Categorie> addCategories(List<Categorie> categories) {
        // ----------- Kategorien (mit ihren Produkten) hinzufügen
        try {
            // Anfrage
            Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
                    getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
                    new TypeReference<Response<List<Categorie>>>() {
                    });
            // Fehler?
            if (response.getStatus() != 0) {
                // Es wird eine Ausnahme ausgelöst
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // Der Kern der Serverantwort wird zurückgegeben
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (RuntimeException | IOException e2) {
            throw new DaoException(104, e2);
        }
    }
  • Zeile 2: Die Methode [addCategories] dient dazu, die als Parameter übergebenen Kategorien in der Datenbank zu speichern. Sie gibt dieselben Kategorien, ergänzt um ihre Primärschlüssel, zurück. Werden die Kategorien zusammen mit Produkten übergeben, werden diese ebenfalls gespeichert;
  • Zeile 7: Die Methode [getResponse] des übergeordneten Objekts wird aufgerufen, um den Datenaustausch mit dem Webdienst / jSON durchzuführen;
    • Der erste Parameter ist URL [/addCategories];
    • der zweite Parameter ist der übermittelte Wert, in diesem Fall die Liste der zu speichernden Kategorien;

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

Die erhaltene Zeichenfolge jSON wird anschließend deserialisiert, um den erwarteten Typ [Response<List<Categorie>] 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: Behandlung von Ausnahmen;

Alle anderen Methoden folgen dem Schema der beiden vorgestellten Methoden.

13.6.4. Der Test JUnit

Kehren wir zur Client-Server-Architektur zurück, die gerade aufgebaut wird:

Wir haben eine Schicht [DAO] [2] mit derselben Schnittstelle wie die Schicht [DAO] [4] erstellt. Um die Schicht [DAO] [2] zu testen, kann daher der Test JUnit verwendet werden, der bereits zum Testen der Schicht [DAO] [4] diente. Zur Erinnerung: Dieser lautet wie folgt:

  

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 {

    // Schicht [DAO]
    @Autowired
    private IDao dao;

    // Filter 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() {
        // Die Datenbank wird vor jedem Test bereinigt
        log("Vidage de la base de données", 1);
        // Die Tabelle [CATEGORIES] wird geleert – in der Folge wird auch die Tabelle [PRODUITS] geleert
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // Die Tabellen werden gefüllt
        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);
        }
        // Hinzufügen der Kategorie – durch die Kaskadenwirkung werden auch die Produkte eingefügt
        categories = dao.addCategories(categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // Liste der Kategorien
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // Liste der Produkte
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // einige Überprüfungen
        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);
        // Das Einfügen erfolgt
        Categorie categorie = new Categorie("cat1");
        categorie.addProduit(new Produit("x", 1.0, ""));
        categorie.addProduit(new Produit("x", 1.0, ""));
        // Hinzufügen der Kategorie – die Produkte werden ebenfalls automatisch eingefügt
        try {
            categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
        } catch (DaoException e) {
            show("Les erreurs suivantes se sont produites :", e.getErreurs());
        }
        // Überprüfungen
        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);
        // Überprüfungen
        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);
        // Überprüfung
        affiche(produits, jsonMapperProduitWithoutCategorie);
    }

    // -------------- private Methoden
    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;
    }

    // Anzeige eines Elements vom Typ T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // Anzeige einer Liste von Elementen vom Typ 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) {
        // Meldung anzeigen
        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) {
        // Titel
        System.out.println(String.format("%s : ", title));
        // Meldungen
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }

}

Die Ausführung war erfolgreich und liefert folgende Ergebnisse in der Konsole:


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