13. [Kurs]: Bereitstellung einer Datenbank im Web mit Spring MVC
Stichworte: mehrschichtige Architektur, Spring, Dependency Injection, Webservice / JSON, Client / Server
13.1. Support
![]() | ![]() |
Die Projekte zu diesem Kapitel finden Sie im Ordner [support / chap-13]. Das SQL-Skript [dbintrospringdata.sql] erstellt die für die Tests erforderliche MySQL-Datenbank.
13.2. Die Rolle von Spring MVC in einer Webanwendung
Betrachten wir Spring MVC im Kontext der Entwicklung einer Webanwendung. Meistens basiert diese auf einer mehrschichtigen Architektur wie der folgenden:
![]() |
- Die [Web]-Schicht ist die Schicht, die mit dem Benutzer der Webanwendung in Kontakt steht. Der Benutzer interagiert mit der Webanwendung über Webseiten, die in einem Browser angezeigt werden. Spring MVC befindet sich in dieser Schicht und nur in dieser Schicht;
- Die [Business]-Schicht implementiert die Geschäftslogik der Anwendung, wie beispielsweise die Berechnung eines Gehalts oder einer Rechnung. Diese Schicht nutzt Daten vom Benutzer über die [Web]-Schicht und aus dem DBMS über die [DAO]-Schicht;
- Die [DAO]-Schicht (Data Access Objects), die [ORM]-Schicht (Object Relational Mapper) und der JDBC-Treiber verwalten den Zugriff auf Daten im DBMS. Die [ORM]-Schicht fungiert als Brücke zwischen den von der [DAO]-Schicht verwalteten Objekten und den Zeilen und Spalten von Tabellen in einer relationalen Datenbank. Die JPA-Spezifikation (Java Persistence API) ermöglicht eine Abstraktion vom verwendeten ORM, sofern dieses die Spezifikationen implementiert. Dies ist hier der Fall, und wir werden die ORM-Schicht fortan als JPA-Schicht bezeichnen;
- Die Integration der Schichten wird vom Spring-Framework übernommen;
13.3. Das Spring-MVC-Entwicklungsmodell
Spring MVC implementiert das MVC-Architekturmuster (Model–View–Controller) wie folgt:
![]() |
Die Verarbeitung einer Client-Anfrage verläuft wie folgt:
- Anfrage – die angeforderten URLs haben die Form http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Der [Front Controller] nutzt eine Konfigurationsdatei oder Java-Annotationen, um die Anfrage an den richtigen Controller und die richtige Aktion innerhalb dieses Controllers weiterzuleiten. Dazu verwendet er das Feld [Action] der URL. Der Rest der URL [/param1/param2/...] besteht aus optionalen Parametern, die an die Aktion übergeben werden. Das C in MVC bezieht sich hier auf die Kette [Front Controller, Controller, Action]. Wenn kein Controller die angeforderte Aktion verarbeiten kann, antwortet der Webserver, dass die angeforderte URL nicht gefunden wurde.
- Verarbeitung
- Die ausgewählte Aktion kann die Parameter verwenden, die der [Front Controller] an sie übergeben hat. Diese können aus verschiedenen Quellen stammen:
- dem Pfad [/param1/param2/...] der URL,
- die [p1=v1&p2=v2]-Parameter der URL,
- aus Parametern, die der Browser mit seiner Anfrage gesendet hat;
- Bei der Verarbeitung der Benutzeranfrage benötigt die Aktion möglicherweise die [Business]-Schicht [2b]. Sobald die Anfrage des Clients verarbeitet wurde, kann dies verschiedene Antworten auslösen. Ein klassisches Beispiel ist:
- eine Fehlerseite, wenn die Anfrage nicht korrekt verarbeitet werden konnte
- ansonsten eine Bestätigungsseite
- die Aktion weist an, eine bestimmte Ansicht anzuzeigen [3]. Diese Ansicht zeigt Daten an, die als View-Modell bezeichnet werden. Dies ist das M in MVC. Die Aktion erstellt dieses M-Modell [2c] und weist an, eine V-Ansicht anzuzeigen [3];
- Antwort – die ausgewählte Ansicht V verwendet das von der Aktion erstellte Modell M, um die dynamischen Teile der HTML-Antwort zu initialisieren, die sie an den Client senden muss, und sendet dann diese Antwort.
Bei einem Webdienst / JSON wird die vorstehende Architektur leicht modifiziert:
![]() |
- In [4a] wird das Modell, bei dem es sich um eine Java-Klasse handelt, durch eine JSON-Bibliothek in eine JSON-Zeichenkette umgewandelt;
- in [4b] wird diese JSON-Zeichenkette an den Browser gesendet;
Lassen Sie uns nun die Beziehung zwischen der MVC-Webarchitektur und der Schichtenarchitektur klären. Je nachdem, wie das Modell definiert ist, können diese beiden Konzepte miteinander in Verbindung stehen oder auch nicht. Betrachten wir eine einschichtige Spring-MVC-Webanwendung:
![]() |
Wenn wir die [Web]-Schicht mit Spring MVC implementieren, erhalten wir zwar eine MVC-Webarchitektur, jedoch keine mehrschichtige Architektur. Hier übernimmt die [Web]-Schicht alles: Darstellung, Geschäftslogik und Datenzugriff. Diese Aufgaben werden von den Aktionen ausgeführt.
Betrachten wir nun eine mehrschichtige Webarchitektur:
![]() |
Die [Web]-Schicht kann ohne Framework und ohne Befolgung des MVC-Musters implementiert werden. In diesem Fall haben wir zwar immer noch eine mehrschichtige Architektur, aber die Web-Schicht implementiert das MVC-Muster nicht.
In der .NET-Welt lässt sich die oben beschriebene [Web]-Schicht beispielsweise mit ASP.NET MVC implementieren, was zu einer mehrschichtigen Architektur mit einer [Web]-Schicht im MVC-Stil führt. Allerdings kann diese ASP.NET-MVC-Schicht durch eine klassische ASP.NET-Schicht (WebForms) ersetzt werden, während der Rest (Geschäftslogik, DAO, ORM) unverändert bleibt. Wir haben dann eine Schichtenarchitektur mit einer [Web]-Schicht, die nicht mehr MVC-basiert ist.
In MVC haben wir gesagt, dass das M-Modell das der V-Ansicht ist, d. h. die Menge der von der V-Ansicht angezeigten Daten. Es gibt eine weitere Definition des M-Modells in MVC:
![]() |
Viele Autoren sind der Ansicht, dass das, was rechts von der [Web]-Schicht liegt, das M-Modell von MVC bildet. Um Mehrdeutigkeiten zu vermeiden, können wir uns auf Folgendes beziehen:
- das Domänenmodell, wenn wir uns auf alles rechts von der [Web]-Schicht beziehen
- das View-Modell, wenn wir uns auf die von einer Ansicht V angezeigten Daten beziehen
Im Folgenden bezieht sich der Begriff „M-Modell“ ausschließlich auf das Modell einer V-Ansicht.
13.4. Ein Web/JSON-Projekt mit Spring MVC
Die Website [http://spring.io/guides] bietet Einführungs-Tutorials zur Erkundung des Spring-Ökosystems. Wir werden einem davon folgen, um die für ein Spring-MVC-Projekt erforderliche Maven-Konfiguration zu ermitteln.
13.4.1. Das Demo-Projekt
![]() |
- In [1] importieren wir einen der Spring-Guides;
![]() |
- in [2] wählen wir das Beispiel [Rest Service] aus;
- in [3] wählen wir das Maven-Projekt aus;
- in [4] wählen wir die endgültige Version des Leitfadens aus;
- in [5] bestätigen wir;
- in [6] das importierte Projekt;
Webdienste, auf die über Standard-URLs zugegriffen werden kann und die JSON-Daten zurückgeben, werden oft als REST-Dienste (REpresentational State Transfer) bezeichnet. Ein Dienst gilt als RESTful, wenn er bestimmte Regeln befolgt.
Betrachten wir nun das importierte Projekt, beginnend mit seiner Maven-Konfiguration.
13.4.2. Maven-Konfiguration
Die Datei [pom.xml] sieht wie folgt aus:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>https://repo.spring.io/libs-release</url>
</pluginRepository>
</pluginRepositories>
</project>
- Zeilen 6–8: Die Maven-Projekteigenschaften. Ein [<packaging>]-Tag, das den vom Maven-Build erzeugten Dateityp angibt, fehlt. In diesem Fall wird der Typ [jar] verwendet. Die Anwendung ist daher eine konsolenbasierte ausführbare Anwendung und keine Webanwendung; in diesem Fall wäre die Verpackung [war];
- Zeilen 10–14: Das Maven-Projekt hat ein übergeordnetes Projekt [spring-boot-starter-parent]. Dieses definiert die meisten Abhängigkeiten des Projekts. Diese können ausreichend sein – in diesem Fall werden keine zusätzlichen Abhängigkeiten hinzugefügt – oder auch nicht; in diesem Fall werden die fehlenden Abhängigkeiten hinzugefügt;
- Zeilen 17–20: Das Artefakt [spring-boot-starter-web] enthält die Bibliotheken, die für ein Spring-MVC-Webservice-Projekt erforderlich sind, bei dem keine Ansichten generiert werden. Dieses Artefakt umfasst eine sehr große Anzahl von Bibliotheken, darunter auch solche für einen eingebetteten Tomcat-Server. Die Anwendung wird auf diesem Server ausgeführt;
Die in dieser Konfiguration enthaltenen Bibliotheken sind sehr zahlreich:
![]() | ![]() |
Oben sehen wir die drei Tomcat-Server-Archive.
13.4.3. Die Architektur eines Spring [Web/JSON]-Dienstes
Für einen Web-/JSON-Dienst implementiert Spring MVC das MVC-Modell wie folgt:
![]() |
- In [4a] wird das Modell – eine Java-Klasse – durch eine JSON-Bibliothek in eine JSON-Zeichenkette umgewandelt;
- in [4b] wird diese JSON-Zeichenkette an den Browser gesendet;
13.4.4. Der C-Controller
![]() |
Die importierte Anwendung verfügt über den folgenden Controller:
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
- Zeile 9: Die Annotation [@RestController] macht die Klasse [GreetingController] zu einem Spring-Controller, was bedeutet, dass ihre Methoden für die Verarbeitung von URLs registriert sind. Wir haben bereits die ähnliche Annotation [@Controller] gesehen. Der Rückgabetyp der Methoden dieses Controllers war [String], also der Name der anzuzeigenden Ansicht. Hier ist das anders. Die Methoden eines [@RestController] geben Objekte zurück, die serialisiert und an den Browser gesendet werden. Die Art der durchgeführten Serialisierung hängt von der Spring-MVC-Konfiguration ab. Hier werden sie in JSON serialisiert. Das Vorhandensein einer JSON-Bibliothek in den Projektabhängigkeiten bewirkt, dass Spring Boot das Projekt automatisch auf diese Weise konfiguriert;
- Zeile 14: Die Annotation [@RequestMapping] gibt die von der Methode bearbeitete URL an, in diesem Fall die URL [/greeting];
- Zeile 15: Die Annotation [@RequestParam] haben wir bereits erläutert. Das von der Methode zurückgegebene Ergebnis ist ein Objekt vom Typ [Greeting].
- Zeile 12: Eine Long-Integer-Variable vom Typ „atomic“. Das bedeutet, dass sie parallelen Zugriff unterstützt. Es kann vorkommen, dass mehrere Threads gleichzeitig die Variable [counter] inkrementieren wollen. Dies wird ordnungsgemäß gehandhabt. Ein Thread kann den Wert des Zählers erst lesen, wenn der Thread, der ihn gerade ändert, seine Änderung abgeschlossen hat.
13.4.5. Das M-Modell
Das durch die vorherige Methode erzeugte M-Modell ist das folgende [Greeting]-Objekt:
![]() |
package hello;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
Die JSON-Umwandlung dieses Objekts erzeugt die Zeichenfolge {"id":n,"content":"text"}. Letztendlich hat die von der Controller-Methode erzeugte JSON-Zeichenfolge folgende Form:
oder
13.4.6. Ausführung
![]() |
Die Klasse [Application.java] ist die ausführbare Klasse des Projekts. Ihr Code lautet wie folgt:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Wir sind diesem Code bereits im vorherigen Beispiel begegnet und haben ihn dort erklärt.
13.4.7. Das Projekt ausführen
![]() |
Wir erhalten die folgenden Konsolenprotokolle:
- Zeile 13: Der Tomcat-Server startet auf Port 8080 (Zeile 12);
- Zeile 17: Das Servlet [DispatcherServlet] ist vorhanden;
- Zeile 20: Die Methode [GreetingController.greeting] wurde gefunden;
Um die Webanwendung zu testen, rufen wir die URL [http://localhost:8080/greeting] auf:
![]() | ![]() |
Wir erhalten die erwartete JSON-Zeichenkette. Es könnte interessant sein, die vom Server gesendeten HTTP-Header anzusehen. Dazu verwenden wir die Chrome-Erweiterung namens [Advanced Rest Client] (Chrome / Strg-T / Menü [Anwendungen] / [Advanced Rest Client] – siehe Anhang, Abschnitt 22.5):
![]() |
- in [1] die angeforderte URL;
- in [2] wird die GET-Methode verwendet;
- in [3] die JSON-Antwort;
- in [4] hat der Server angegeben, dass er eine Antwort im JSON-Format sendet;
- in [5] fordern wir dieselbe URL an, diesmal jedoch mit einer POST-Anfrage;
- in [7] werden die Informationen im [urlencoded]-Format an den Server gesendet;
- in [6] der Parameter „name“ und sein Wert;
- in [8] teilt der Browser dem Server mit, dass er [urlencoded]-Informationen sendet;
- in [9] die JSON-Antwort des Servers;
13.4.8. Erstellen eines ausführbaren Archivs
Wir erstellen nun ein ausführbares Archiv:
![]() |
![]() |
- in [1]: Wir führen ein Maven-Target aus;
- in [2]: Es gibt zwei Ziele: [clean], um den Ordner [target] aus dem Maven-Projekt zu löschen, und [package], um ihn neu zu generieren;
- in [3]: Der generierte Ordner [target] befindet sich in diesem Ordner;
- in [4]: Wir generieren das Ziel;
In den Protokollen, die in der Konsole angezeigt werden, ist es wichtig, das Plugin [spring-boot-maven-plugin] zu sehen. Dies ist das Plugin, das das ausführbare Archiv generiert.
Navigieren Sie über die Konsole zum generierten Ordner:
D:\Temp\wksSTS\gs-rest-service\target>dir
...
11/06/2014 15:30 <DIR> classes
11/06/2014 15:30 <DIR> generated-sources
11/06/2014 15:30 11 073 572 gs-rest-service-0.1.0.jar
11/06/2014 15:30 3 690 gs-rest-service-0.1.0.jar.original
11/06/2014 15:30 <DIR> maven-archiver
11/06/2014 15:30 <DIR> maven-status
...
- Zeile 5: das erstellte Archiv;
Dieses Archiv wird wie folgt ausgeführt:
D:\Temp\wksSTS\gs-rest-service-complete\target>java -jar gs-rest-service-0.1.0.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.1.0.RELEASE)
2014-06-11 15:32:47.088 INFO 4972 --- [ main] hello.Application
: Starting Application on Gportpers3 with PID 4972 (D:\Temp\wk
sSTS\gs-rest-service-complete\target\gs-rest-service-0.1.0.jar started by ST in
D:\Temp\wksSTS\gs-rest-service-complete\target)
...
Nachdem die Webanwendung nun ausgeführt wird, können Sie über einen Browser darauf zugreifen:
![]() |
13.4.9. Bereitstellung der Anwendung auf einem Tomcat-Server
Wie bereits beim vorherigen Projekt ändern wir die Datei [pom.xml] wie folgt:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
...
</project>
- Zeile 9: Sie müssen angeben, dass Sie eine WAR-Datei (Web Archive) erstellen möchten;
Außerdem müssen Sie die Webanwendung konfigurieren. Wenn keine [web.xml]-Datei vorhanden ist, erfolgt dies über eine Klasse, die [SpringBootServletInitializer] erweitert:
![]() |
Die Klasse [ApplicationInitializer] sieht wie folgt aus:
package hello;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ApplicationInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
- Zeile 6: Die Klasse [ApplicationInitializer] erweitert die Klasse [SpringBootServletInitializer];
- Zeile 9: Die Methode [configure] wird überschrieben (Zeile 8);
- Zeile 10: Die Klasse, die das Projekt konfiguriert, wird bereitgestellt;
Um das Projekt auszuführen, gehen Sie wie folgt vor:
![]() |
- Führen Sie in [1-2] das Projekt auf einem der in der Eclipse-IDE registrierten Server aus;
Sobald dies geschehen ist, können Sie die URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] in einem Browser aufrufen:
![]() |
13.4.10. Fazit
Wir haben eine Art von Spring-MVC-Projekt vorgestellt, bei dem die Webanwendung einen JSON-Stream an den Browser sendet. Wir werden nun eine Web-/JSON-Anwendung entwickeln, um die im Tutorial [Einführung in Spring Data] behandelte Datenbank [dbintrospringdata] im Web verfügbar zu machen.
13.5. Bereitstellung der Datenbank [dbintrospringdata] im Web
13.5.1. Web/JSON-Service-Architektur
Wir werden die folgende Architektur implementieren:
![]() |
Die [DAO]- und [JPA]-Schichten werden durch die im Tutorial [Einführung in Spring Data] beschriebene Anwendung implementiert.
13.5.2. Installation der Datenbank
![]() |
Das SQL-Skript [dbintrospringdata.sql] erstellt die für den Test erforderliche MySQL-Datenbank.
13.5.3. Das Eclipse-Projekt für den Webservice / JSON
Das Eclipse-Projekt für den Webservice / JSON sieht wie folgt aus:
![]() |
Dies ist ein Maven-Projekt, dessen [pom.xml]-Datei wie folgt aussieht:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.webjson</groupId>
<artifactId>intro-server-webjson01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>intro-server-webjson01</name>
<description>démo spring mvc</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>istia.st.springdata</groupId>
<artifactId>intro-spring-data-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
</project>
- Zeilen 11–15: das übergeordnete Maven-Projekt, das bereits für die [DAO]-Schicht verwendet wurde;
- Zeilen 18–22: die Abhängigkeit zur [DAO]-Schicht;
- Zeilen 23–26: die Abhängigkeit vom Artefakt [spring-boot-starter-web]. Dieses Artefakt enthält alle Abhängigkeiten, die zum Erstellen eines Web-/JSON-Dienstes benötigt werden. Es enthält jedoch auch unnötige Bibliotheken. Eine präzisere Konfiguration wäre daher erforderlich. Diese Konfiguration ist jedoch für den Einstieg nützlich;
- Zeilen 28–30: Die Abhängigkeit vom Artefakt [spring-boot-starter] ermöglicht die Verwaltung von Spring-Boot-Annotationen;
Die durch diese Konfiguration eingeführten Abhängigkeiten lauten wie folgt:
![]() |
- In [1] ist zu sehen, dass Eclipse die Abhängigkeit vom Projektarchiv [intro-spring-data-01] erkannt hat;
Die oben genannten Abhängigkeiten betreffen sowohl die [DAO]-Schicht als auch die [Web]-Schicht.
13.5.3.1. Konfiguration der [web]-Schicht
Die [web]-Schicht wird über eine [AppConfig]-Datei konfiguriert:
![]() |
Die Klasse [WebConfig] konfiguriert die [Web]-Schicht:
package spring.webjson.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.ServletRegistrationBean;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
// -------------------------------- layer configuration [web]
@Autowired
private ApplicationContext context;
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet((WebApplicationContext) context);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// filters jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// result
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// result
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// result
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// result
return mapper;
}
}
- Zeile 18: Die Annotation [@EnableWebMvc] löst automatische Konfigurationen für das Spring MVC-Framework aus;
- Zeile 19: Die Klasse [WebConfig] erweitert die Spring-Klasse [WebMvcConfigurerAdapter], um bestimmte Beans neu zu definieren (Zeilen 26–40);
- Zeilen 22–23: Einbindung des Spring-Kontexts;
- Zeilen 25–29: Definition des Servlets des Spring MVC-Frameworks, das HTTP-Anfragen an den richtigen Controller und die richtige Methode weiterleitet. [DispatcherServlet] ist eine Spring-Klasse;
- Zeilen 31–34: Wir legen fest, dass dieses Servlet alle URLs verarbeitet;
- Zeilen 36–39: Das Vorhandensein dieses Beans aktiviert den in den Projektarchiven enthaltenen Tomcat-Server. Er lauscht auf Anfragen am Port 8080;
- Zeilen 42–91: Beans, die zur Verwaltung von JSON-Filtern verwendet werden;
- Zeilen 42–45: ein JSON-Mapper ohne Filter;
- Zeilen 47–57: der JSON-Mapper, mit dem Sie eine Kategorie zusammen mit ihren Produkten abrufen können. Beachten Sie, dass Sie bei der Anforderung einer Kategorie mit ihren Produkten sowohl den JSON-Filter für die Klasse [Category] als auch den für die Klasse [Product] konfigurieren müssen. Dies ist immer der Fall. Bei der Serialisierung/Deserialisierung einer Klasse in JSON müssen Sie den JSON-Filter für die Klasse und die für alle darin enthaltenen Abhängigkeiten konfigurieren;
- Zeilen 59–69: Der JSON-Mapper, der es ermöglicht, ein Produkt zusammen mit seiner Kategorie anzuzeigen;
- Zeilen 71–80: der JSON-Mapper, der es ermöglicht, eine Kategorie ohne ihre Produkte anzuzeigen;
- Zeilen 82–91: der JSON-Mapper, mit dem Sie ein Produkt ohne seine Kategorie abrufen können;
Die Klasse [AppConfig] konfiguriert die gesamte Anwendung, d. h. die Schichten [web] und [DAO]:
package spring.webjson.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import spring.data.config.DaoConfig;
@ComponentScan(basePackages = { "spring.webjson" })
@Import({ DaoConfig.class, WebConfig.class})
public class AppConfig {
}
- Zeile 9: importiert die Beans aus der [DAO]-Schicht und aus der [Web]-Schicht;
- Zeile 8: gibt die Pakete an, in denen andere Spring-Beans zu finden sind;
Beachten Sie, dass wir die Annotation [@EnableAutoConfiguration] nirgendwo verwendet haben. Wir haben es vorgezogen, die Konfiguration selbst zu steuern.
13.5.4. Das Anwendungsmodell
![]() |
Die Klasse [ApplicationModel] sieht wie folgt aus:
package spring.webjson.models;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
@Component
public class ApplicationModel implements IDao {
// the [DAO] layer
@Autowired
private IDao dao;
@Override
public void addProduits(List<Produit> produits) {
dao.addProduits(produits);
}
@Override
public void deleteAllProduits() {
dao.deleteAllProduits();
}
@Override
public void updateProduits(List<Produit> produits) {
dao.updateProduits(produits);
}
@Override
public List<Produit> getAllProduits() {
return dao.getAllProduits();
}
@Override
public void addCategories(List<Categorie> categories) {
dao.addCategories(categories);
}
@Override
public void deleteAllCategories() {
dao.deleteAllCategories();
}
@Override
public void updateCategories(List<Categorie> categories) {
dao.updateCategories(categories);
}
@Override
public List<Categorie> getAllCategories() {
return dao.getAllCategories();
}
@Override
public Produit getProduitByIdWithCategorie(Long idProduit) {
return dao.getProduitByIdWithCategorie(idProduit);
}
@Override
public Produit getProduitByNameWithCategorie(String nom) {
return dao.getProduitByNameWithCategorie(nom);
}
@Override
public Categorie getCategorieByIdWithProduits(Long idCategorie) {
return dao.getCategorieByIdWithProduits(idCategorie);
}
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
return dao.getCategorieByNameWithProduits(nom);
}
@Override
public Produit getProduitByIdWithoutCategorie(Long idProduit) {
return dao.getProduitByIdWithoutCategorie(idProduit);
}
@Override
public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
return dao.getCategorieByIdWithoutProduits(idCategorie);
}
@Override
public Produit getProduitByNameWithoutCategorie(String nom) {
return dao.getProduitByNameWithoutCategorie(nom);
}
@Override
public Categorie getCategorieByNameWithoutProduits(String nom) {
return dao.getCategorieByNameWithoutProduits(nom);
}
}
- Zeile 12: Die Klasse ist ein Spring-Singleton;
- Zeile 13: die die [IDao]-Schnittstelle der [DAO]-Schicht implementiert;
- Zeilen 16–17: Injektion einer Referenz in die [DAO]-Schicht;
- Zeilen 19–99: Implementierung der [IDao]-Schnittstelle;
Die Architektur der Webschicht entwickelt sich wie folgt:
![]() |
- In [2b] kommunizieren die Methoden des/der Controller(s) mit dem [ApplicationModel]-Singleton;
Diese Strategie bietet Flexibilität hinsichtlich der Verwaltung eines potenziellen Caches. Die Klasse [ApplicationModel] kann verwendet werden, um Informationen aus der [DAO]-Schicht oder Konfigurationsdaten zu speichern. Dies kann nützlich sein, wenn Sie keine Kontrolle über die [DAO]-Schicht haben. Diese Caching-Strategie kann sich im Laufe der Zeit weiterentwickeln. Änderungen haben keine Auswirkungen auf den Code des/der Controller(s).
13.5.5. Der Controller
![]() |
![]() |
Hier haben wir nur einen Controller, die Klasse [MyController].
13.5.5.1. Veröffentlichte URLs
Die von diesem Controller bereitgestellten URLs lauten wie folgt:
| Fügt Produkte zur Datenbank hinzu. Diese werden per POST gesendet. Die Antwort ist eine JSON-Zeichenkette, die die Liste der hinzugefügten Produkte mit ihren Primärschlüsseln enthält. |
| Löscht alle Produkte aus der Datenbank. |
| Aktualisiert Produkte in der Datenbank. Diese werden gepostet. Die Antwort ist eine JSON-Zeichenkette, die die Liste der aktualisierten Produkte enthält. |
| Ruft die JSON-Zeichenkette für alle Produkte ab. |
| Fügt Kategorien zur Datenbank hinzu. Diese werden per POST gesendet. Die Antwort ist ein JSON-String, der die Liste der hinzugefügten Kategorien zusammen mit ihren Primärschlüsseln enthält. Wenn die Kategorien Produkte enthalten, werden diese ebenfalls zur Datenbank hinzugefügt. |
| Löscht alle Kategorien aus der Datenbank sowie alle darin enthaltenen Produkte. Danach ist die Datenbank leer. |
| Aktualisiert Kategorien in der Datenbank. Diese werden per POST gesendet. Die Antwort ist die Liste der aktualisierten Kategorien. Wenn die Kategorien Produkte enthalten, werden diese ebenfalls in der Datenbank aktualisiert. Gibt die JSON-Zeichenkette der geänderten Kategorien zurück; |
| Ruft die JSON-Zeichenkette für alle Kategorien ab. |
| Ruft die JSON-Zeichenkette für ein Produkt ab, das anhand seiner ID identifiziert wird, zusammen mit seiner Kategorie. |
| Ruft die JSON-Zeichenkette für ein Produkt ab, das anhand seiner ID identifiziert wird, ohne dessen Kategorie. |
| Ruft die JSON-Zeichenkette für ein Produkt ab, das anhand seines Namens identifiziert wird, zusammen mit seiner Kategorie. |
| Ruft die JSON-Zeichenkette für ein Produkt ab, das anhand seines Namens identifiziert wird, ohne dessen Kategorie. |
| Ruft die JSON-Zeichenkette für eine anhand ihrer ID angegebene Kategorie zusammen mit den dazugehörigen Produkten ab. |
| Ruft die JSON-Zeichenkette für eine anhand ihres Namens angegebene Kategorie zusammen mit den dazugehörigen Produkten ab. |
| Ruft die JSON-Zeichenkette für eine Kategorie ab, die durch ihren Namen angegeben wird, ohne deren Produkte. |
| Ruft die JSON-Zeichenkette für eine Kategorie ab, die durch ihre ID identifiziert wird, wobei ihre Produkte ausgeschlossen werden. |
Die offengelegten URLs entsprechen den Methoden der [IDao]-Schnittstelle in der [DAO]-Schicht. Die Methoden des Webdienstes / JSON basieren alle auf demselben Modell. Wir werden einige davon näher betrachten.
13.5.5.2. Das Controller-Gerüst
Das Controller-Gerüst sieht wie folgt aus:
package spring.webjson.service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.CharStreams;
import spring.data.dao.DaoException;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.webjson.models.ApplicationModel;
import spring.webjson.models.Response;
@Controller
public class MyController {
// spring dependencies
@Autowired
private ApplicationModel application;
// filters jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
// class [MyController] is a singleton and is instantiated only once the bean
public MyController() {
// System.out.println("MyController");
}
@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProduits(HttpServletRequest request) throws JsonProcessingException {
...
}
- Zeile 28: Die Annotation [@Controller] macht die Klasse zu einer Spring-Komponente;
- Zeilen 32–33: Einfügen einer Referenz auf die Klasse [ApplicationModel];
- Zeilen 36–50: Injektion von Referenzen auf die JSON-Mapper;
- Zeile 58: Die exponierte URL lautet [/addProducts]. Der Client muss eine [POST]-Methode verwenden, um seine Anfrage zu stellen (method = RequestMethod.POST). Er muss den gesendeten Wert als JSON-String übermitteln (content-type = "application/json; charset=UTF-8"). Die Methode selbst gibt die Antwort an den Client zurück (Zeile 59). Dies ist eine Zeichenkette (Zeile 60). Der HTTP-Header [Content-type: application/json; charset=UTF-8] wird an den Client gesendet, um anzuzeigen, dass er eine JSON-Zeichenkette erhält (Zeile 58);
- Zeile 60: Die Methode [addProduits] gibt die JSON-Zeichenkette zurück, die die Liste der zur Datenbank hinzugefügten Produkte enthält;
13.5.5.3. Antworten von Controller-Methoden
Alle Controller-Methoden geben den folgenden Typ [Response] zurück:
![]() |
package spring.webjson.service;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
- Zeile 5: Die Antwort kapselt einen Typ T;
- Zeile 13: die Antwort vom Typ T;
- Zeilen 9–11: Eine Methode kann auf eine Ausnahme stoßen. In diesem Fall gibt sie eine Antwort zurück mit:
- Zeile 9: status!=0;
- Zeile 11: die Liste der aufgetretenen Fehler;
13.5.5.4. Die URL [/addProducts]
Die URL [/addProducts] wird von der folgenden Methode verarbeitet:
@RequestMapping(value = "/addProduits", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addProduits(HttpServletRequest request) throws JsonProcessingException {
// answer
Response<List<Produit>> response;
try {
// retrieve the posted value
String body = CharStreams.toString(request.getReader());
List<Produit> produits = jsonMapperProduitWithoutCategorie.readValue(body, new TypeReference<List<Produit>>() {
});
// we re-establish the link between products and categories
for (Produit produit : produits) {
produit.setCategorie(application.getCategorieByIdWithoutProduits(produit.getIdCategorie()));
}
// we persist products
application.addProduits(produits);
response = new Respon se<List<Produit>>(0, null, produits);
} catch (DaoException e1) {
response = new Response<List<Produit>>(1000, e1.getErreurs(), null);
} catch (Exception e2) {
response = new Response<List<Produit>>(1000, getErreursForException(e2), null);
}
// answer jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- Zeile 3: Die Methode nimmt [HttpServletRequest request] als Parameter entgegen, der alle Informationen zur Anfrage des Clients enthält;
- Zeile 5: Die Antwort, die an den Client gesendet wird: eine Liste von Produkten;
- Zeile 8: Wir rufen den gesendeten Wert ab. Die Klasse [CharStreams] gehört zur Bibliothek [Google Guava], deren Referenz wir der Datei [pom.xml] hinzugefügt haben. Wir erhalten die vom Client gesendete JSON-Zeichenkette. Wir müssen sie deserialisieren, um damit arbeiten zu können;
- Zeilen 8–10: Die Deserialisierung wird durchgeführt. Wir erhalten eine Liste von Produkten, wobei jedes Produkt ein Feld [category=null] aufweist;
- Zeilen 12–14: Wir setzen das Feld [category] für alle Produkte in der Liste zurück. Dazu verwenden wir das Feld [categoryId] des Produkts, das initialisiert ist;
- Zeile 16: Die Produkte werden in die Datenbank eingefügt;
- Zeile 17: Das [response]-Objekt wird mit der Liste der Produkte initialisiert;
- Zeilen 18–19: Fall, in dem die Methode auf eine Ausnahme aus der [DAO]-Schicht stößt. Wir initialisieren die Antwort mit [status=1000] (Fehlercode) [messages=e1.getMessages()], d. h., wir senden dem Client die Liste der auf der Serverseite aufgetretenen Fehler;
- Zeilen 20–21: Fall, in dem die Methode auf eine andere Art von Ausnahme stößt. Wir initialisieren die Antwort mit [status=1000] (Fehlercode) [messages=getErrorsForException(e)], wobei [getErrorsForException] eine private Methode der Klasse ist, die die Liste der Fehler zurückgibt, die mit den Ausnahmen im Ausnahmestapel von e verbunden sind, und [body=null];
- Zeile 24: Die JSON-Zeichenkette der Antwort wird zurückgegeben;
13.5.5.5. Die URL [/getAllProducts]
Die URL [/getAllProducts] wird von der folgenden Methode verarbeitet:
@RequestMapping(value = "/getAllProduits", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllProduits() throws JsonProcessingException {
// answer
Response<List<Produit>> response;
try {
response = new Response<List<Produit>>(0, null, application.getAllProduits());
} catch (DaoException e1) {
response = new Response<List<Produit>>(1003, e1.getErreurs(), null);
} catch (Exception e2) {
response = new Response<List<Produit>>(1003, getErreursForException(e2), null);
}
// answer jSON
return jsonMapperProduitWithoutCategorie.writeValueAsString(response);
}
- Zeile 1: Die URL [/getAllProduits] wird mit einer [GET]-Anfrage aufgerufen. Sie gibt JSON zurück;
- Zeile 2: Die Methode selbst sendet die JSON-Antwort an den Client;
- Zeile 5: Die Methode gibt eine JSON-Zeichenkette vom Typ [Response<List<Product>>] zurück;
- Zeile 7: Produkte werden ohne ihre Kategorie angefordert;
- Zeilen 8–12: Im Falle eines Fehlers wird die Antwort mit einem Fehlercode und Fehlermeldungen initialisiert;
- Zeile 14: Die JSON-Antwort wird an den Client gesendet;
13.5.5.6. Fazit
Auf die anderen Methoden des Controllers werden wir nicht eingehen. Sie ähneln der einen oder anderen der beiden Methoden, die wir gerade vorgestellt haben.
13.5.6. Die Webdienst-/JSON-Ausführungsklasse
![]() |
Die Klasse [Boot] ist die ausführbare Klasse des Projekts:
package spring.webjson.boot;
import org.springframework.boot.SpringApplication;
import spring.webjson.server.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
- Zeile 10: Die statische Methode [SpringApplication.run] wird ausgeführt. Die Klasse [SpringApplication] ist eine Klasse aus dem [Spring Boot]-Projekt (Zeile 3). Ihr werden zwei Parameter übergeben:
- [AppConfig.class]: die Klasse, die die gesamte Anwendung konfiguriert;
- [args]: alle Argumente, die in Zeile 9 an die Methode [main] übergeben wurden. Dieser Parameter wird hier nicht verwendet;
Wenn diese Klasse ausgeführt wird, werden die folgenden Protokolleinträge generiert:
- Zeilen 17–19: Tomcat-Server-Start zum Ausführen des Web-/JSON-Dienstes;
- Zeilen 25–33: Aufbau der [DAO]-Schicht;
- Zeilen 32–51: Die exponierten URLs werden ermittelt;
13.5.7. Webservice-/JSON-Tests
Um die Tests durchzuführen, generieren wir die MySQL-Datenbank [dbintrospringdata] aus dem SQL-Skript [dbintrospringdata.sql]:
![]() |
Sobald dies erledigt ist, verwenden wir den [Advanced Rest Client] (siehe Abschnitt 22.5), um die vom Webdienst / JSON bereitgestellten URLs abzufragen (der Webdienst / JSON muss dabei laufen).
![]() |
- In [1-3] rufen wir die URL [/getAllCategories] über eine HTTP-GET-Anfrage ab;
Wir erhalten die folgende Antwort:
![]() |
- In [1] die HTTP-Anfrage des Clients;
- in [2] die HTTP-Antwort des Servers;
- in [3] zeigt der Status [200 OK] an, dass der Server die Anfrage erfolgreich verarbeitet hat;
- in [4] die JSON-Antwort des Servers;
Die vollständige JSON-Antwort lautet wie folgt:
{"status":0,"messages":null,"body":[{"id":415,"version":0,"nom":"categorie0","produits":[{"id":1849,"version":0,"nom":"produit00","idCategorie":415,"prix":100.0,"description":"desc00"},{"id":1850,"version":0,"nom":"produit01","idCategorie":415,"prix":101.0,"description":"desc01"},{"id":1851,"version":0,"nom":"produit02","idCategorie":415,"prix":102.0,"description":"desc02"},{"id":1852,"version":0,"nom":"produit03","idCategorie":415,"prix":103.0,"description":"desc03"},{"id":1853,"version":0,"nom":"produit04","idCategorie":415,"prix":104.0,"description":"desc04"}]},{"id":416,"version":0,"nom":"categorie1","produits":[{"id":1856,"version":0,"nom":"produit12","idCategorie":416,"prix":112.0,"description":"desc12"},{"id":1857,"version":0,"nom":"produit13","idCategorie":416,"prix":113.0,"description":"desc13"},{"id":1858,"version":0,"nom":"produit14","idCategorie":416,"prix":114.0,"description":"desc14"},{"id":1854,"version":0,"nom":"produit10","idCategorie":416,"prix":110.0,"description":"desc10"},{"id":1855,"version":0,"nom":"produit11","idCategorie":416,"prix":111.0,"description":"desc11"}]}]}
- status:0 bedeutet, dass keine serverseitigen Fehler aufgetreten sind;
- messages: null bedeutet, dass keine Fehlermeldungen vorliegen;
- body: ist der Hauptteil der Antwort, in diesem Fall die Liste der Kategorien mit ihren Produkten. Es gibt zwei Kategorien mit jeweils 5 Produkten;
Wir fügen das Produkt [product15] zur Kategorie [category1] hinzu. Dazu verwenden wir die URL [/addCategories], die folgenden Code enthält:
@RequestMapping(value = "/addCategories", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8", produces = "application/json; charset=UTF-8")
@ResponseBody
public String addCategories(HttpServletRequest request) throws JsonProcessingException {
Response<List<Categorie>> response;
ObjectMapper mapper = context.getBean(ObjectMapper.class);
// we persist categories
try {
// retrieve the posted value
String body = CharStreams.toString(request.getReader());
mapper.setFilters(jsonFilterCategorieWithProduits);
List<Categorie> categories = mapper.readValue(body, new TypeReference<List<Categorie>>() {
});
// we re-establish the link between products and categories
for (Categorie categorie : categories) {
Set<Produit> produits = categorie.getProduits();
if (produits != null) {
for (Produit produit : categorie.getProduits()) {
produit.setCategorie(categorie);
}
}
}
// we persist categories
application.addCategories(categories);
response = new Response<List<Categorie>>(0, null, categories);
} catch (Exception e) {
response = new Response<List<Categorie>>(1004, getErreursForException(e), null);
}
// answer jSON
return mapper.writeValueAsString(response);
}
- Zeile 1: Der Client muss eine POST-Anfrage senden, und der übermittelte Wert muss eine JSON-Zeichenkette sein;
- Zeilen 9–12: Der übermittelte Wert muss eine Liste von Kategorien mit den zugehörigen Produkten sein;
Wir erstellen eine Kategorie [category2] mit einem Produkt [product21]. Die zu sendende JSON-Zeichenkette lautet dann wie folgt:
[{"id":null,"version":0,"nom":"categorie2","produits":[{"id":null,"version":0,"nom":"produit21","idCategorie":null,"prix":111.0,"description":"desc21"}]}]
Die Anfrage an den Webservice / JSON erfolgt wie folgt:
![]() |
- in [1] die angeforderte URL;
- in [2] wird sie über einen POST-Vorgang angefordert;
- in [3] die gesendete JSON-Zeichenkette;
- in [4] wird dem Server mitgeteilt, dass JSON-Daten gesendet werden;
Die Antwort des Servers lautet wie folgt:
![]() |
- In [1] sehen wir, dass sowohl die Kategorie als auch ihr Produkt nun über einen Primärschlüssel verfügen, was darauf hindeutet, dass sie wahrscheinlich in die Datenbank eingefügt wurden. Wir werden dies anhand der URL [/getCategorieByNameWithProduits/categorie2] überprüfen:
![]() |
Wir erhalten das folgende Ergebnis:
![]() |
Wir haben tatsächlich die Kategorie [categorie2] mit ihrem einzigen Produkt [produit21] abgerufen. Wir können auch nur das Produkt abfragen. Dazu verwenden wir die URL [/getProduitByIdWithoutCategorie/1859]:
![]() |
Wir erhalten das folgende Ergebnis:
![]() |
Alle [GET]-Operationen können in einem Standard-Webbrowser ausgeführt werden:
![]() |
Leser sind eingeladen, die anderen URLs des Webdienstes /json zu testen.
13.6. Ein für den Webdienst /json programmierter Client
Da die Datenbank [dbintrospringdata] nun im Web verfügbar ist, werden wir eine Anwendung schreiben, die sie nutzt. Wir werden dann die folgende Client-Server-Architektur haben:
![]() |
Die Client-Anwendung wird zwei Schichten haben:
- eine [DAO] [2]-Schicht zur Kommunikation mit der /json-Webanwendung, die die Datenbank bereitstellt;
- eine JUnit-Testschicht [1], um zu überprüfen, ob Client und Server korrekt funktionieren;
13.6.1. Das Eclipse-Projekt
Das Eclipse-Projekt des Clients sieht wie folgt aus:
![]() |
- Der Ordner [src/main/java] implementiert die [DAO]-Schicht;
- Der Ordner [src/test/java] enthält die JUnit-Tests;
13.6.2. Maven-Projektkonfiguration
Das Projekt ist ein Maven-Projekt, das durch die folgende [pom.xml]-Datei konfiguriert wird:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.webjson</groupId>
<artifactId>intro-client-webjson-01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Client console du serveur web / jSON</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.7.RELEASE</version>
</parent>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<!-- jSON library used by Spring -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- component used by Spring RestTemplate -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!-- Google Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
<scope>test</scope>
</dependency>
<!-- log library -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Spring Boot Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- plugins -->
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18.1</version>
</plugin>
</plugins>
</build>
<name>intro-client-webjson-01</name>
</project>
- Zeilen 14–18: das übergeordnete Maven-Projekt [spring-boot-starter-parent], das es uns ermöglicht, eine Reihe von Abhängigkeiten zu definieren, ohne deren Versionen anzugeben, da diese im übergeordneten Projekt definiert sind;
- Zeilen 22–25: Obwohl wir keine Webanwendung schreiben, benötigen wir die Abhängigkeit [spring-web], die die Klasse [RestTemplate] enthält, die eine einfache Anbindung an eine Web-/JSON-Anwendung ermöglicht;
- Zeilen 27–34: eine JSON-Bibliothek;
- Zeilen 36–39: eine Abhängigkeit, mit der wir ein Timeout für die HTTP-Anfragen des Clients festlegen können. Ein Timeout ist die maximale Wartezeit auf die Antwort des Servers. Nach Ablauf dieser Zeit signalisiert der Client einen Timeout-Fehler, indem er eine Ausnahme auslöst;
- Zeilen 41–46: die Google Guava-Bibliothek, die im JUnit-Test verwendet wird. Aus diesem Grund haben wir ihren Geltungsbereich auf [test] gesetzt (Zeile 45). Das bedeutet, dass diese Abhängigkeit nur einbezogen wird, wenn Code aus dem Zweig [src/test/java] ausgeführt wird;
- Zeilen 48–51: die Logging-Bibliothek;
- Zeilen 52–63: Die Abhängigkeiten für JUnit-Tests. Dazu gehört insbesondere die für die Tests erforderliche JUnit-4-Bibliothek. Diese Abhängigkeiten sind mit dem Attribut [<scope>test</scope>] versehen, was darauf hinweist, dass sie nur für die Testphase benötigt werden. Sie werden nicht in das endgültige Projektarchiv aufgenommen;
13.6.3. Implementierung der [DAO]-Schicht
![]() |
![]() |
- Das Paket [spring.client.config] enthält die Spring-Konfiguration für die [DAO]-Schicht;
- Das Paket [spring.client.dao] enthält die Implementierung der [DAO]-Schicht;
- Das Paket [spring.client.entities] enthält die Objekte, die mit dem Webservice / JSON ausgetauscht werden;
13.6.3.1. Konfiguration
![]() |
Die Klasse [DaoConfig] übernimmt die Spring-Konfiguration der [DAO]-Schicht. Der Code lautet wie folgt:
package spring.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@ComponentScan({ "spring.client.dao" })
public class DaoConfig {
// constants
static private final int TIMEOUT = 1000;
static private final String URL_WEBJSON = "http://localhost:8080";
@Bean
public RestTemplate restTemplate(int timeout) {
// creation of the RestTemplate component
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// exchange timeout
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
// result
return restTemplate;
}
@Bean
public int timeout() {
return TIMEOUT;
}
@Bean
public String urlWebJson() {
return URL_WEBJSON;
}
// filters jSON
@Bean(name = "jsonMapper")
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean(name = "jsonMapperCategorieWithProduits")
public ObjectMapper jsonMapperCategorieWithProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// result
return mapper;
}
@Bean(name = "jsonMapperProduitWithCategorie")
public ObjectMapper jsonMapperProduitWithCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(
new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
.addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// result
return mapper;
}
@Bean(name = "jsonMapperCategorieWithoutProduits")
public ObjectMapper jsonMapperCategorieWithoutProduits() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
SimpleBeanPropertyFilter.serializeAllExcept("produits")));
// result
return mapper;
}
@Bean(name = "jsonMapperProduitWithoutCategorie")
public ObjectMapper jsonMapperProduitWithoutCategorie() {
// mapper jSON
ObjectMapper mapper = new ObjectMapper();
// filters
mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
// result
return mapper;
}
}
- Zeile 13: Die Klasse ist eine Spring-Konfigurationsklasse – Spring-Komponenten befinden sich im Paket [spring.client.dao];
- Zeile 17: Es wird ein Timeout von einer Sekunde (1000 ms) festgelegt;
- Zeilen 32–35: Die Bean, die diesen Wert zurückgibt;
- Zeile 18: Die URL des Webdienstes / JSON;
- Zeilen 37–40: Die Bean, die diesen Wert zurückgibt;
- Zeilen 20–30: Die Konfiguration der [RestTemplate]-Klasse, die die Kommunikation mit dem Webservice / JSON übernimmt. Wenn keine Konfiguration erforderlich ist, kann sie im Code mit einem einfachen [new RestTemplate()] instanziiert werden. Hier möchten wir das Timeout für die Kommunikation mit dem Webservice / JSON festlegen. Die [timeout]-Bean in Zeile 36 wird als Parameter an die [RestTemplate]-Methode in Zeile 24 übergeben;
- Zeile 23: Die Komponente [HttpComponentsClientHttpRequestFactory] ermöglicht es uns, das Timeout für die Kommunikation festzulegen (Zeilen 29–30);
- Zeile 24: Die Klasse [RestTemplate] wird mithilfe dieser Komponente instanziiert. Da sie für die Kommunikation mit dem Webdienst / JSON auf diese Komponente angewiesen ist, unterliegen die Datenaustausche tatsächlich dem Timeout;
- Client und Server tauschen Textzeilen aus. Ein Konverter übernimmt die Serialisierung eines Objekts in Text und umgekehrt die Deserialisierung von Text in ein Objekt. Es können mehrere Konverter mit der [RestTemplate]-Klasse verknüpft sein, und welcher zu einem bestimmten Zeitpunkt ausgewählt wird, hängt von den vom Server gesendeten HTTP-Headern ab. Hier verwenden wir keinen Konverter. Daher wird die [RestTemplate]-Komponente nicht versuchen, die folgenden beiden Elemente in irgendeiner Weise zu konvertieren:
- den gesendeten Text;
- den als Antwort empfangenen Text;
Diese Texte sind JSON-Strings, die daher von der [RestTemplate]-Komponente unverändert belassen werden. Wir als Entwickler führen die erforderliche JSON-Serialisierung und -Deserialisierung selbst durch. Der Grund dafür ist, dass die Filter, die auf den gesendeten Wert und die empfangene Antwort angewendet werden sollen, unterschiedlich sein können, und die Erfahrung zeigt, dass es einfacher ist, diese selbst zu handhaben, als zu versuchen, die [RestTemplate]-Komponente so zu konfigurieren, dass sie den richtigen JSON-Konverter verwendet;
- Zeilen 42–92: Definieren von JSON-Filtern. Diese entsprechen den serverseitigen Filtern, die in Abschnitt 13.5.3.1 vorgestellt und erläutert wurden;
- Zeilen 43–46: ein JSON-Mapper ohne Filter;
- Zeilen 64–68: ein JSON-Mapper zum Abrufen einer Kategorie ohne deren Produkte;
- Zeilen 48–58: ein JSON-Mapper zum Abrufen einer Kategorie mit ihren Produkten;
- Zeilen 83–92: ein JSON-Mapper zum Abrufen eines Produkts ohne dessen Kategorie;
- Zeilen 60–70: ein JSON-Mapper zum Abrufen eines Produkts mit seiner Kategorie;
All diese Beans stehen sowohl dem Code der [DAO]-Schicht als auch dem JUnit-Test zur Verfügung.
13.6.3.2. Die Entitäten
![]() |
Die Entitäten, die von der [DAO]-Schicht verarbeitet werden, sind diejenigen, die sie mit dem Webservice / JSON austauscht. Dabei handelt es sich um die Artikel und Produkte. Auf der Serverseite waren diese Entitäten mit JPA-Persistenz-Annotationen versehen. Hier wurden diese Annotationen entfernt. Wir fügen den Entitätscode zur Veranschaulichung noch einmal ein:
[AbstractEntity]
package spring.client.entities;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public abstract class AbstractEntity {
// properties
protected Long id;
protected Long version;
// manufacturers
public AbstractEntity() {
}
public AbstractEntity(Long id, Long version) {
this.id = id;
this.version = version;
}
// redefine [equals] and [hashcode]
@Override
public int hashCode() {
return (id != null ? id.hashCode() : 0);
}
@Override
public boolean equals(Object entity) {
if (!(entity instanceof AbstractEntity)) {
return false;
}
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return id != null && this.id == other.id.longValue();
}
// signature jSON
public String toString() {
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(this);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
// getters and setters
...
}
[Kategorie]
package spring.client.entities;
import java.util.HashSet;
import java.util.Set;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {
// properties
private String nom;
// related products
public Set<Produit> produits = new HashSet<Produit>();
// manufacturers
public Categorie() {
}
public Categorie(String nom) {
this.nom = nom;
}
// methods
public void addProduit(Produit produit) {
// we add the product
produits.add(produit);
// set your category
produit.setCategorie(this);
}
// getters and setters
...
}
[Produkt]
package spring.webjson.client.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {
// the name
private String nom;
// category number
private Long idCategorie;
// the price
private double prix;
// the description
private String description;
// the category
private Categorie categorie;
// manufacturers
public Produit() {
}
public Produit(String nom, double prix, String description) {
this.nom = nom;
this.prix = prix;
this.description = description;
}
// getters and setters
...
}
13.6.3.3. Die Klasse [DaoException]
![]() |
Wenn in der [DAO]-Schicht ein Fehler auftritt, wird eine [DaoException] ausgelöst. Diese Klasse wird auf der Serverseite verwendet und ist in Abschnitt 11.3.7 beschrieben.
13.6.3.4. Die Schnittstelle der [DAO]-Schicht
![]() |
Die [DAO]-Schicht implementiert die in Abschnitt 11.3.7 beschriebene [IDao]-Schnittstelle.
package spring.client.dao;
import java.util.List;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
public interface IDao {
// insert product list
public List<Produit> addProduits(List<Produit> produits);
// removal of all products
public void deleteAllProduits();
// product list update
public List<Produit> updateProduits(List<Produit> produits);
// all products obtained
public List<Produit> getAllProduits();
// inserting a list of categories
public List<Categorie> addCategories(List<Categorie> categories);
// delete all categories
public void deleteAllCategories();
// updating a list of categories
public List<Categorie> updateCategories(List<Categorie> categories);
// obtaining all categories
public List<Categorie> getAllCategories();
// a special product
public Produit getProduitByIdWithCategorie(Long idProduit);
public Produit getProduitByIdWithoutCategorie(Long idProduit);
public Produit getProduitByNameWithCategorie(String nom);
public Produit getProduitByNameWithoutCategorie(String nom);
// a special category
public Categorie getCategorieByIdWithProduits(Long idCategorie);
public Categorie getCategorieByIdWithoutProduits(Long idCategorie);
public Categorie getCategorieByNameWithProduits(String nom);
public Categorie getCategorieByNameWithoutProduits(String nom);
}
13.6.3.5. Der Webservice / JSON-Antwort
![]() |
Wir haben gesehen, dass alle URLs des Webdienstes / JSON einen in Abschnitt 13.5.5.3 definierten Typ [Response] zurückgeben. Wir geben diese Klasse hier wieder:
package spring.client.dao;
import java.util.List;
public class Response<T> {
// ----------------- properties
// operation status
private int status;
// any error messages
private List<String> messages;
// the body of the reply
private T body;
// manufacturers
public Response() {
}
public Response(int status, List<String> messages, T body) {
this.status = status;
this.messages = messages;
this.body = body;
}
// getters and setters
...
}
13.6.3.6. Implementierung der Kommunikation mit dem Webdienst / JSON
![]() |
Die Klasse [ AbstractDao] implementiert die Kommunikation mit dem Webservice / JSON:
package spring.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.web.client.RestTemplate;
public abstract class AbstractDao {
// data
@Autowired
protected RestTemplate restTemplate;
@Autowired
protected String urlServiceWebJson;
// generic request
protected String getResponse(String url, String jsonPost) {
// url : URL to contact
// jsonPost: the jSON value to be posted
try {
// request execution
RequestEntity<?> request;
if (jsonPost != null) {
// query POST
request = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON).body(jsonPost);
} else {
// query GET
request = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
.accept(MediaType.APPLICATION_JSON).build();
}
// execute the query
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e1) {
throw new DaoException(20, e1);
} catch (RuntimeException e2) {
throw new DaoException(21, e2);
}
}
}
- Zeilen 15–16: Einbindung der [RestTemplate]-Komponente, die die Kommunikation mit dem Server übernimmt;
- Zeilen 17–18: Einbindung der Webservice-URL / JSON;
Die Implementierung der Methoden für die Kommunikation mit dem Server ist in der Methode [getResponse] zusammengefasst:
- Zeile 21: Die Methode erhält 2 Parameter:
- [url]: die angeforderte URL;
- [jsonPost]: die zu sendende JSON-Zeichenkette oder andernfalls null. Wenn [jsonPost == null] ist, erfolgt die URL-Anfrage per GET; andernfalls per POST;
- Zeile 38: Die Anweisung, die die Anfrage an den Server sendet und dessen Antwort empfängt. Die [RestTemplate]-Komponente bietet eine Vielzahl von Methoden für die Interaktion mit dem Server. Wir haben hier die [exchange]-Methode gewählt, es stehen jedoch auch andere zur Verfügung;
- Zeilen 27–36: Wir müssen die [RequestEntity]-Anfrage erstellen. Diese unterscheidet sich je nachdem, ob wir eine GET- oder eine POST-Anfrage verwenden;
- Zeilen 30–31: die Anfrage für einen GET. Die Klasse [RequestEntity] stellt statische Methoden zum Erstellen von GET-, POST-, HEAD- und anderen Anfragen bereit. Mit der Methode [RequestEntity.get] können Sie eine GET-Anfrage erstellen, indem Sie die verschiedenen Methoden, aus denen sie besteht, verketten:
- Die Methode [RequestEntity.get] nimmt die Ziel-URL als Parameter in Form einer URI-Instanz entgegen;
- die Methode [accept] ermöglicht es Ihnen, die Elemente des HTTP-Headers [Accept] zu definieren. Hier geben wir an, dass wir den Typ [application/json] akzeptieren, den der Server senden wird;
- die Methode [build] verwendet diese Informationen, um den Typ [RequestEntity] der Anfrage zu erstellen;
- Zeilen 34–35: die POST-Anfrage. Die Methode [RequestEntity.post] erstellt eine POST-Anfrage, indem sie die verschiedenen Methoden, aus denen sie besteht, miteinander verknüpft:
- Die Methode [RequestEntity.post] nimmt die Ziel-URL als Parameter in Form einer URI-Instanz entgegen,
- Die Methode [header] definiert einen HTTP-Header. Hier senden wir den Header [Content-Type: application/json] an den Server, um anzugeben, dass die übermittelten Daten in Form einer JSON-Zeichenkette ankommen werden;
- Die Methode [accept] ermöglicht es uns anzugeben, dass wir den Typ [application/json] akzeptieren, den der Server senden wird;
- Die Methode [body] legt den gesendeten Wert fest. Dies ist der vierte Parameter der generischen Methode [getResponse] (Zeile 1);
- Zeile 38: Die Methode [RestTemplate].exchange gibt einen Typ [ResponseEntity<String>] zurück, der die gesamte Serverantwort kapselt: HTTP-Header und Dokumenttext. Die Methode [ResponseEntity].getBody() ruft diesen Text ab, der die Antwort des Servers darstellt – in diesem Fall eine Zeichenkette;
13.6.3.7. Implementierung der [IDao]-Schnittstelle
![]() |
Die Klasse [Dao] implementiert die Schnittstelle [IDao]:
package spring.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
@Component
public class Dao extends AbstractDao implements IDao {
@Autowired
private ApplicationContext context;
// filters jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Override
public List<Produit> addProduits(List<Produit> produits) {
// ----------- add products (without category)
...
}
- Zeile 17: Die Klasse [Dao] ist eine Spring-Komponente, in die andere Spring-Komponenten injiziert werden können;
- Zeile 18: Die Klasse [Dao] erweitert die soeben vorgestellte Klasse [AbstractDao] und implementiert die Schnittstelle [IDao];
- Zeilen 20–21: Wir injizieren den Spring-Kontext, um auf dessen Beans zuzugreifen;
- Zeilen 24–38: Injektion der JSON-Mapper, die in der in Abschnitt 13.6.2 vorgestellten [AppConfig]-Klasse definiert sind;
Die Implementierungen der verschiedenen Methoden der [IDao]-Schnittstelle folgen alle demselben Muster. Wir stellen zwei Methoden vor, eine basierend auf einer [POST]-Operation, die andere auf einer [GET]-Operation.
Ein Beispiel für [GET]: [getCategorieByNameWithProduits]
@Override
public Categorie getCategorieByNameWithProduits(String nom) {
// ----------- obtain a category designated by its name, with its products
try {
// request
Response<Categorie> response = jsonMapperCategorieWithProduits.readValue(
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null),
new TypeReference<Response<Categorie>>() {
});
// mistake?
if (response.getStatus() != 0) {
// 1 exception is thrown
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// render the core of the server response
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(113, e2);
}
}
- Zeile 7: Die Methode [getResponse] der übergeordneten Klasse wird aufgerufen. Diese Methode übernimmt die Kommunikation mit dem Webdienst/JSON. Ihre Parameter lauten wie folgt:
getResponse(String.format("/getCategorieByNameWithProduits/%s", nom), null)
- (Fortsetzung)
- die URL des abgefragten Dienstes [/getCategoryByNameWithProducts/name];
- der übermittelte Wert. Hier gibt es keinen;
Die Methode [getResponse] gibt einen String zurück, der die vom Server gesendete JSON-Antwort darstellt. Wir deserialisieren diese JSON-Antwort wie folgt:
jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<Categorie>>() {
});
da die JSON-Zeichenkette die Serialisierung eines Typs [Response<Category>] ist;
- Zeilen 11–17: Wir prüfen den Antwortstatus. Ist der Status ungleich 0, lag ein serverseitiger Fehler vor. Wir lösen dann eine Ausnahme aus (Zeile 13) und verwenden dabei die in der Antwort enthaltenen Informationen (Status und Liste der Fehlermeldungen);
- Zeile 16: Wenn kein serverseitiger Fehler aufgetreten ist, geben wir den Body vom Typ [Response<Category>] zurück, d. h. die angeforderte Kategorie;
- Zeilen 18–19: Behandlung der in Zeile 16 ausgelösten Ausnahme;
- Zeilen 20–22: Alle anderen Ausnahmen werden behandelt;
Ein Beispiel für [POST]: [addCategories]
@Override
public List<Categorie> addCategories(List<Categorie> categories) {
// ----------- add categories (with their products)
try {
// request
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories)),
new TypeReference<Response<List<Categorie>>>() {
});
// mistake?
if (response.getStatus() != 0) {
// 1 exception is thrown
throw new DaoException(response.getStatus(), response.getMessages());
} else {
// render the core of the server response
return response.getBody();
}
} catch (DaoException e1) {
throw e1;
} catch (RuntimeException | IOException e2) {
throw new DaoException(104, e2);
}
}
- Zeile 2: Die Methode [addCategories] wird verwendet, um die als Parameter übergebenen Kategorien in der Datenbank zu speichern. Sie gibt dieselben Kategorien zurück, ergänzt um ihre Primärschlüssel. Wenn die Kategorien zusammen mit Produkten übergeben werden, werden diese ebenfalls gespeichert;
- Zeile 7: Die [getResponse]-Methode des übergeordneten Objekts wird aufgerufen, um die Kommunikation mit dem Webservice / JSON zu handhaben;
- Der erste Parameter ist die URL [/addCategories];
- der zweite Parameter ist der übermittelte Wert, in diesem Fall die Liste der zu speichernden Kategorien;
getResponse("/addCategories", jsonMapperCategorieWithProduits.writeValueAsString(categories))
Die resultierende JSON-Zeichenkette wird anschließend deserialisiert, um den erwarteten Typ [Response<List<Category>>] zu erhalten:
Response<List<Categorie>> response = jsonMapperCategorieWithProduits.readValue(
jsonResponse,
new TypeReference<Response<List<Categorie>>>() {
});
- Zeilen 11–17: Verarbeitung der Serverantwort (Fehler oder nicht);
- Zeilen 20–22: Ausnahmebehandlung;
Alle anderen Methoden folgen dem Muster der beiden vorgestellten Methoden.
13.6.4. Der JUnit-Test
Kehren wir nun zur derzeit in Entwicklung befindlichen Client/Server-Architektur zurück:
![]() |
Wir haben eine [DAO]-Schicht [2] mit derselben Schnittstelle wie die [DAO]-Schicht [4] erstellt. Um die [DAO]-Schicht [2] zu testen, können wir daher den JUnit-Test verwenden, der bereits zum Testen der [DAO]-Schicht [4] verwendet wurde. Zur Erinnerung: Er sieht wie folgt aus:
![]() |
package spring.client.junit;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import spring.client.config.DaoConfig;
import spring.client.dao.DaoException;
import spring.client.dao.IDao;
import spring.client.entities.Categorie;
import spring.client.entities.Produit;
@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {
// layer [DAO]
@Autowired
private IDao dao;
// filters jSON
@Autowired
@Qualifier("jsonMapper")
private ObjectMapper jsonMapper;
@Autowired
@Qualifier("jsonMapperCategorieWithProduits")
private ObjectMapper jsonMapperCategorieWithProduits;
@Autowired
@Qualifier("jsonMapperProduitWithCategorie")
private ObjectMapper jsonMapperProduitWithCategorie;
@Autowired
@Qualifier("jsonMapperCategorieWithoutProduits")
private ObjectMapper jsonMapperCategorieWithoutProduits;
@Autowired
@Qualifier("jsonMapperProduitWithoutCategorie")
private ObjectMapper jsonMapperProduitWithoutCategorie;
@Before
public void cleanAndFill() {
// the base is cleaned before each test
log("Vidage de la base de données", 1);
// table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
dao.deleteAllCategories();
// --------------------------------------------------------------------------------------
log("Remplissage de la base", 1);
// fill the tables
List<Categorie> categories = new ArrayList<Categorie>();
for (int i = 0; i < 2; i++) {
Categorie categorie = new Categorie(String.format("categorie%d", i));
for (int j = 0; j < 5; j++) {
categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
String.format("desc%d%d", i, j)));
}
categories.add(categorie);
}
// add the category - the products will be cascaded in as well
categories = dao.addCategories(categories);
}
@Test
public void showDataBase() throws BeansException, JsonProcessingException {
// list of categories
log("Liste des catégories", 2);
List<Categorie> categories = dao.getAllCategories();
affiche(categories, jsonMapperCategorieWithoutProduits);
// product list
log("Liste des produits", 2);
List<Produit> produits = dao.getAllProduits();
affiche(produits, jsonMapperProduitWithoutCategorie);
// a few checks
Assert.assertEquals(2, categories.size());
Assert.assertEquals(10, produits.size());
Categorie categorie = findCategorieByName("categorie0", categories);
Assert.assertNotNull(categorie);
Produit produit = findProduitByName("produit03", produits);
Assert.assertNotNull(produit);
Long idCategorie = produit.getIdCategorie();
Assert.assertEquals(categorie.getId(), idCategorie);
}
@Test
public void getCategorieByNameWithProduits() {
log("getCategorieByNameWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals(5, categorie1.getProduits().size());
}
@Test
public void getCategorieByNameWithoutProduits() {
log("getCategorieByNameWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
Assert.assertNotNull(categorie1);
Assert.assertEquals("categorie1", categorie1.getNom());
}
@Test
public void getCategorieByIdWithProduits() {
log("getCategorieByIdWithProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie2 = dao.getCategorieByIdWithProduits(categorie1.getId());
Assert.assertNotNull(categorie2);
Assert.assertEquals(categorie1.getId(), categorie2.getId());
Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
}
@Test
public void getCategorieByIdWithoutProduits() {
log("getCategorieByIdWithoutProduits", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie2 = dao.getCategorieByIdWithoutProduits(categorie1.getId());
Assert.assertNotNull(categorie2);
Assert.assertEquals(categorie1.getNom(), categorie2.getNom());
}
@Test
public void getProduitByNameWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Assert.assertNotNull(produit);
Assert.assertNotNull(produit.getCategorie());
}
@Test
public void getProduitByNameWithoutCategorie() {
log("getProduitByNameWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithoutCategorie("produit03");
Assert.assertNotNull(produit);
Assert.assertEquals("produit03", produit.getNom());
}
@Test
public void getProduitByIdWithCategorie() {
log("getProduitByNameWithCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
}
@Test
public void getProduitByIdWithoutCategorie() {
log("getProduitByIdWithoutCategorie", 1);
Produit produit = dao.getProduitByNameWithCategorie("produit03");
Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
Assert.assertNotNull(produit2);
Assert.assertEquals(produit2.getNom(), produit.getNom());
Assert.assertEquals(produit2.getId(), produit.getId());
}
@Test
public void doInsertsInTransaction() {
log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
// we insert
Categorie categorie = new Categorie("cat1");
categorie.addProduit(new Produit("x", 1.0, ""));
categorie.addProduit(new Produit("x", 1.0, ""));
// add the category - the products will be cascaded in as well
try {
categorie = dao.addCategories(Lists.newArrayList(categorie)).get(0);
} catch (DaoException e) {
show("Les erreurs suivantes se sont produites :", e.getErreurs());
}
// checks
List<Categorie> categories = dao.getAllCategories();
Assert.assertEquals(2, categories.size());
List<Produit> produits = dao.getAllProduits();
Assert.assertEquals(10, produits.size());
}
@Test
public void updateDataBase() {
log("Mise à jour du prix des produits de [categorie1]", 1);
Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
Categorie categorie1Saved = dao.getCategorieByNameWithProduits("categorie1");
Set<Produit> produits = categorie1.getProduits();
for (Produit produit : produits) {
produit.setPrix(1.1 * produit.getPrix());
}
List<Produit> produits2 = Lists.newArrayList(produits);
produits2 = dao.updateProduits(produits2);
// checks
List<Produit> produitsSaved = Lists.newArrayList(categorie1Saved.getProduits());
for (Produit produit2 : produits2) {
Produit produit = findProduitByName(produit2.getNom(), produitsSaved);
Assert.assertEquals(produit2.getPrix(), produit.getPrix() * 1.1, 1e-6);
}
}
@Test
public void addProduits() throws BeansException, JsonProcessingException {
log("Ajout de deux produits de catégorie [categorie0]", 1);
Categorie categorie0 = dao.getCategorieByNameWithoutProduits("categorie0");
Long idCategorie = categorie0.getId();
Produit p1 = new Produit("x", 1, "");
p1.setIdCategorie(idCategorie);
p1.setCategorie(categorie0);
Produit p2 = new Produit("y", 1, "");
p2.setIdCategorie(idCategorie);
p2.setCategorie(categorie0);
List<Produit> produits = new ArrayList<Produit>();
produits.add(p1);
produits.add(p2);
produits = dao.addProduits(produits);
// check
affiche(produits, jsonMapperProduitWithoutCategorie);
}
// -------------- private methods
private Produit findProduitByName(String nom, List<Produit> produits) {
for (Produit produit : produits) {
if (produit.getNom().equals(nom)) {
return produit;
}
}
return null;
}
private Categorie findCategorieByName(String nom, List<Categorie> categories) {
for (Categorie categorie : categories) {
if (categorie.getNom().equals(nom)) {
return categorie;
}
}
return null;
}
// display of a T-type element
static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
System.out.println(jsonMapper.writeValueAsString(element));
}
// display a list of elements of type T
static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
for (T element : elements) {
affiche(element, jsonMapper);
}
}
private static void log(String message, int mode) {
// poster message
String toPrint = null;
switch (mode) {
case 1:
toPrint = String.format("%s --------------------------------", message);
break;
case 2:
toPrint = String.format("-- %s", message);
break;
}
System.out.println(toPrint);
}
private static void show(String title, List<String> messages) {
// title
System.out.println(String.format("%s : ", title));
// messages
for (String message : messages) {
System.out.println(String.format("- %s", message));
}
}
}
Der Befehl wird erfolgreich ausgeführt und gibt auf der Konsole folgende Ergebnisse aus:
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout de deux produits de catégorie [categorie0] --------------------------------
{"id":6285,"version":0,"nom":"x","idCategorie":1319,"prix":1.0,"description":""}
{"id":6286,"version":0,"nom":"y","idCategorie":1319,"prix":1.0,"description":""}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Mise à jour du prix des produits de [categorie1] --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByNameWithoutProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByNameWithCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getProduitByIdWithoutCategorie --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
-- Liste des catégories
{"id":1337,"version":0,"nom":"categorie0"}
{"id":1338,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":6367,"version":0,"nom":"produit00","idCategorie":1337,"prix":100.0,"description":"desc00"}
{"id":6368,"version":0,"nom":"produit01","idCategorie":1337,"prix":101.0,"description":"desc01"}
{"id":6369,"version":0,"nom":"produit02","idCategorie":1337,"prix":102.0,"description":"desc02"}
{"id":6370,"version":0,"nom":"produit03","idCategorie":1337,"prix":103.0,"description":"desc03"}
{"id":6371,"version":0,"nom":"produit04","idCategorie":1337,"prix":104.0,"description":"desc04"}
{"id":6372,"version":0,"nom":"produit10","idCategorie":1338,"prix":110.0,"description":"desc10"}
{"id":6373,"version":0,"nom":"produit11","idCategorie":1338,"prix":111.0,"description":"desc11"}
{"id":6374,"version":0,"nom":"produit12","idCategorie":1338,"prix":112.0,"description":"desc12"}
{"id":6375,"version":0,"nom":"produit13","idCategorie":1338,"prix":113.0,"description":"desc13"}
{"id":6376,"version":0,"nom":"produit14","idCategorie":1338,"prix":114.0,"description":"desc14"}
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
getCategorieByIdWithProduits --------------------------------
Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites :
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
11:24:37.650 [Thread-1] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@f8c1ddd: startup date [Fri Nov 20 11:24:34 CET 2015]; root of context hierarchy



























































