8. Fallstudie
8.1. Einleitung
Wir schlagen vor, eine Webanwendung zur Terminplanung für eine Arztpraxis zu entwickeln. Dieses Problem wurde im Dokument „AngularJS / Spring 4 Tutorial“ unter der URL [http://tahe.developpez.com/angularjs-spring4/] behandelt. Die Architektur dieser Anwendung sah wie folgt aus:
![]() |
- In [1] liefert ein Webserver statische Seiten an einen Browser. Diese Seiten enthalten eine AngularJS-Anwendung, die auf dem MVC-Muster (Model–View–Controller) basiert. Das Modell umfasst hier sowohl die Ansichten als auch die Domäne, die hier durch die [Services]-Schicht dargestellt wird;
- Der Benutzer interagiert mit den Ansichten, die ihm im Browser angezeigt werden. Seine Aktionen erfordern manchmal eine Abfrage an den Spring 4-Server [2]. Der Server verarbeitet die Anfrage und gibt eine JSON-Antwort (JavaScript Object Notation) zurück [3]. Diese Antwort wird verwendet, um die dem Benutzer angezeigte Ansicht zu aktualisieren.
Wir schlagen vor, diese Anwendung zu übernehmen und sie durchgängig mit Spring MVC zu implementieren. Die Architektur sieht dann wie folgt aus:
![]() |
Der Browser stellt eine Verbindung zu einer mit Spring MVC implementierten [Web 1]-Anwendung her, die ihre Daten von einem ebenfalls mit Spring MVC implementierten [Web 2]-Webdienst abruft.
8.2. Funktionen der Anwendung
Leser sind eingeladen, die Funktionen der Anwendung durch Ausprobieren zu erkunden. Wir laden die Maven-Projekte aus dem Ordner [case-study] in STS:
![]() | ![]() |
Zunächst erstellen wir die MySQL-5-Datenbank [dbrdvmedecins] mit dem Tool [Wamp Server] (siehe Abschnitt 9.5):
![]() |
- Wählen Sie in [1] das Tool [phpMyAdmin] aus WampServer aus;
- Wählen Sie in [2] die Option [Importieren] aus;
![]() |
- Wählen Sie in [3] die Datei [database/dbrdvmedecins.sql] aus;
- führen Sie sie in [4] aus;
- in [5] wird die Datenbank erstellt.
Als Nächstes müssen wir den mit der Datenbank verbundenen Server starten. Dies ist das Projekt [rdvmedecins-webjson-server]
![]() |
Der Server ist unter der URL [http://localhost:8080] erreichbar. Dies kann in der Datei [application.properties] des Projekts geändert werden:
![]() |
server.port=8080
Die Anmeldedaten für den Datenbankzugriff sind in der Klasse [DomainAndPersistenceConfig] des Projekts [rdvmedecins-metier-dao] gespeichert:
![]() |
// the MySQL data source
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
return dataSource;
}
Wenn Sie mit anderen Anmeldedaten auf die MySQL-Datenbank zugreifen, nehmen Sie die Änderungen hier vor.
Als Nächstes starten wir, genau wie beim vorherigen Server, den Server [rdvmedecins-springthymeleaf-server]:
![]() | ![]() |
Dieser Server ist standardmäßig unter der URL [http://localhost:8081] erreichbar. Auch dies kann in der Datei [application.properties] des Projekts konfiguriert werden:
server.port=8081
Außerdem muss dieser Server die URL des mit der Datenbank verbundenen Servers kennen. Diese Konfiguration findet sich in der oben genannten [AppConfig]-Klasse:
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// racine service web / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// timeout en millisecondes
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
Wenn der erste Server auf einem anderen Port als 8080 gestartet wurde, müssen Sie Zeile 5 ändern.
Rufen Sie anschließend mit einem Browser die URL [http://localhost:8081/boot.html] auf:
![]() |
- in [1] die Anmeldeseite der Anwendung;
- in [2] und [3] den Benutzernamen und das Passwort des Benutzers, der die Anwendung nutzen möchte. Es gibt zwei Benutzer: admin/admin (Benutzername/Passwort) mit der Rolle (ADMIN) und user/user mit der Rolle (USER). Nur die Rolle ADMIN verfügt über die Berechtigung, die Anwendung zu nutzen. Die Rolle USER dient in diesem Anwendungsfall lediglich dazu, die Reaktion des Servers zu veranschaulichen;
- in [4] die Schaltfläche, über die Sie eine Verbindung zum Server herstellen können;
- in [5] die Sprache der Anwendung. Es gibt zwei Optionen: Französisch (Standard) und Englisch;
- in [6] die Server-URL [rdvmedecins-springthymeleaf-server];
![]() |
- unter [1] melden Sie sich an;
![]() |
- Sobald Sie angemeldet sind, können Sie den gewünschten Arzt [2] und den Termin [3] auswählen. Sobald ein Arzt und ein Termin ausgewählt wurden, wird der Kalender automatisch angezeigt:
![]() |
- Sobald der Kalender des Arztes angezeigt wird, können Sie einen Termin buchen [5];
![]() |
- Wählen Sie unter [6] den Patienten für den Termin aus und bestätigen Sie Ihre Auswahl unter [7];
![]() |
Sobald der Termin bestätigt ist, gelangen Sie automatisch zurück zum Kalender, wo der neue Termin nun aufgeführt ist. Dieser Termin kann später gelöscht werden [8].
Die wichtigsten Funktionen wurden beschrieben. Sie sind einfach. Schließen wir mit den Spracheinstellungen ab:

- Unter [1] wechseln Sie von Französisch zu Englisch;
![]() |
- In [2] wechselt die Ansicht zu Englisch, einschließlich des Kalenders;
8.3. Die Datenbank
![]() |
Die Datenbank, im Folgenden als [dbrdvmedecins] bezeichnet, ist eine MySQL5-Datenbank mit den folgenden Tabellen:
![]() |
Termine werden über die folgenden Tabellen verwaltet:
- [doctors]: enthält die Liste der Ärzte der Praxis;
- [clients]: enthält die Liste der Patienten der Praxis;
- [slots]: enthält die Zeitfenster für jeden Arzt;
- [rv]: enthält die Liste der Arzttermine.
Die Tabellen [roles], [users] und [users_roles] beziehen sich auf die Authentifizierung. Diese werden wir vorerst nicht behandeln. Die Beziehungen zwischen den Tabellen zur Verwaltung von Terminen sind wie folgt:
![]() |
- Ein Zeitfenster gehört zu einem Arzt – ein Arzt hat 0 oder mehr Zeitfenster;
- Ein Termin bringt einen Kunden und einen Arzt über den Zeitfenster des Arztes zusammen;
- Ein Kunde hat 0 oder mehr Termine;
- ein Zeitfenster ist mit 0 oder mehr Terminen (an verschiedenen Tagen) verknüpft.
8.3.1. Die Tabelle [DOCTORS]
Sie enthält Informationen zu den Ärzten, die von der Anwendung [RdvMedecins] verwaltet werden.
![]() | ![]() |
- ID: Nummer zur Identifizierung des Arztes – Primärschlüssel der Tabelle
- VERSION: Nummer, die die Version der Zeile in der Tabelle identifiziert. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
- LAST_NAME: der Nachname des Arztes
- FIRST_NAME: Vorname des Arztes
- TITLE: Anrede (Frau, Frau, Herr)
8.3.2. Die Tabelle [CLIENTS]
Die Patienten der verschiedenen Ärzte werden in der Tabelle [CLIENTS] gespeichert:
![]() | ![]() |
- ID: ID-Nummer zur Identifizierung des Kunden – Primärschlüssel der Tabelle
- VERSION: Nummer, die die Version der Zeile in der Tabelle identifiziert. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
- LAST NAME: Der Nachname des Kunden
- VORNAME: Der Vorname des Kunden
- TITLE: Anrede (Frau, Frau, Herr)
8.3.3. Die Tabelle [SLOTS]
Sie listet die Zeitfenster auf, in denen Termine verfügbar sind:
![]() |
![]() |
- ID: ID-Nummer für den Zeitblock – Primärschlüssel der Tabelle (Zeile 8)
- VERSION: Nummer, die die Version der Zeile in der Tabelle angibt. Diese Nummer wird bei jeder Änderung an der Zeile um 1 erhöht.
- DOCTOR_ID: ID-Nummer, die den Arzt identifiziert, zu dem dieses Zeitfenster gehört – Fremdschlüssel auf der Spalte DOCTORS(ID).
- START_TIME: Startzeit des Zeitfensters
- MSTART: Startminute des Zeitfensters
- HFIN: Endzeit des Zeitfensters
- MFIN: Endminute des Zeitfensters
Die zweite Zeile der Tabelle [SLOTS] (siehe [1] oben) gibt beispielsweise an, dass Zeitfenster Nr. 2 um 8:20 Uhr beginnt und um 8:40 Uhr endet und der Ärztin Nr. 1 (Frau Marie PELISSIER) zugeordnet ist.
8.3.4. Die Tabelle [RV]
Sie listet die für jeden Arzt gebuchten Termine auf:
![]() |
- ID: eindeutige Kennung für den Termin – Primärschlüssel
- DAY: Tag des Termins
- SLOT_ID: Terminzeitfenster – Fremdschlüssel auf das Feld [ID] der Tabelle [SLOTS] – bestimmt sowohl das Zeitfenster als auch den beteiligten Arzt.
- CLIENT_ID: ID des Kunden, für den die Reservierung vorgenommen wurde – Fremdschlüssel auf dem Feld [ID] der Tabelle [CLIENTS]
Diese Tabelle verfügt über eine Eindeutigkeitsbeschränkung für die Werte der verknüpften Spalten (DAY, SLOT_ID):
Wenn eine Zeile in der Tabelle [RV] den Wert (DAY1, SLOT_ID1) für die Spalten (DAY, SLOT_ID) enthält, darf dieser Wert an keiner anderen Stelle vorkommen. Andernfalls würde dies bedeuten, dass zwei Termine zur gleichen Zeit für denselben Arzt gebucht wurden. Aus Sicht der Java-Programmierung löst der JDBC-Treiber der Datenbank in diesem Fall eine SQLException aus.
Die Zeile mit der ID 3 (siehe [1] oben) bedeutet, dass am 23.08.2006 ein Termin für Slot Nr. 20 und Kunde Nr. 4 gebucht wurde. Die Tabelle [SLOTS] gibt an, dass Slot Nr. 20 dem Zeitfenster 16:20 – 16:40 Uhr entspricht und zur Ärztin Nr. 1 (Frau Marie PELISSIER) gehört. Die Tabelle [CLIENTS] zeigt uns, dass Patient Nr. 4 Frau Brigitte BISTROU ist.
8.3.5. Erstellen der Datenbank
Zum Erstellen der Datenbank [dbrdvmedecins] wird ein Skript [dbrdvmedecins.sql] mit den Beispielen in diesem Dokument bereitgestellt [1-3]:
![]() |
Wir verwenden das Tool [PhpMyAdmin] von WampServer:
![]() |
- Wählen Sie in [1] das Tool [phpMyAdmin] von WampServer aus;
- Wählen Sie unter [2] die Option [Importieren] aus;
![]() |
- Wählen Sie in [3] die Datei [database/dbrdvmedecins.sql] aus;
- Führen Sie sie in [4] aus;
- in [5] wird die Datenbank erstellt.
8.4. Der Webdienst / JSON
![]() |
In der obigen Architektur befassen wir uns nun mit der Erstellung des Webdienstes / JSON, der mit dem Spring MVC-Framework aufgebaut wurde. Wir werden dies in mehreren Schritten schreiben:
- zunächst die [Business]- und [DAO]-Schichten (Data Access Object). Wir werden hier Spring Data verwenden;
- Als Nächstes den JSON-Webdienst ohne Authentifizierung. Hier verwenden wir Spring MVC;
- Dann fügen wir die Authentifizierungskomponente mithilfe von Spring Security hinzu.
Im Folgenden finden Sie eine Wiedergabe des Dokuments [http://tahe.developpez.com/angularjs-spring4/] mit einigen Änderungen.
8.4.1. Einführung in Spring Data
Wir werden die [DAO]-Schicht des Projekts mit Spring Data, einer Komponente des Spring-Ökosystems, implementieren.
![]() |
Die Spring-Website bietet zahlreiche Tutorials für den Einstieg in Spring [http://spring.io/guides]. Wir werden eines davon nutzen, um Spring Data vorzustellen. Dazu verwenden wir die Spring Tool Suite (STS).
![]() |
- In [1] importieren wir eines der Tutorials von [spring.io/guides];
![]() |
- In [2] wählen wir das Tutorial [Accessing Data JPA] aus, das zeigt, wie man mit Spring Data auf eine Datenbank zugreift;
- In [3] wählen wir ein von Maven konfiguriertes Projekt aus;
- In [4] ist das Tutorial in zwei Formen verfügbar: [initial], eine leere Version, die Sie gemäß dem Tutorial ausfüllen, oder [complete], die endgültige Version des Tutorials. Wir wählen Letzteres;
- In [5] können Sie das Tutorial in einem Browser anzeigen;
- In [6] das fertige Projekt.
8.4.1.1. Die Maven-Konfiguration des Projekts
Die Maven-Abhängigkeiten des Projekts sind in der Datei [pom.xml] konfiguriert:
<groupId>org.springframework</groupId>
<artifactId>gs-accessing-data-jpa</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.10.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
<properties>
<!-- use UTF-8 for everything -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>hello.Application</start-class>
</properties>
- Zeilen 5–9: Definieren Sie ein übergeordnetes Maven-Projekt. Dieses Projekt definiert die meisten Abhängigkeiten des Projekts. Diese können ausreichend sein, in diesem Fall werden keine zusätzlichen Abhängigkeiten hinzugefügt, oder sie können unzureichend sein, in diesem Fall werden die fehlenden Abhängigkeiten hinzugefügt;
- Zeilen 12–15: Definieren eine Abhängigkeit von [spring-boot-starter-data-jpa]. Dieses Artefakt enthält die Spring-Data-Klassen;
- Zeilen 16–19: Definieren eine Abhängigkeit vom H2-DBMS, mit dem Sie In-Memory-Datenbanken erstellen und verwalten können.
Sehen wir uns die von diesen Abhängigkeiten bereitgestellten Klassen an:
![]() | ![]() | ![]() |
Es gibt viele davon:
- Einige gehören zum Spring-Ökosystem (diejenigen, die mit „spring“ beginnen);
- andere sind Teil des Hibernate-Ökosystems (Hibernate, JBoss), und wir verwenden hier die JPA-Implementierung;
- wieder andere sind Testbibliotheken (JUnit, Hamcrest);
- wieder andere sind Logging-Bibliotheken (log4j, logback, slf4j);
Wir werden sie alle behalten. Für eine Produktionsanwendung sollten nur die notwendigen beibehalten werden.
In Zeile 26 der Datei [pom.xml] finden wir die Zeile:
<start-class>hello.Application</start-class>
Diese Zeile ist mit den folgenden Zeilen verknüpft:
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Zeilen 6–9: Mit dem [spring-boot-maven-plugin] können Sie die ausführbare JAR-Datei der Anwendung generieren. In Zeile 26 der Datei [pom.xml] wird dann die ausführbare Klasse dieser JAR-Datei angegeben.
8.4.1.2. Die [JPA]-Schicht
Der Datenbankzugriff wird über eine [JPA]-Schicht, die Java Persistence API, abgewickelt:
![]() |
![]() |
Die Anwendung ist einfach aufgebaut und verwaltet [Customer]-Entitäten. Die [Customer]-Klasse ist Teil der [JPA]-Schicht und sieht wie folgt aus:
package hello;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
private String firstName;
private String lastName;
protected Customer() {
}
public Customer(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
@Override
public String toString() {
return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
}
}
Ein Kunde hat eine ID [id], einen Vornamen [firstName] und einen Nachnamen [lastName]. Jede [Customer]-Instanz repräsentiert eine Zeile in einer Datenbanktabelle.
- Zeile 8: JPA-Annotation, die sicherstellt, dass die Persistenz von [Customer]-Instanzen (Erstellen, Lesen, Aktualisieren, Löschen) von einer JPA-Implementierung verwaltet wird. Anhand der Maven-Abhängigkeiten lässt sich erkennen, dass die JPA/Hibernate-Implementierung verwendet wird;
- Zeilen 11–12: JPA-Annotationen, die das Feld [id] mit dem Primärschlüssel der Tabelle [Customer] verknüpfen. Zeile 12 gibt an, dass die JPA-Implementierung die für das verwendete DBMS spezifische Methode zur Primärschlüsselgenerierung verwendet, in diesem Fall H2;
Es gibt keine weiteren JPA-Annotationen. Daher werden Standardwerte verwendet:
- Die Tabelle [Customer] wird nach der Klasse benannt, d. h. [Customer];
- die Spalten dieser Tabelle tragen die Namen der Klassenfelder: [id, firstName, lastName], wobei zu beachten ist, dass bei den Spaltennamen der Groß-/Kleinschreibung keine Bedeutung zukommt;
Beachten Sie, dass die verwendete JPA-Implementierung nie erwähnt wird.
8.4.1.3. Die [DAO]-Schicht
![]() |
![]() |
Die Klasse [CustomerRepository] implementiert die [DAO]-Schicht. Ihr Code lautet wie folgt:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
Es handelt sich also um eine Schnittstelle und nicht um eine Klasse (Zeile 7). Sie erweitert die Schnittstelle [CrudRepository], eine Spring-Data-Schnittstelle (Zeile 5). Diese Schnittstelle wird durch zwei Typen parametrisiert: Der erste ist der Typ der verwalteten Elemente, hier der Typ [Customer]; der zweite ist der Typ des Primärschlüssels der verwalteten Elemente, hier ein Typ [Long]. Die Schnittstelle [CrudRepository] sieht wie folgt aus:
package org.springframework.data.repository;
import java.io.Serializable;
@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {
<S extends T> S save(S entity);
<S extends T> Iterable<S> save(Iterable<S> entities);
T findOne(ID id);
boolean exists(ID id);
Iterable<T> findAll();
Iterable<T> findAll(Iterable<ID> ids);
long count();
void delete(ID id);
void delete(T entity);
void delete(Iterable<? extends T> entities);
void deleteAll();
}
Diese Schnittstelle definiert die CRUD-Operationen (Create – Read – Update – Delete), die für einen JPA-Typ vom Typ T ausgeführt werden können:
- Zeile 8: Mit der Methode „save“ können Sie eine T-Entität in der Datenbank persistieren. Dabei wird die Entität unter Verwendung des ihr vom DBMS zugewiesenen Primärschlüssels persistiert. Außerdem können Sie damit eine T-Entität aktualisieren, die durch ihre Primärschlüssel-ID identifiziert wird. Die Wahl zwischen diesen beiden Aktionen hängt vom Wert der Primärschlüssel-ID ab: Ist dieser null, wird die Persistenzoperation ausgeführt; andernfalls erfolgt die Aktualisierungsoperation;
- Zeile 10: Wie oben, jedoch für eine Liste von Entitäten;
- Zeile 12: Die Methode `findOne` ruft eine Entität T ab, die durch ihre Primärschlüssel-ID identifiziert wird;
- Zeile 22: Mit der Methode delete können Sie eine Entität T löschen, die durch ihre Primärschlüssel-ID identifiziert wird;
- Zeilen 24–28: Varianten der Methode [delete];
- Zeile 16: Die Methode [findAll] ruft alle persistierten T-Entitäten ab;
- Zeile 18: wie oben, jedoch beschränkt auf Entitäten, für die eine Liste von Identifikatoren angegeben wurde;
Kehren wir zur Schnittstelle [CustomerRepository] zurück:
package hello;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByLastName(String lastName);
}
- Zeile 9 ermöglicht es Ihnen, einen [Kunden] anhand seines [Nachnamens] abzurufen;
Und das war’s auch schon für die [DAO]-Schicht. Es gibt keine Implementierungsklasse für die vorherige Schnittstelle. Sie wird zur Laufzeit von [Spring Data] generiert. Die Methoden der [CrudRepository]-Schnittstelle werden automatisch implementiert. Bei den Methoden, die der [CustomerRepository]-Schnittstelle hinzugefügt wurden, kommt es darauf an. Kehren wir zur Definition von [Customer] zurück:
private long id;
private String firstName;
private String lastName;
Die Methode in Zeile 9 wird von [Spring Data] automatisch implementiert, da sie auf das Feld [lastName] (Zeile 3) von [Customer] verweist. Wenn Spring Data in der zu implementierenden Schnittstelle auf eine [findBySomething]-Methode stößt, implementiert es diese mithilfe der folgenden JPQL-Abfrage (Java Persistence Query Language):
Daher muss der Typ T ein Feld namens [something] haben. Somit ist die Methode
wird mit einem Code implementiert, der in etwa wie folgt aussieht:
return [em].createQuery("select c from Customer c where c.lastName=:value").setParameter("value",lastName).getResultList()
wobei [em] für den JPA-Persistenzkontext steht. Dies ist nur möglich, wenn die Klasse [Customer] ein Feld namens [lastName] enthält, was der Fall ist.
Zusammenfassend lässt sich sagen, dass Spring Data es uns in einfachen Fällen ermöglicht, die [DAO]-Schicht mit einer einfachen Schnittstelle zu implementieren.
8.4.1.4. Die [console]-Schicht
![]() |
![]() |
Die Klasse [Application] sieht wie folgt aus:
package hello;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
// save a couple of customers
repository.save(new Customer("Jack", "Bauer"));
repository.save(new Customer("Chloe", "O'Brian"));
repository.save(new Customer("Kim", "Bauer"));
repository.save(new Customer("David", "Palmer"));
repository.save(new Customer("Michelle", "Dessler"));
// fetch all customers
Iterable<Customer> customers = repository.findAll();
System.out.println("Customers found with findAll():");
System.out.println("-------------------------------");
for (Customer customer : customers) {
System.out.println(customer);
}
System.out.println();
// fetch an individual customer by ID
Customer customer = repository.findOne(1L);
System.out.println("Customer found with findOne(1L):");
System.out.println("--------------------------------");
System.out.println(customer);
System.out.println();
// fetch customers by last name
List<Customer> bauers = repository.findByLastName("Bauer");
System.out.println("Customer found with findByLastName('Bauer'):");
System.out.println("--------------------------------------------");
for (Customer bauer : bauers) {
System.out.println(bauer);
}
context.close();
}
}
- Zeile 10: gibt an, dass die Klasse zur Konfiguration von Spring verwendet wird. Neuere Versionen von Spring können tatsächlich in Java statt in XML konfiguriert werden. Beide Methoden können gleichzeitig verwendet werden. Im Code einer mit [Configuration] annotierten Klasse finden wir normalerweise Spring-Beans, d. h. Klassendefinitionen, die instanziiert werden sollen. Hier sind keine Beans definiert. Es ist wichtig zu beachten, dass bei der Arbeit mit einem DBMS verschiedene Spring-Beans definiert werden müssen:
- eine [EntityManagerFactory], die die zu verwendende JPA-Implementierung definiert,
- eine [DataSource], die die zu verwendende Datenquelle definiert,
- eine [TransactionManager], die den zu verwendenden Transaktionsmanager definiert;
Hier ist keines dieser Beans definiert.
- Zeile 11: Die Annotation [EnableAutoConfiguration] stammt aus dem [Spring Boot]-Projekt (Zeilen 5–6). Diese Annotation weist Spring Boot über die Klasse [SpringApplication] (Zeile 16) an, die Anwendung auf der Grundlage der in ihrem Klassenpfad gefundenen Bibliotheken zu konfigurieren. Da sich die Hibernate-Bibliotheken im Klassenpfad befinden, wird die Bean [entityManagerFactory] mit Hibernate implementiert. Da sich die H2-DBMS-Bibliothek im Klassenpfad befindet, wird die Bean [ ] mit H2 implementiert. In der [dataSource]-Bean müssen wir außerdem den Benutzernamen und das Passwort definieren. Hier verwendet Spring Boot den Standard-H2-Administrator, der kein Passwort hat. Da sich die [spring-tx]-Bibliothek im Klassenpfad befindet, wird der Transaktionsmanager von Spring verwendet.
Zusätzlich wird das Verzeichnis, das die [Application]-Klasse enthält, nach Beans durchsucht, die von Spring implizit erkannt oder explizit durch Spring-Annotationen definiert wurden. Somit werden die Klassen [Customer] und [CustomerRepository] überprüft. Da die erste die Annotation [@Entity] trägt, wird sie als Entität katalogisiert, die von Hibernate verwaltet wird. Da die zweite die Schnittstelle [CrudRepository] erweitert, wird sie als Spring-Bean registriert.
Betrachten wir die Zeilen 16–17 des Codes:
ConfigurableApplicationContext context = SpringApplication.run(Application.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
- Zeile 16: Die statische Methode [run] der Klasse [SpringApplication] im Spring-Boot-Projekt wird ausgeführt. Ihr Parameter ist die Klasse, die mit der Annotation [Configuration] oder [EnableAutoConfiguration] versehen ist. Daraufhin werden alle zuvor erläuterten Schritte ausgeführt. Das Ergebnis ist ein Spring-Anwendungskontext, d. h. eine Reihe von Beans, die von Spring verwaltet werden;
- Zeile 17: Wir fordern von diesem Spring-Kontext eine Bean an, die die Schnittstelle [CustomerRepository] implementiert. Hier greifen wir auf die von Spring Data generierte Klasse zurück, um diese Schnittstelle zu implementieren.
Die folgenden Operationen nutzen lediglich die Methoden des Beans, der die Schnittstelle [CustomerRepository] implementiert. Beachten Sie in Zeile 50, dass der Kontext geschlossen wird. Die Konsolenausgabe lautet wie folgt:
- Zeilen 1–8: das Spring Boot-Projektlogo;
- Zeile 9: Die Klasse [hello.Application] wird ausgeführt;
- Zeile 10: [AnnotationConfigApplicationContext] ist eine Klasse, die die [ApplicationContext]-Schnittstelle von Spring implementiert. Es handelt sich um einen Bean-Container;
- Zeile 11: Die Bean [entityManagerFactory] wird mithilfe der Klasse [LocalContainerEntityManagerFactory], einer Spring-Klasse, implementiert;
- Zeile 15: [Hibernate] erscheint. Dies ist die gewählte JPA-Implementierung;
- Zeile 19: Ein Hibernate-Dialekt ist die SQL-Variante, die mit dem DBMS verwendet werden soll. Hier gibt der Dialekt [H2Dialect] an, dass Hibernate mit dem H2-DBMS arbeiten wird;
- Zeilen 21–22: Die Datenbank wird angelegt. Die Tabelle [CUSTOMER] wird angelegt. Das bedeutet, dass Hibernate so konfiguriert wurde, dass es Tabellen aus JPA-Definitionen generiert, in diesem Fall aus der JPA-Definition der Klasse [Customer];
- Zeilen 27–31: Die fünf Kunden werden eingefügt;
- Zeilen 33–35: Ergebnis der Methode [findOne] der Schnittstelle;
- Zeilen 37–40: Ergebnisse der Methode [findByLastName];
- Zeilen 41 ff.: Protokolle aus dem Spring-Kontext.
8.4.1.5. Manuelle Konfiguration des Spring Data-Projekts
Wir duplizieren das vorherige Projekt in das Projekt [gs-accessing-data-jpa-2]:
![]() |
In diesem neuen Projekt werden wir nicht auf die von Spring Boot bereitgestellte automatische Konfiguration zurückgreifen. Wir werden sie manuell konfigurieren. Dies kann nützlich sein, wenn die Standardkonfigurationen nicht unseren Anforderungen entsprechen.
Zunächst werden wir die erforderlichen Abhängigkeiten in der Datei [pom.xml] angeben:
...
<dependencies>
<!-- Spring Core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring transactions -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring ORM -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.1.2.RELEASE</version>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.7.1.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.1.10.RELEASE</version>
</dependency>
<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>4.3.4.Final</version>
</dependency>
<!-- H2 Database -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.178</version>
</dependency>
<!-- Commons DBCP -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
<version>1.6</version>
</dependency>
</dependencies>
...
</project>
- Zeilen 2–18: Spring-Kernbibliotheken;
- Zeilen 19–29: Spring-Bibliotheken zur Verwaltung von Datenbanktransaktionen;
- Zeilen 30–35: die Spring-Bibliothek für die Arbeit mit einem ORM (Object Relational Mapper);
- Zeilen 36–41: Spring Data für den Zugriff auf die Datenbank;
- Zeilen 42–47: Spring Boot zum Starten der Anwendung;
- Zeilen 54–59: das H2-DBMS;
- Zeilen 60–70: Datenbanken werden häufig mit Verbindungspools verwendet, wodurch das wiederholte Öffnen und Schließen von Verbindungen vermieden wird. Hier wird die Implementierung [commons-dbcp] verwendet;
Ebenfalls in [pom.xml] ändern wir den Namen der ausführbaren Klasse:
<properties>
...
<start-class>demo.console.Main</start-class>
</properties>
Im neuen Projekt bleiben die Entität [Customer] und die Schnittstelle [CustomerRepository] unverändert. Wir werden die Klasse [Application] ändern, die in zwei Klassen aufgeteilt wird:
- [Config], die als Konfigurationsklasse dient:
- [Main], die die ausführbare Klasse sein wird;
![]() |
Die ausführbare Klasse [Main] ist dieselbe wie zuvor, ohne die Konfigurationsanmerkungen:
package demo.console;
import java.util.List;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
...
context.close();
}
}
- Zeile 12: Die Klasse [Main] enthält keine Konfigurationsanmerkungen mehr;
- Zeile 16: Die Anwendung wird mit Spring Boot gestartet. Der Parameter [Config.class] ist die neue Konfigurationsklasse des Projekts;
Die [Config]-Klasse, die das Projekt konfiguriert, sieht wie folgt aus:
package demo.config;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
//@ComponentScan(basePackages = { "demo" })
//@EntityScan(basePackages = { "demo.entities" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "demo.repositories" })
@Configuration
public class Config {
// h2 data source
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("org.h2.Driver");
dataSource.setUrl("jdbc:h2:./demo");
dataSource.setUsername("sa");
dataSource.setPassword("");
return dataSource;
}
// the provider JPA
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(true);
hibernateJpaVendorAdapter.setDatabase(Database.H2);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan("demo.entities");
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Transaction manager
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- Zeile 22: Die Annotation [@Configuration] macht die Klasse [Config] zu einer Spring-Konfigurationsklasse;
- Zeile 21: Die Annotation [@EnableJpaRepositories] gibt die Verzeichnisse an, in denen sich die Spring Data [CrudRepository]-Schnittstellen befinden. Diese Schnittstellen werden zu Spring-Komponenten und stehen in ihrem Kontext zur Verfügung;
- Zeile 20: Die Annotation [@EnableTransactionManagement] gibt an, dass die Methoden der [CrudRepository]-Schnittstellen innerhalb einer Transaktion ausgeführt werden müssen;
- Zeile 19: Die Annotation [@EntityScan] gibt die Verzeichnisse an, in denen nach JPA-Entitäten gesucht werden soll. Hier wurde sie auskommentiert, da diese Information bereits explizit in Zeile 50 angegeben wurde. Diese Annotation sollte vorhanden sein, wenn der Modus [@EnableAutoConfiguration] verwendet wird und sich die JPA-Entitäten nicht im selben Verzeichnis wie die Konfigurationsklasse befinden;
- Zeile 18: Mit der Annotation [@ComponentScan] können Sie die Verzeichnisse angeben, in denen nach Spring-Komponenten gesucht werden soll. Spring-Komponenten sind Klassen, die mit Spring-Annotationen wie @Service, @Component, @Controller usw. versehen sind. Da es hier außer den in der Klasse [Config] definierten keine weiteren gibt, wurde die Annotation auskommentiert;
- Zeilen 25–33: Definieren die Datenquelle, die H2-Datenbank. Es ist die Annotation @Bean in Zeile 25, die das von dieser Methode erstellte Objekt zu einer von Spring verwalteten Komponente macht. Der Methodenname kann hier beliebig gewählt werden. Er muss jedoch [dataSource] lauten, wenn die EntityManagerFactory in Zeile 47 fehlt und über die automatische Konfiguration definiert wird;
- Zeile 29: Die Datenbank erhält den Namen [demo] und wird im Projektordner erstellt;
- Zeilen 36–43: Definieren Sie die verwendete JPA-Implementierung, in diesem Fall eine Hibernate-Implementierung. Der Methodenname kann hier beliebig gewählt werden;
- Zeile 39: keine SQL-Protokolle;
- Zeile 30: Die Datenbank wird erstellt, falls sie noch nicht existiert;
- Zeilen 46–54: Definieren die EntityManagerFactory, die die JPA-Persistenz verwaltet. Die Methode muss den Namen [entityManagerFactory] tragen;
- Zeile 47: Die Methode erhält zwei Parameter der Typen der beiden zuvor definierten Beans. Diese werden dann von Spring als Methodenparameter konstruiert und injiziert;
- Zeile 49: Legt die zu verwendende JPA-Implementierung fest;
- Zeile 50: gibt die Verzeichnisse an, in denen die JPA-Entitäten zu finden sind;
- Zeile 51: Legt die zu verwaltende Datenquelle fest;
- Zeilen 57–62: der Transaktionsmanager. Die Methode muss den Namen [transactionManager] tragen. Sie erhält die Bean aus den Zeilen 46–54 als Parameter;
- Zeile 60: Der Transaktionsmanager wird mit der EntityManagerFactory verknüpft;
Die vorangehenden Methoden können in beliebiger Reihenfolge definiert werden.
Die Ausführung des Projekts liefert die gleichen Ergebnisse. Im Projektordner erscheint eine neue Datei, die H2-Datenbankdatei:
![]() |
Endlich können wir auf Spring Boot verzichten. Wir erstellen eine zweite ausführbare Klasse [Main2]:
![]() |
Die Klasse [Main2] enthält den folgenden Code:
package demo.console;
import java.util.List;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import demo.config.Config;
import demo.entities.Customer;
import demo.repositories.CustomerRepository;
public class Main2 {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
CustomerRepository repository = context.getBean(CustomerRepository.class);
....
context.close();
}
}
- Zeile 15: Die Konfigurationsklasse [Config] wird nun von der Spring-Klasse [AnnotationConfigApplicationContext] verwendet. Wie in Zeile 5 zu sehen ist, bestehen keine Abhängigkeiten mehr zu Spring Boot.
Die Ausführung liefert die gleichen Ergebnisse wie zuvor.
8.4.1.6. Erstellen eines ausführbaren Archivs
Um ein ausführbares Archiv des Projekts zu erstellen, gehen Sie wie folgt vor:
![]() |
- in [1]: Erstellen Sie eine Laufzeitkonfiguration;
- in [2]: vom Typ [Java-Anwendung]
- in [3]: Geben Sie das auszuführende Projekt an (verwenden Sie die Schaltfläche „Durchsuchen“);
- in [4]: Geben Sie die auszuführende Klasse an;
- in [5]: den Namen der Ausführungskonfiguration – kann beliebig sein;
![]() |
- in [6]: das Projekt exportieren;
- in [7]: als ausführbares JAR-Archiv;
- in [8]: Geben Sie den Pfad und den Namen der zu erstellenden ausführbaren Datei an;
- in [9]: den Namen der in [5] erstellten Ausführungskonfiguration;
Sobald dies erledigt ist, öffnen Sie eine Konsole in dem Ordner, der das ausführbare Archiv enthält:
Das Archiv wird wie folgt ausgeführt:
.....\dist>java -jar gs-accessing-data-jpa-2.jar
Die in der Konsole angezeigten Ergebnisse lauten wie folgt:
8.4.1.7. Erstellen Sie ein neues Spring Data-Projekt
Um eine Spring Data-Projektvorlage zu erstellen, gehen Sie wie folgt vor:
![]() |
- Erstellen Sie in [1] ein neues Projekt;
- Wählen Sie in [2] „Spring Starter Project“ aus;
- Das generierte Projekt ist ein Maven-Projekt. Geben Sie in [3] den Namen der Projektgruppe an;
- Geben Sie in [4] den Namen des Artefakts an (in diesem Fall eine JAR-Datei), das beim Erstellen des Projekts generiert wird;
- in [5]: Geben Sie das Paket der ausführbaren Klasse an, die im Projekt erstellt wird;
- in [6]: den Eclipse-Namen des Projekts – dieser kann beliebig sein (muss nicht mit [4] übereinstimmen);
- in [7]: Geben Sie an, dass Sie ein Projekt mit einer [JPA]-Schicht erstellen. Die für ein solches Projekt erforderlichen Abhängigkeiten werden dann in die [pom.xml]-Datei aufgenommen;
![]() |
- in [8]: das erstellte Projekt;
Die Datei [pom.xml] enthält die für ein JPA-Projekt erforderlichen Abhängigkeiten:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- Zeilen 9–12: Für JPA erforderliche Abhängigkeiten – beinhalten [Spring Data];
- Zeilen 13–17: Für JUnit-Tests erforderliche Abhängigkeiten, die in Spring integriert sind;
Die ausführbare Klasse [Application] führt keine Aktionen aus, ist jedoch vorkonfiguriert:
package istia.st;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Die Testklasse [ApplicationTests] führt keine Aktionen aus, ist jedoch vorkonfiguriert:
package istia.st;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
public class ApplicationTests {
@Test
public void contextLoads() {
}
}
- Zeile 9: Die Annotation [@SpringApplicationConfiguration] ermöglicht die Verwendung der Konfigurationsdatei [Application]. Die Testklasse kann somit auf alle in dieser Datei definierten Beans zugreifen;
- Zeile 8: Die Annotation [@RunWith] ermöglicht die Integration von Spring mit JUnit: Die Klasse kann als JUnit-Test ausgeführt werden. [@RunWith] ist eine JUnit-Annotation (Zeile 4), während die Klasse [SpringJUnit4ClassRunner] eine Spring-Klasse ist (Zeile 6);
Da wir nun über ein JPA-Anwendungsgerüst verfügen, können wir es vervollständigen, um die serverseitige Persistenzschicht unserer Terminverwaltungsanwendung zu schreiben.
8.4.2. Das Eclipse-Server-Projekt
![]() |
![]() |
Die Hauptkomponenten des Projekts sind wie folgt:
- [pom.xml]: die Maven-Konfigurationsdatei des Projekts;
- [rdvmedecins.entities]: die JPA-Entitäten;
- [rdvmedecins.repositories]: Spring-Data-Schnittstellen für den Zugriff auf JPA-Entitäten;
- [rdvmedecins.metier]: die [Business]-Schicht;
- [rdvmedecins.domain]: die von der [Business]-Schicht verwalteten Entitäten;
- [rdvmdecins.config]: die Konfigurationsklassen der Persistenzschicht;
- [rdvmedecins.boot]: eine einfache Konsolenanwendung;
8.4.3. Die Maven-Konfiguration
![]() | ![]() | ![]() |
Die [pom.xml]-Datei des Projekts sieht wie folgt aus:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
</parent>
<dependencies>
<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- Spring test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- driver JDBC / MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- Tomcat JDBC -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jdbc</artifactId>
</dependency>
<!-- mapper jSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Googe Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
</dependencies>
<properties>
<!-- use UTF-8 for everything -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>rdvmedecins.boot.Boot</start-class>
<java.version>1.8</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>org.jboss.repository.releases</id>
<name>JBoss Maven Release Repository</name>
<url>https://repository.jboss.org/nexus/content/repositories/releases</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>http://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
- Zeilen 8–12: Das Projekt stützt sich auf das übergeordnete Projekt [spring-boot-starter-parent]. Für Abhängigkeiten, die bereits im übergeordneten Projekt vorhanden sind, wird keine Version angegeben. Es wird die im übergeordneten Projekt definierte Version verwendet. Andere Abhängigkeiten werden wie gewohnt deklariert;
- Zeilen 15–18: für Spring Data;
- Zeilen 20–24: für JUnit-Tests;
- Zeilen 26–29: für die Spring Security-Bibliothek, deren [DAO]-Schicht eine der Passwortverschlüsselungsklassen verwendet;
- Zeilen 31–34: JDBC-Treiber für das DBMS MySQL5;
- Zeilen 36–39: Tomcat-JDBC-Verbindungspool. Ein Verbindungspool sammelt offene Verbindungen zu einer Datenbank. Wenn der Code eine Verbindung öffnen möchte, fordert er eine aus dem Pool an. Wenn der Code die Verbindung schließt, wird sie nicht geschlossen, sondern an den Pool zurückgegeben. All dies geschieht transparent auf Codeebene. Die Leistung wird verbessert, da das wiederholte Öffnen und Schließen einer Verbindung Zeit kostet. Hier baut der Verbindungspool bei der Instanziierung eine bestimmte Anzahl von Verbindungen zur Datenbank auf. Danach werden keine Verbindungen mehr geöffnet oder geschlossen, es sei denn, die im Pool gespeicherte Anzahl an Verbindungen erweist sich als unzureichend. In diesem Fall erstellt der Pool automatisch neue Verbindungen;
- Zeilen 41–44: Jackson-Bibliothek für die JSON-Verarbeitung;
- Zeilen 46–50: Google Collections-Bibliothek;
8.4.4. JPA-Entitäten
![]() |
JPA-Entitäten sind die Objekte, die die Zeilen der Datenbanktabellen kapseln.
![]() |
Die Klasse [AbstractEntity] ist die Oberklasse der Entitäten [Person, Slot, Appointment]. Ihre Definition lautet wie folgt:
package rdvmedecins.entities;
import java.io.Serializable;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;
@MappedSuperclass
public class AbstractEntity implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
protected Long id;
@Version
protected Long version;
@Override
public int hashCode() {
int hash = 0;
hash += (id != null ? id.hashCode() : 0);
return hash;
}
// initialization
public AbstractEntity build(Long id, Long version) {
this.id = id;
this.version = version;
return this;
}
@Override
public boolean equals(Object entity) {
String class1 = this.getClass().getName();
String class2 = entity.getClass().getName();
if (!class2.equals(class1) || entity==null) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id.longValue() == other.id.longValue();
}
// getters and setters
..
}
- Zeile 11: Die Annotation [@MappedSuperclass] gibt an, dass die annotierte Klasse eine übergeordnete Klasse von JPA-Entitäten [@Entity] ist;
- Zeilen 15–17: definieren den Primärschlüssel [id] für jede Entität. Es ist die Annotation [@Id], die das Feld [id] zu einem Primärschlüssel macht. Die Annotation [@GeneratedValue(strategy = GenerationType.IDENTITY)] gibt an, dass der Wert dieses Primärschlüssels vom DBMS generiert wird und dass der Generierungsmodus [IDENTITY] erzwungen wird. Für das MySQL-DBMS bedeutet dies, dass Primärschlüssel vom DBMS mit dem Attribut [AUTO_INCREMENT] generiert werden
- Zeilen 18–19: definieren die Version jeder Entität. Die JPA-Implementierung erhöht diese Versionsnummer bei jeder Änderung der Entität. Diese Nummer wird verwendet, um gleichzeitige Aktualisierungen der Entität durch zwei verschiedene Benutzer zu verhindern: Zwei Benutzer, U1 und U2, lesen die Entität E mit der Versionsnummer V1. U1 ändert E und speichert diese Änderung in der Datenbank: Die Versionsnummer ändert sich daraufhin zu V1+1. U2 ändert seinerseits E und speichert diese Änderung in der Datenbank: Er erhält eine Ausnahme, da seine Version (V1) von der in der Datenbank (V1+1) abweicht;
- Zeilen 29–33: Die Methode [build] initialisiert die beiden Felder von [AbstractEntity]. Diese Methode gibt eine Referenz auf die so initialisierte Instanz von [AbstractEntity] zurück;
- Zeilen 36–44: Die [equals]-Methode der Klasse wird überschrieben: Zwei Entitäten gelten als gleich, wenn sie denselben Klassennamen und dieselbe ID-Kennung haben;
- Zeilen 21–26: Beim Überschreiben der [equals]-Methode einer Klasse muss auch deren [hashCode]-Methode überschrieben werden (Zeilen 21–26). Die Regel lautet, dass zwei Entitäten, die von der [equals]-Methode als gleich angesehen werden, auch denselben [hashCode] haben müssen. Hier ist der [hashCode] einer Entität gleich ihrem Primärschlüssel [id]. Der [hashCode] einer Klasse wird insbesondere bei der Verwaltung von Wörterbüchern verwendet, deren Werte Instanzen der Klasse sind;
Die Entität [Person] ist die übergeordnete Klasse der Entitäten [Doctor] und [Client]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.MappedSuperclass;
@MappedSuperclass
public class Personne extends AbstractEntity {
private static final long serialVersionUID = 1L;
// attributes of a person
@Column(length = 5)
private String titre;
@Column(length = 20)
private String nom;
@Column(length = 20)
private String prenom;
// default builder
public Personne() {
}
// builder with parameters
public Personne(String titre, String nom, String prenom) {
this.titre = titre;
this.nom = nom;
this.prenom = prenom;
}
// toString
public String toString() {
return String.format("Personne[%s, %s, %s, %s, %s]", id, version, titre, nom, prenom);
}
// getters and setters
...
}
- Zeile 6: Die Annotation [@MappedSuperclass] gibt an, dass die annotierte Klasse eine übergeordnete Klasse von JPA-Entitäten [@Entity] ist;
- Zeilen 10–15: Eine Person hat einen Titel (Frau), einen Vornamen (Jacqueline) und einen Nachnamen (Tatou). Es werden keine Informationen zu den Tabellenspalten angegeben. Standardmäßig haben diese daher dieselben Namen wie die Felder;
Die Entität [Medecin] sieht wie folgt aus:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "medecins")
public class Medecin extends Personne {
private static final long serialVersionUID = 1L;
// default builder
public Medecin() {
}
// builder with parameters
public Medecin(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
public String toString() {
return String.format("Medecin[%s]", super.toString());
}
}
- Zeile 6: Die Klasse ist eine JPA-Entität;
- Zeile 7: Sie ist mit der Tabelle [DOCTORS] in der Datenbank verknüpft;
- Zeile 8: Die Entität [Doctor] leitet sich von der Entität [Person] ab;
Ein Arzt kann wie folgt initialisiert werden:
Wenn wir ihm zusätzlich eine ID und eine Version zuweisen möchten, können wir schreiben:
wobei die Methode [build] diejenige ist, die in [AbstractEntity] definiert ist.
Die Entität [Client] sieht wie folgt aus:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "clients")
public class Client extends Personne {
private static final long serialVersionUID = 1L;
// default builder
public Client() {
}
// builder with parameters
public Client(String titre, String nom, String prenom) {
super(titre, nom, prenom);
}
// identity
public String toString() {
return String.format("Client[%s]", super.toString());
}
}
- Zeile 6: Die Klasse ist eine JPA-Entität;
- Zeile 7: verknüpft mit der Tabelle [CLIENTS] in der Datenbank;
- Zeile 8: Die Entität [Client] leitet sich von der Entität [Person] ab;
Die Entität [TimeSlot] sieht wie folgt aus:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of a RV slot
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// a slot is linked to a doctor
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// foreign key
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
// default builder
public Creneau() {
}
// builder with parameters
public Creneau(Medecin medecin, int hdebut, int mdebut, int hfin, int mfin) {
this.medecin = medecin;
this.hdebut = hdebut;
this.mdebut = mdebut;
this.hfin = hfin;
this.mfin = mfin;
}
// toString
public String toString() {
return String.format("Créneau[%d, %d, %d, %d:%d, %d:%d]", id, version, idMedecin, hdebut, mdebut, hfin, mfin);
}
// foreign key
public long getIdMedecin() {
return idMedecin;
}
// setters - getters
...
}
- Zeile 10: Die Klasse ist eine JPA-Entität;
- Zeile 11: verknüpft mit der Tabelle [CRENEAUX] in der Datenbank;
- Zeile 12: Die Entität [Creneau] leitet sich von der Entität [AbstractEntity] ab und erbt daher die Felder [id] und [version];
- Zeile 16: Startzeit des Zeitfensters (14);
- Zeile 17: Startminute des Zeitfensters (20);
- Zeile 18: Endstunde des Zeitfensters (14);
- Zeile 19: Endminuten des Zeitfensters (40);
- Zeilen 22–24: der Arzt, dem der Slot gehört. Die Tabelle [CRENEAUX] verfügt über einen Fremdschlüssel zur Tabelle [MEDECINS]. Diese Beziehung wird durch die Zeilen 22–24 dargestellt;
- Zeile 22: Die Annotation [@ManyToOne] kennzeichnet eine Viele-zu-Eins-Beziehung (Slot zu Arzt). Das Attribut [fetch=FetchType.LAZY] gibt an, dass bei einer Abfrage einer [Slot]-Entität aus dem Persistenzkontext, die aus der Datenbank abgerufen werden muss, die [Doctor]-Entität nicht mit zurückgegeben wird. Der Vorteil dieses Modus besteht darin, dass die [Doctor]-Entität nur abgerufen wird, wenn der Entwickler sie anfordert. Dies spart Speicherplatz und verbessert die Leistung;
- Zeile 23: gibt den Namen der Fremdschlüsselspalte in der Tabelle [CRENEAUX] an;
- Zeilen 27–28: der Fremdschlüssel in der Tabelle [MEDECINS];
- Zeile 27: Die Spalte [ID_MEDECIN] wurde bereits in Zeile 23 verwendet. Das bedeutet, dass sie auf zwei verschiedene Arten geändert werden kann, was nach dem JPA-Standard nicht zulässig ist. Wir fügen daher die Attribute [insertable = false, updatable = false] hinzu, um sicherzustellen, dass die Spalte schreibgeschützt ist;
Die Entität [Rv] sieht wie folgt aus:
package rdvmedecins.entities;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of an Rv
@Temporal(TemporalType.DATE)
private Date jour;
// an appointment is linked to a customer
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// an appointment is linked to a time slot
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// foreign keys
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
// default builder
public Rv() {
}
// with parameters
public Rv(Date jour, Client client, Creneau creneau) {
this.jour = jour;
this.client = client;
this.creneau = creneau;
}
// toString
public String toString() {
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
}
// foreign keys
public long getIdCreneau() {
return idCreneau;
}
public long getIdClient() {
return idClient;
}
// getters and setters
...
}
- Zeile 14: Die Klasse ist eine JPA-Entität;
- Zeile 15: Sie ist mit der Tabelle [RV] in der Datenbank verknüpft;
- Zeile 16: Die Entität [Rv] leitet sich von der Entität [AbstractEntity] ab und erbt daher die Felder [id] und [version];
- Zeile 21: das Termin-Datum;
- Zeile 20: Der Java-Typ [Date] enthält sowohl ein Datum als auch eine Uhrzeit. Hier legen wir fest, dass nur das Datum verwendet wird;
- Zeilen 24–26: der Kunde, für den dieser Termin vereinbart wurde. Die Tabelle [RV] hat einen Fremdschlüssel auf die Tabelle [CLIENTS]. Diese Beziehung wird durch die Zeilen 24–26 dargestellt;
- Zeilen 29–31: das Terminfenster. Die Tabelle [RV] hat einen Fremdschlüssel auf die Tabelle [CRENEAUX]. Diese Beziehung wird durch die Zeilen 29–31 dargestellt;
- Zeilen 34–35: der Fremdschlüssel [idClient];
- Zeilen 36–37: der Fremdschlüssel [idCreneau];
8.4.5. Die [DAO]-Schicht
![]() |
Wir werden die [DAO]-Schicht mit Spring Data implementieren:
![]() |
Die [DAO]-Schicht wird mithilfe von vier Spring Data-Schnittstellen implementiert:
- [ClientRepository]: bietet Zugriff auf [Client]-JPA-Entitäten;
- [CreneauRepository]: bietet Zugriff auf [Creneau]-JPA-Entitäten;
- [MedecinRepository]: bietet Zugriff auf [Medecin]-JPA-Entitäten;
- [RvRepository]: bietet Zugriff auf [Rv]-JPA-Entitäten;
Die Schnittstelle [MedecinRepository] sieht wie folgt aus:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Medecin;
public interface MedecinRepository extends CrudRepository<Medecin, Long> {
}
- Zeile 7: Die Schnittstelle [MedecinRepository] erbt lediglich die Methoden der Schnittstelle [CrudRepository], ohne weitere hinzuzufügen;
Die [ClientRepository]-Schnittstelle sieht wie folgt aus:
package rdvmedecins.repositories;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Client;
public interface ClientRepository extends CrudRepository<Client, Long> {
}
- Zeile 7: Die Schnittstelle [ClientRepository] erbt lediglich die Methoden der Schnittstelle [CrudRepository], ohne weitere hinzuzufügen;
Die Schnittstelle [CreneauRepository] sieht wie folgt aus:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Creneau;
public interface CreneauRepository extends CrudRepository<Creneau, Long> {
// liste des créneaux horaires d'un médecin
@Query("select c from Creneau c where c.medecin.id=?1")
Iterable<Creneau> getAllCreneaux(long idMedecin);
}
- Zeile 8: Die Schnittstelle [CreneauRepository] erbt die Methoden der Schnittstelle [CrudRepository];
- Zeilen 10–11: Die Methode [getAllCreneaux] ruft die verfügbaren Zeitfenster eines Arztes ab;
- Zeile 11: Der Parameter ist die ID des Arztes. Das Ergebnis ist eine Liste von Zeitfenstern in Form eines [Iterable<Creneau>]-Objekts;
- Zeile 10: Die Annotation [@Query] dient dazu, die JPQL-Abfrage (Java Persistence Query Language) anzugeben, die die Methode implementiert. Der Parameter [?1] wird durch den Parameter [idMedecin] der Methode ersetzt;
Die Schnittstelle [RvRepository] sieht wie folgt aus:
package rdvmedecins.repositories;
import java.util.Date;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Rv;
public interface RvRepository extends CrudRepository<Rv, Long> {
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
Iterable<Rv> getRvMedecinJour(long idMedecin, Date jour);
}
- Zeile 10: Die Schnittstelle [RvRepository] erbt die Methoden der Schnittstelle [CrudRepository];
- Zeilen 12–13: Die Methode [getRvMedecinJour] ruft die Termine eines Arztes für einen bestimmten Tag ab;
- Zeile 13: Die Parameter sind die ID des Arztes und der Tag. Das Ergebnis ist eine Liste von Terminen in Form eines [Iterable<Rv>]-Objekts;
- Zeile 12: Mit der Annotation [@Query] können Sie die JPQL-Abfrage angeben, die die Methode implementiert. Der Parameter [?1] wird durch den Parameter [idMedecin] der Methode ersetzt, und der Parameter [?2] wird durch den Parameter [jour] der Methode ersetzt. Die folgende JPQL-Abfrage ist nicht ausreichend:
da die Felder der Klasse Rv vom Typ [Client] und [Creneau] im Modus [FetchType.LAZY] abgerufen werden, was bedeutet, dass sie explizit angefordert werden müssen, um abgerufen zu werden. Dies geschieht in der JPQL-Abfrage mithilfe der Syntax [left join fetch entity], die eine Verknüpfung mit der Tabelle erfordert, auf die der Fremdschlüssel verweist, um die referenzierte Entität abzurufen;
8.4.6. Die [Business]-Schicht
![]() |
![]() |
- [IMetier] ist die Schnittstelle für die [business]-Schicht, und [Metier] ist deren Implementierung;
- [Doctor'sDailySchedule] und [Doctor'sDailyTimeSlot] sind zwei Geschäftsentitäten;
8.4.6.1. Die Entitäten
Die Entität [CreneauMedecinJour] ordnet einem Zeitfenster jeden darin gebuchten Termin zu:
package rdvmedecins.domain;
import java.io.Serializable;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Creneau creneau;
private Rv rv;
// manufacturers
public CreneauMedecinJour() {
}
public CreneauMedecinJour(Creneau creneau, Rv rv) {
this.creneau=creneau;
this.rv=rv;
}
// toString
@Override
public String toString() {
return String.format("[%s %s]", creneau, rv);
}
// getters and setters
...
}
- Zeile 12: der Zeitfenster;
- Zeile 13: der Termin, falls vorhanden – andernfalls null;
Die Entität [AgendaMedecinJour] ist der Terminkalender eines Arztes für einen bestimmten Tag, d. h. die Liste seiner Termine:
package rdvmedecins.domain;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
import rdvmedecins.entities.Medecin;
public class AgendaMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
// manufacturers
public AgendaMedecinJour() {
}
public AgendaMedecinJour(Medecin medecin, Date jour, CreneauMedecinJour[] creneauxMedecinJour) {
this.medecin = medecin;
this.jour = jour;
this.creneauxMedecinJour = creneauxMedecinJour;
}
public String toString() {
StringBuffer str = new StringBuffer("");
for (CreneauMedecinJour cr : creneauxMedecinJour) {
str.append(" ");
str.append(cr.toString());
}
return String.format("Agenda[%s,%s,%s]", medecin, new SimpleDateFormat("dd/MM/yyyy").format(jour), str.toString());
}
// getters and setters
...
}
- Zeile 13: der Arzt;
- Zeile 14: der Tag im Kalender;
- Zeile 15: ihre verfügbaren Zeitfenster, mit oder ohne Termin;
8.4.6.2. Der Dienst
Die Schnittstelle der [Business]-Schicht sieht wie folgt aus:
package rdvmedecins.metier;
import java.util.Date;
import java.util.List;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
public interface IMetier {
// customer list
public List<Client> getAllClients();
// list of doctors
public List<Medecin> getAllMedecins();
// list of physician slots
public List<Creneau> getAllCreneaux(long idMedecin);
// list of doctor's appointments on a given day
public List<Rv> getRvMedecinJour(long idMedecin, Date jour);
// find a customer identified by its id
public Client getClientById(long id);
// find a customer identified by its id
public Medecin getMedecinById(long id);
// find an Rv identified by its id
public Rv getRvById(long id);
// find a time slot identified by its id
public Creneau getCreneauById(long id);
// add a RV
public Rv ajouterRv(Date jour, Creneau créneau, Client client);
// delete a RV
public void supprimerRv(Rv rv);
// job
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour);
}
Die Kommentare erläutern die Funktion der einzelnen Methoden.
Die Implementierung der Schnittstelle [IMetier] erfolgt durch die folgende Klasse [Metier]:
package rdvmedecins.metier;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.domain.CreneauMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.repositories.ClientRepository;
import rdvmedecins.repositories.CreneauRepository;
import rdvmedecins.repositories.MedecinRepository;
import rdvmedecins.repositories.RvRepository;
import com.google.common.collect.Lists;
@Service("métier")
public class Metier implements IMetier {
// repositories
@Autowired
private MedecinRepository medecinRepository;
@Autowired
private ClientRepository clientRepository;
@Autowired
private CreneauRepository creneauRepository;
@Autowired
private RvRepository rvRepository;
// interface implementation
@Override
public List<Client> getAllClients() {
return Lists.newArrayList(clientRepository.findAll());
}
@Override
public List<Medecin> getAllMedecins() {
return Lists.newArrayList(medecinRepository.findAll());
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return Lists.newArrayList(creneauRepository.getAllCreneaux(idMedecin));
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return Lists.newArrayList(rvRepository.getRvMedecinJour(idMedecin, jour));
}
@Override
public Client getClientById(long id) {
return clientRepository.findOne(id);
}
@Override
public Medecin getMedecinById(long id) {
return medecinRepository.findOne(id);
}
@Override
public Rv getRvById(long id) {
return rvRepository.findOne(id);
}
@Override
public Creneau getCreneauById(long id) {
return creneauRepository.findOne(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau créneau, Client client) {
return rvRepository.save(new Rv(jour, client, créneau));
}
@Override
public void supprimerRv(Rv rv) {
rvRepository.delete(rv.getId());
}
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
...
}
}
- Zeile 24: Die Annotation [@Service] ist eine Spring-Annotation, die die annotierte Klasse zu einer von Spring verwalteten Komponente macht. Du kannst einer Komponente einen Namen geben oder auch nicht. Diese hier heißt [business];
- Zeile 25: Die Klasse [Metier] implementiert die Schnittstelle [IMetier];
- Zeile 28: Die Annotation [@Autowired] ist eine Spring-Annotation. Der Wert des auf diese Weise annotierten Feldes wird von Spring mit der Referenz auf eine Spring-Komponente des angegebenen Typs oder Namens initialisiert (injiziert). Hier gibt die Annotation [@Autowired] keinen Namen an. Daher wird eine typbasierte Injektion durchgeführt;
- Zeile 29: Das Feld [medecinRepository] wird mit der Referenz auf eine Spring-Komponente vom Typ [MedecinRepository] initialisiert. Dies ist die Referenz auf die von Spring Data generierte Klasse zur Implementierung der bereits vorgestellten Schnittstelle [MedecinRepository];
- Zeilen 30–35: Dieser Vorgang wird für die anderen drei besprochenen Schnittstellen wiederholt;
- Zeilen 39–41: Implementierung der Methode [getAllClients];
- Zeile 40: Wir verwenden die Methode [findAll] der Schnittstelle [ClientRepository]. Diese Methode gibt einen Typ [Iterable<Client>] zurück, den wir mithilfe der statischen Methode [Lists.newArrayList] in eine [List<Client>] konvertieren. Die Klasse [Lists] ist in der Google Guava-Bibliothek definiert. In [pom.xml] wurde diese Abhängigkeit importiert:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>16.0.1</version>
</dependency>
- Zeilen 38–86: Die Methoden der [IMetier]-Schnittstelle werden mithilfe von Klassen aus der [DAO]-Schicht implementiert;
Nur die Methode in Zeile 88 ist spezifisch für die [business]-Schicht. Sie wurde hier platziert, da sie Geschäftslogik ausführt, die über den einfachen Datenzugriff hinausgeht. Ohne diese Methode gäbe es keinen Grund, eine [business]-Schicht zu erstellen. Die Methode [getAgendaMedecinJour] lautet wie folgt:
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
// list of doctor's time slots
List<Creneau> creneauxHoraires = getAllCreneaux(idMedecin);
// list of bookings for the same doctor on the same day
List<Rv> reservations = getRvMedecinJour(idMedecin, jour);
// a dictionary is created from the Rvs taken
Map<Long, Rv> hReservations = new Hashtable<Long, Rv>();
for (Rv resa : reservations) {
hReservations.put(resa.getCreneau().getId(), resa);
}
// create the agenda for the requested day
AgendaMedecinJour agenda = new AgendaMedecinJour();
// the doctor
agenda.setMedecin(getMedecinById(idMedecin));
// the day
agenda.setJour(jour);
// reservation slots
CreneauMedecinJour[] creneauxMedecinJour = new CreneauMedecinJour[creneauxHoraires.size()];
agenda.setCreneauxMedecinJour(creneauxMedecinJour);
// filling reservation slots
for (int i = 0; i < creneauxHoraires.size(); i++) {
// line i agenda
creneauxMedecinJour[i] = new CreneauMedecinJour();
// time slot
Creneau créneau = creneauxHoraires.get(i);
long idCreneau = créneau.getId();
creneauxMedecinJour[i].setCreneau(créneau);
// is the slot free or reserved?
if (hReservations.containsKey(idCreneau)) {
// the slot is occupied - we note the resa
Rv resa = hReservations.get(idCreneau);
creneauxMedecinJour[i].setRv(resa);
}
}
// we return the result
return agenda;
}
Leser werden gebeten, die Kommentare zu lesen. Der Algorithmus lautet wie folgt:
- alle Zeitfenster für den angegebenen Arzt abrufen;
- alle Termine für den angegebenen Tag abrufen;
- Anhand dieser beiden Informationen können wir feststellen, ob ein Zeitfenster frei oder belegt ist;
8.4.7. Die Spring-Projektkonfiguration
![]() |
Die Klasse [DomainAndPersistenceConfig] konfiguriert das gesamte Projekt:
package rdvmedecins.config;
import javax.persistence.EntityManagerFactory;
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@ComponentScan(basePackages = { "rdvmedecins" })
public class DomainAndPersistenceConfig {
// JPA entity packages
public final static String[] ENTITIES_PACKAGES = { "rdvmedecins.entities", "rdvmedecins.security" };
// the MySQL data source
@Bean
public DataSource dataSource() {
// data source TomcatJdbc
DataSource dataSource = new DataSource();
// configuration JDBC
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/dbrdvmedecins");
dataSource.setUsername("root");
dataSource.setPassword("");
// initially open connections
dataSource.setInitialSize(5);
// result
return dataSource;
}
// provider JPA is Hibernate
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
// EntityManagerFactory
@Bean
public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
factory.setJpaVendorAdapter(jpaVendorAdapter);
factory.setPackagesToScan(ENTITIES_PACKAGES);
factory.setDataSource(dataSource);
factory.afterPropertiesSet();
return factory.getObject();
}
// Transaction manager
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
JpaTransactionManager txManager = new JpaTransactionManager();
txManager.setEntityManagerFactory(entityManagerFactory);
return txManager;
}
}
- Zeile 17: Die Klasse ist eine Spring-Konfigurationsklasse;
- Zeile 18: Die Pakete, die die Spring Data [CrudRepository]-Schnittstellen enthalten. Diese werden dem Spring-Kontext hinzugefügt;
- Zeile 19: Fügt alle Klassen im Paket [rdvmedecins] und deren Unterklassen, die eine Spring-Annotation aufweisen, zum Spring-Kontext hinzu. Im Paket [rdvmedecins.metier] wird die Klasse [Metier] mit ihrer Annotation [@Service] gefunden und dem Spring-Kontext hinzugefügt;
- Zeilen 26–39: Konfigurieren des Tomcat-JDBC-Verbindungspools (Zeile 5);
- Zeile 36: Der Verbindungspool verfügt standardmäßig über 5 offene Verbindungen. Diese Zeile dient nur zur Veranschaulichung. In unserem Fall wäre 1 Verbindung ausreichend. Wenn die [DAO]-Schicht von mehreren Threads genutzt werden soll, wäre diese Zeile notwendig. Dies wird später der Fall sein, wenn die [DAO]-Schicht als Grundlage für eine Webanwendung dient, die naturgemäß die gleichzeitige Bedienung mehrerer Benutzer unterstützt;
- Zeilen 42–49: Die verwendete JPA-Implementierung ist eine Hibernate-Implementierung;
- Zeile 45: keine SQL-Protokolle;
- Zeile 46: keine Tabellenregenerierung;
- Zeile 47: Das verwendete DBMS ist MySQL;
- Zeilen 53–61: Definieren die EntityManagerFactory für die JPA-Schicht. Von diesem Objekt erhalten wir das [EntityManager]-Objekt, das zur Durchführung von JPA-Operationen verwendet wird;
- Zeile 57: Gibt das/die Paket(e) an, in dem/denen sich die JPA-Entitäten befinden;
- Zeile 58: Gibt die Datenquelle an, die mit der JPA-Schicht verbunden werden soll;
- Zeilen 64–69: Der mit der vorherigen EntityManagerFactory verknüpfte Transaktionsmanager. Standardmäßig werden Methoden der [CrudRepository]-Schnittstellen von Spring Data innerhalb einer Transaktion ausgeführt. Die Transaktion wird vor dem Aufruf der Methode gestartet und nach dem Verlassen der Methode (durch einen Commit oder Rollback) abgeschlossen;
8.4.8. Tests für die [Business]-Schicht
Die Klasse [rdvmedecins.tests.Metier] ist eine Spring/JUnit 4-Testklasse:
package rdvmedecins.tests;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Metier {
@Autowired
private IMetier métier;
@Test
public void test1(){
// customer display
List<Client> clients = métier.getAllClients();
display("Liste des clients :", clients);
// physician display
List<Medecin> medecins = métier.getAllMedecins();
display("Liste des médecins :", medecins);
// display doctor's slots
Medecin médecin = medecins.get(0);
List<Creneau> creneaux = métier.getAllCreneaux(médecin.getId());
display(String.format("Liste des créneaux du médecin %s", médecin), creneaux);
// list of doctor's appointments on a given day
Date jour = new Date();
display(String.format("Liste des rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// add a RV to the list
Rv rv = null;
Creneau créneau = creneaux.get(2);
Client client = clients.get(0);
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
rv = métier.ajouterRv(jour, créneau, client);
// check
Rv rv2 = métier.getRvById(rv.getId());
Assert.assertEquals(rv, rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// add a RV in the same slot on the same day
// must trigger an exception
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau %s pour le client %s", jour, créneau,
client));
Boolean erreur = false;
try {
rv = métier.ajouterRv(jour, créneau, client);
System.out.println("Rv ajouté");
} catch (Exception ex) {
Throwable th = ex;
while (th != null) {
System.out.println(ex.getMessage());
th = th.getCause();
}
// we note the error
erreur = true;
}
// check for errors
Assert.assertTrue(erreur);
// RV list
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
// calendar display
AgendaMedecinJour agenda = métier.getAgendaMedecinJour(médecin.getId(), jour);
System.out.println(agenda);
Assert.assertEquals(rv, agenda.getCreneauxMedecinJour()[2].getRv());
// delete a RV
System.out.println("Suppression du Rv ajouté");
métier.supprimerRv(rv);
// check
rv2 = métier.getRvById(rv.getId());
Assert.assertNull(rv2);
display(String.format("Liste des Rv du médecin %s, le [%s]", médecin, jour), métier.getRvMedecinJour(médecin.getId(), jour));
}
// utility method - displays items in a collection
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- Zeile 22: Die Annotation [@SpringApplicationConfiguration] ermöglicht die Verwendung der zuvor besprochenen Konfigurationsdatei [DomainAndPersistenceConfig]. Die Testklasse profitiert somit von allen in dieser Datei definierten Beans;
- Zeile 23: Die Annotation [@RunWith] ermöglicht die Integration von Spring mit JUnit: Die Klasse kann als JUnit-Test ausgeführt werden. [@RunWith] ist eine JUnit-Annotation (Zeile 9), während die Klasse [SpringJUnit4ClassRunner] eine Spring-Klasse ist (Zeile 12);
- Zeilen 26–27: Injektion einer Referenz auf die [business]-Schicht in die Testklasse;
- Viele Tests sind einfache visuelle Tests:
- Zeilen 32–33: Liste der Kunden;
- Zeilen 35–36: Liste der Ärzte;
- Zeilen 39–40: Liste der Zeitfenster eines Arztes;
- Zeile 43: Liste der Termine eines Arztes;
- Zeile 50: Hinzufügen eines neuen Termins. Die Methode [addAppt] gibt den Termin mit zusätzlichen Informationen zurück, nämlich seiner Primärschlüssel-ID;
- Zeile 53: Dieser Primärschlüssel wird verwendet, um den Termin in der Datenbank zu suchen;
- Zeile 54: Wir überprüfen, ob der gesuchte Termin und der gefundene Termin identisch sind. Zur Erinnerung: Die Methode [equals] der Entität [Rv] wurde neu definiert: Zwei Termine sind gleich, wenn sie dieselbe ID haben. Dies zeigt uns, dass der hinzugefügte Termin tatsächlich in die Datenbank eingefügt wurde;
- Zeilen 61–73: Wir versuchen, denselben Termin ein zweites Mal hinzuzufügen. Dies sollte vom DBMS abgelehnt werden, da eine Eindeutigkeitsbeschränkung besteht:
CREATE TABLE IF NOT EXISTS `rv` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
`JOUR` date NOT NULL,
`ID_CLIENT` bigint(20) NOT NULL,
`ID_CRENEAU` bigint(20) NOT NULL,
`VERSION` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`ID`),
UNIQUE KEY `UNQ1_RV` (`JOUR`,`ID_CRENEAU`),
KEY `FK_RV_ID_CRENEAU` (`ID_CRENEAU`),
KEY `FK_RV_ID_CLIENT` (`ID_CLIENT`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_swedish_ci AUTO_INCREMENT=60 ;
Zeile 8 oben legt fest, dass die Kombination [DAY, SLOT_ID] eindeutig sein muss, wodurch verhindert wird, dass zwei Termine am selben Tag im selben Zeitfenster geplant werden.
- Zeile 73: Wir überprüfen, ob tatsächlich eine Ausnahme aufgetreten ist;
- Zeile 77: Wir rufen den Kalender des Arztes ab, für den wir gerade einen Termin hinzugefügt haben;
- Zeile 79: Wir überprüfen, ob der hinzugefügte Termin tatsächlich in seinem Terminkalender vorhanden ist;
- Zeile 82: Löschen des hinzugefügten Termins;
- Zeile 84: Den gelöschten Termin aus der Datenbank abrufen;
- Zeile 85: Wir prüfen, ob wir einen Null-Zeiger abgerufen haben, was darauf hindeutet, dass der gesuchte Termin nicht existiert;
Der Test läuft erfolgreich ab:
![]() |
8.4.9. Das Konsolenprogramm
![]() |
Das Konsolenprogramm ist einfach gehalten. Es veranschaulicht, wie man einen Fremdschlüssel abruft:
package rdvmedecins.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
public class Boot {
// the boot
public static void main(String[] args) {
// prepare the configuration
SpringApplication app = new SpringApplication(DomainAndPersistenceConfig.class);
app.setLogStartupInfo(false);
// launch it
ConfigurableApplicationContext context = app.run(args);
// business
IMetier métier = context.getBean(IMetier.class);
try {
// add a RV to the list
Date jour = new Date();
System.out.println(String.format("Ajout d'un Rv le [%s] dans le créneau 1 pour le client 1", new SimpleDateFormat("dd/MM/yyyy").format(jour)));
Client client = (Client) new Client().build(1L, 1L);
Creneau créneau = (Creneau) new Creneau().build(1L, 1L);
Rv rv = métier.ajouterRv(jour, créneau, client);
System.out.println(String.format("Rv ajouté = %s", rv));
// check
créneau = métier.getCreneauById(1L);
long idMedecin = créneau.getIdMedecin();
display("Liste des rendez-vous", métier.getRvMedecinJour(idMedecin, jour));
} catch (Exception ex) {
System.out.println("Exception : " + ex.getCause());
}
// closing the Spring context
context.close();
}
// utility method - displays items in a collection
private static <T> void display(String message, Iterable<T> elements) {
System.out.println(message);
for (T element : elements) {
System.out.println(element);
}
}
}
Das Programm fügt einen Termin hinzu und überprüft anschließend, ob dieser hinzugefügt wurde.
- Zeile 19: Die Klasse [SpringApplication] verwendet die Konfigurationsklasse [DomainAndPersistenceConfig];
- Zeile 20: Unterdrückung der Startprotokolle der Anwendung;
- Zeile 22: Die Klasse [SpringApplication] wird ausgeführt. Sie gibt einen Spring-Kontext zurück, d. h. die Liste der registrierten Beans;
- Zeile 24: Es wird eine Referenz auf das Bean abgerufen, das die Schnittstelle [IMetier] implementiert. Dies ist somit eine Referenz auf die [business]-Schicht;
- Zeilen 27–31: Fügen Sie einen neuen Termin für heute hinzu, für Kunde Nr. 1 in Slot Nr. 1. Der Kunde und der Slot wurden von Grund auf neu erstellt, um zu demonstrieren, dass nur Identifikatoren verwendet werden. Wir haben hier die Version initialisiert, hätten aber jeden beliebigen Wert verwenden können. Sie wird hier nicht verwendet;
- Zeile 34: Wir möchten wissen, welcher Arzt Slot Nr. 1 belegt. Dazu müssen wir die Datenbank nach Slot Nr. 1 abfragen. Da wir uns im Modus [FetchType.LAZY] befinden, wird der Arzt nicht zusammen mit dem Slot zurückgegeben. Wir haben jedoch darauf geachtet, ein Feld [idMedecin] in die Entität [Creneau] aufzunehmen, um den Primärschlüssel des Arztes abzurufen;
- Zeile 35: Wir rufen den Primärschlüssel des Arztes ab;
- Zeile 36: Wir zeigen die Liste der Termine des Arztes an;
Die Konsolenausgabe lautet wie folgt:
8.4.10. Protokollverwaltung
Konsolenprotokolle werden über zwei Dateien konfiguriert: [application.properties] und [logback.xml] [1]:
![]() |
Die Datei [application.properties] wird vom Spring Boot-Framework verwendet. Sie ermöglicht es Ihnen, eine Vielzahl von Einstellungen zu definieren, um die von Spring Boot verwendeten Standardwerte zu überschreiben (http://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html). Hier ist ihr Inhalt:
logging.level.org.hibernate=OFF
spring.main.show-banner=false
- Zeile 1: Steuert die Protokollierungsstufe von Hibernate – hier werden keine Protokolle ausgegeben
- Zeile 2: steuert die Anzeige des Spring Boot-Banners – hier wird kein Banner angezeigt
Die Datei [logback.xml] ist die Konfigurationsdatei für das Logging-Framework [logback] [2]:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- log level control -->
<root level="info"> <!-- off, info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- Die allgemeine Protokollstufe wird durch Zeile 9 gesteuert – hier werden Protokolle der Stufe [info] aufgezeichnet;
Dies führt zu folgendem Ergebnis:
Wenn wir die Hibernate-Protokollierungsstufe auf [info] setzen (ohne sonstige Änderungen):
logging.level.org.hibernate=INFO
spring.main.show-banner=false
Dies führt zu folgendem Ergebnis:
Wenn wir die Protokollstufe auf [debug] setzen (ohne sonstige Änderungen):
logging.level.org.hibernate=DEBUG
spring.main.show-banner=false
Dies führt zu folgendem Ergebnis:
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Eagerly caching bean 'clientRepository' to allow for resolving potential circular references
10:35:13.522 [main] DEBUG o.s.b.f.annotation.InjectionMetadata - Processing injected element of bean 'clientRepository': PersistenceElement for public void org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean.setEntityManager(javax.persistence.EntityManager)
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#b967222'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6a2eea2a'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#1ba05e38'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'entityManagerFactory'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean '(inner bean)#6c298dc'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'jpaMappingContext'
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Invoking afterPropertiesSet() on bean with name 'clientRepository'
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.o.j.SharedEntityManagerCreator$SharedEntityManagerInvocationHandler - Creating new EntityManager for shared EntityManager invocation
10:35:13.522 [main] DEBUG o.s.o.jpa.EntityManagerFactoryUtils - Closing JPA EntityManager
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$ThreadBoundTargetSource@723ed581
10:35:13.522 [main] DEBUG o.s.aop.framework.JdkDynamicAopProxy - Creating JDK dynamic proxy: target source is SingletonTargetSource for target object [org.springframework.data.jpa.repository.support.SimpleJpaRepository@796065aa]
10:35:13.522 [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Finished creating instance of bean 'clientRepository'
10:35:13.522 [main] DEBUG o.s.b.f.a.AutowiredAnnotationBeanPostProcessor - Autowiring by type from bean name 'métier' to bean named 'clientRepository'
...
8.4.11. Die [Web-/JSON]-Schicht
![]() |
![]() |
Wir werden die [Web-/JSON]-Schicht in mehreren Schritten aufbauen:
- Schritt 1: Eine funktionsfähige Web-Schicht ohne Authentifizierung;
- Schritt 2: Implementierung der Authentifizierung mit Spring Security;
- Schritt 3: Implementierung von CORS [Cross-Origin Resource Sharing (CORS) ist ein Mechanismus, der es ermöglicht, viele Ressourcen (z. B. Schriftarten, JavaScript usw.) auf einer Webseite von einer anderen Domain außerhalb der Domain anzufordern, aus der die Ressource stammt. (Wikipedia)]. Der Client für unseren Webdienst wird ein Angular-Webclient sein, der nicht unbedingt zur selben Domain gehört wie unser Webdienst. Standardmäßig kann er nicht darauf zugreifen, es sei denn, der Webdienst autorisiert ihn dazu. Wir werden sehen, wie das geht;
8.4.11.1. Maven-Konfiguration
Die [pom.xml]-Datei des Projekts 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>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webjson-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
</parent>
<dependencies>
<!-- spring mvc web layer -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- test layer -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- layer DAO -->
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
...
</project>
- Zeilen 12–15: das übergeordnete Maven-Projekt;
- Zeilen 19–22: Abhängigkeiten für ein Spring-MVC-Projekt;
- Zeilen 24–28: Abhängigkeiten für JUnit-/Spring-Tests;
- Zeilen 30–34: Abhängigkeiten der Projekt-Schichten [Geschäftslogik, DAO, JPA];
8.4.11.2. Die Webservice-Schnittstelle
![]() |
- In [1] (siehe oben) kann der Browser nur eine begrenzte Anzahl von URLs mit einer bestimmten Syntax anfordern;
- in [4] erhält er eine JSON-Antwort;
Die Antworten unseres Webdienstes haben alle das gleiche Format, das der JSON-Darstellung eines Objekts vom Typ [Response] wie folgt entspricht:
package rdvmedecins.web.models;
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 7: Antwort-Fehlercode 0: OK, alles andere: KO;
- Zeile 11: eine Liste von Fehlermeldungen, falls ein Fehler vorliegt;
- Zeile 13: der Hauptteil der Antwort;
Wir präsentieren nun die Screenshots, die die Webservice-/JSON-Schnittstelle veranschaulichen:
Liste aller Patienten der Arztpraxis [/getAllClients]
![]() |
Liste aller Ärzte der Praxis [/getAllMedecins]
![]() |
Liste der verfügbaren Termine eines Arztes [/getAllCreneaux/{idMedecin}]
![]() |
Liste der Termine eines Arztes [/getRvMedecinJour/{idMedecin}/{yyyy-mm-dd}
![]() |
Tagesplan eines Arztes [/getAgendaMedecinJour/{idMedecin}/{yyyy-mm-dd}]
![]() |
Um einen Termin hinzuzufügen oder zu löschen, verwenden wir die Chrome-Erweiterung [Advanced Rest Client], da diese Vorgänge über eine POST-Anfrage ausgeführt werden.
Termin hinzufügen [/addAppointment]
![]() |
- in [0] die URL des Webdienstes;
- in [1] wird die POST-Methode verwendet;
- in [2] der JSON-Text der an den Webdienst gesendeten Informationen im Format {day, clientId, slotId};
- in [3] teilt der Client dem Webdienst mit, dass er Informationen im JSON-Format sendet;
Die Antwort lautet dann wie folgt:
![]() |
- in [4]: Der Client sendet den Header, der angibt, dass die gesendeten Daten im JSON-Format vorliegen;
- in [5]: Der Webdienst antwortet, dass er ebenfalls JSON sendet;
- in [6]: die JSON-Antwort des Webdienstes. Das Feld [body] enthält die JSON-Darstellung des hinzugefügten Termins;
Das Vorhandensein des neuen Termins kann überprüft werden:
![]() |
Notieren Sie sich die Termin-ID [50]. Wir werden diesen Termin löschen.
Termin löschen [/deleteApp]
![]() |
- in [1] die URL des Webdienstes;
- in [2] wird die POST-Methode verwendet;
- in [3] der JSON-Text der an den Webdienst gesendeten Informationen in der Form {idRv};
- in [4] teilt der Client dem Webdienst mit, dass er JSON-Daten sendet;
Die Antwort lautet dann wie folgt:
![]() |
- in [5]: Das Feld [status] wird auf 0 gesetzt, was anzeigt, dass der Vorgang erfolgreich war;
Die Löschung des Termins kann überprüft werden:
![]() |
Der Termin für die Patientin [Frau GERMAIN] ist oben nicht mehr aufgeführt.
Der Webdienst ermöglicht es auch, Entitäten anhand ihrer ID abzurufen:
![]() |
![]() |
![]() |
![]() |
Alle diese URLs werden vom Controller [RdvMedecinsController] verarbeitet, den wir in Kürze vorstellen werden.
8.4.11.3. Konfiguration des Webdienstes
![]() |
Die Konfigurationsklasse [AppConfig] sieht wie folgt aus:
package rdvmedecins.web.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- Zeile 12: Die Klasse [AppConfig] konfiguriert die gesamte Anwendung;
- Zeile 9: Die Klasse [AppConfig] ist eine Spring-Konfigurationsklasse;
- Zeile 10: Wir legen fest, dass im Paket [rdvmedecins.web] und dessen Unterpaketen nach Spring-Komponenten gesucht werden soll. Auf diese Weise werden die folgenden Komponenten gefunden:
- [@RestController RdvMedecinsController] im Paket [rdvmedecins.web.controllers];
- [@Component ApplicationModel] im Paket [rdvmedecins.web.models];
- Zeile 11: Wir importieren die Klasse [DomainAndPersistenceConfig], die das Projekt [rdvmedecins-metier-dao] so konfiguriert, dass Zugriff auf die Beans dieses Projekts gewährt wird;
- Zeile 11: Die Klasse [SecurityConfig] konfiguriert die Sicherheit der Webanwendung. Wir werden sie vorerst ignorieren;
- Zeile 11: Die Klasse [WebConfig] konfiguriert die [Web-/JSON]-Schicht;
Die Klasse [WebConfig] sieht wie folgt aus:
package rdvmedecins.web.config;
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.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
@Configuration
@EnableWebMvc
public class WebConfig {
// dispatcherservlet configuration for CORS headers
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8080);
}
// mappers jSON
@Bean
public ObjectMapper jsonMapper() {
return new ObjectMapper();
}
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(
new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
return jsonMapperLongRv;
}
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
}
- Zeilen 20–25: Definieren Sie die [dispatcherServlet]-Bean. Die [DispatcherServlet]-Klasse ist das Servlet des Spring MVC-Frameworks. Sie fungiert als [FrontController]: Sie fängt an die Spring MVC-Site gesendete Anfragen ab und leitet sie an einen der Controller der Site weiter;
- Zeile 22: Instanziierung der Klasse;
- Zeile 23: Diese Zeile kann vorerst ignoriert werden;
- Zeilen 27–30: Das [dispatcherServlet]-Servlet verarbeitet alle URLs;
- Zeilen 27–30: Aktivierung des in den Projektabhängigkeiten eingebetteten Tomcat-Servers. Er läuft auf Port 8080;
- Zeilen 38–67: vier JSON-Mapper, die mit verschiedenen JSON-Filtern konfiguriert sind;
- Zeilen 38–41: ein JSON-Mapper ohne Filter;
- Zeilen 43–49: Der JSON-Mapper [jsonMapperShortCreneau] serialisiert/deserialisiert ein [Creneau]-Objekt und ignoriert dabei das Feld [Creneau.medecin];
- Zeilen 51–59: Der JSON-Mapper [jsonMapperLongRv] serialisiert/deserialisiert ein [Rv]-Objekt und ignoriert dabei das Feld [Rv.creneau.medecin];
- Zeilen 61–67: Der JSON-Mapper [jsonMapperShortRv] serialisiert/deserialisiert ein [Rv]-Objekt und ignoriert dabei die Felder [Rv.creneau] und [Rv.client];
8.4.11.4. Die Klasse [ApplicationModel]
![]() |
Die Klasse [ApplicationModel] erfüllt zwei Zwecke:
- als Cache zum Speichern von Listen mit Ärzten und Patienten (Clients);
- als zentrale Schnittstelle für die Controller;
package rdvmedecins.web.models;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.metier.IMetier;
import rdvmedecins.web.helpers.Static;
@Component
public class ApplicationModel implements IMetier {
// the [business] layer
@Autowired
private IMetier métier;
// data from the [business] layer
private List<Medecin> médecins;
private List<Client> clients;
private List<String> messages;
// configuration data
private boolean CORSneeded = false;
private boolean secured = false;
@PostConstruct
public void init() {
// we get the doctors and the customers
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
// getter
public List<String> getMessages() {
return messages;
}
// ------------------------- [business] layer interface
@Override
public List<Client> getAllClients() {
return clients;
}
@Override
public List<Medecin> getAllMedecins() {
return médecins;
}
@Override
public List<Creneau> getAllCreneaux(long idMedecin) {
return métier.getAllCreneaux(idMedecin);
}
@Override
public List<Rv> getRvMedecinJour(long idMedecin, Date jour) {
return métier.getRvMedecinJour(idMedecin, jour);
}
@Override
public Client getClientById(long id) {
return métier.getClientById(id);
}
@Override
public Medecin getMedecinById(long id) {
return métier.getMedecinById(id);
}
@Override
public Rv getRvById(long id) {
return métier.getRvById(id);
}
@Override
public Creneau getCreneauById(long id) {
return métier.getCreneauById(id);
}
@Override
public Rv ajouterRv(Date jour, Creneau creneau, Client client) {
return métier.ajouterRv(jour, creneau, client);
}
@Override
public void supprimerRv(long idRv) {
métier.supprimerRv(idRv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
// getters and setters
public boolean isCORSneeded() {
return CORSneeded;
}
public boolean isSecured() {
return secured;
}
}
- Zeile 19: Die Annotation [@Component] macht die Klasse [ApplicationModel] zu einer Spring-Komponente. Wie bei allen bisher behandelten Spring-Komponenten (mit Ausnahme von @Controller) wird nur ein einziges Objekt dieses Typs instanziiert (Singleton);
- Zeile 20: Die Klasse [ApplicationModel] implementiert die Schnittstelle [IMetier];
- Zeilen 23–24: Eine Referenz auf die [business]-Schicht wird von Spring injiziert;
- Zeile 34: Die Annotation [@PostConstruct] stellt sicher, dass die Methode [init] unmittelbar nach der Instanziierung der Klasse [ApplicationModel] ausgeführt wird;
- Zeilen 38–39: Abrufen der Listen mit Ärzten und Kunden aus der [business]-Schicht;
- Zeile 41: Tritt eine Ausnahme auf, werden die Meldungen aus dem Ausnahmestapel im Feld in Zeile 17 gespeichert;
Die Architektur der Web-Schicht entwickelt sich wie folgt:
![]() |
- In [2b] kommunizieren die Methoden des/der Controller(s) mit dem [ApplicationModel]-Singleton;
Diese Strategie bietet Flexibilität bei der Cache-Verwaltung. Derzeit werden die Terminfenster der Ärzte nicht zwischengespeichert. Um sie zu zwischenspeichern, ändern Sie einfach die Klasse [ApplicationModel]. Dies hat keine Auswirkungen auf den Controller, der weiterhin wie bisher die Methode [List<Creneau> getAllCreneaux(long idMedecin)] verwendet. Es ist die Implementierung dieser Methode in [ApplicationModel], die geändert wird.
8.4.11.5. Die Klasse „Static“
Die Klasse [Static] enthält eine Reihe statischer Hilfsmethoden, die keine „Business“- oder „Web“-Aspekte aufweisen:
![]() |
Der Code lautet wie folgt:
package rdvmedecins.web.helpers;
import java.util.ArrayList;
import java.util.List;
public class Static {
public Static() {
}
// list of exception error messages
public static List<String> getErreursForException(Exception exception) {
// retrieve the list of exception error messages
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
erreurs.add(cause.getMessage());
cause = cause.getCause();
}
return erreurs;
}
}
- Zeile 12: Die Methode [Static.getErrorsForException], die (in Zeile 8 unten) in der Methode [init] der Klasse [ApplicationModel] verwendet wurde:
@PostConstruct
public void init() {
// we get the doctors and the customers
try {
médecins = métier.getAllMedecins();
clients = métier.getAllClients();
} catch (Exception ex) {
messages = Static.getErreursForException(ex);
}
}
Die Methode erstellt ein [List<String>]-Objekt, das die Fehlermeldungen [exception.getMessage()] einer Ausnahme [exception] und die ihrer inneren Ausnahme [exception.getCause()] enthält.
8.4.11.6. Das Controller-Skelett [RdvMedecinsController]
![]() |
Wir werden nun die Handhabung der URLs des Webdienstes näher erläutern. An diesem Prozess sind drei Hauptklassen beteiligt:
- der Controller [RdvMedecinsController];
- die Klasse mit den Hilfsmethoden [Static];
- die Cache-Klasse [ApplicationModel];
![]() |
Der [RdvMedecinsController]-Controller sieht wie folgt aus:
package rdvmedecins.web.controllers;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
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.databind.ObjectMapper;
import rdvmedecins.domain.AgendaMedecinJour;
import rdvmedecins.entities.Client;
import rdvmedecins.entities.Creneau;
import rdvmedecins.entities.Medecin;
import rdvmedecins.entities.Rv;
import rdvmedecins.web.helpers.Static;
import rdvmedecins.web.models.ApplicationModel;
import rdvmedecins.web.models.PostAjouterRv;
import rdvmedecins.web.models.PostSupprimerRv;
import rdvmedecins.web.models.Response;
@Controller
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
// message list
private List<String> messages;
// mappers jSON
@Autowired
private ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
@PostConstruct
public void init() {
// application error messages
messages = application.getMessages();
}
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {...}
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {...}
// list of physician slots
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {...}
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
throws JsonProcessingException {...}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) String origin) throws JsonProcessingException {...}
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {...}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {...}
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {...}
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour)
throws JsonProcessingException {...}
@RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String authenticate() throws JsonProcessingException {...}
}
- Zeile 35: Die Annotation [@Controller] macht die Klasse [RdvMedecinsController] zu einem Spring-Controller, dem C in MVC;
- Zeilen 38–39: Ein Objekt vom Typ [ApplicationModel] wird hier von Spring injiziert. Wir haben es bereits vorgestellt;
- Zeilen 41–42: Hier wird von Spring ein Objekt vom Typ [RdvMedecinsCorsController] injiziert. Wir werden dieses Objekt später vorstellen;
- Zeilen 48–58: Die in der Konfigurationsklasse [WebConfig] definierten JSON-Mapper;
- Zeile 60: Die Annotation [@PostConstruct] kennzeichnet eine Methode, die unmittelbar nach der Instanziierung der Klasse ausgeführt werden soll. Wenn diese Methode ausgeführt wird, stehen die von Spring injizierten Objekte zur Verfügung;
- Zeile 63: Wir rufen alle Fehlermeldungen aus dem [ApplicationModel]-Objekt ab. Dieses Objekt wurde beim Start der Anwendung instanziiert und hat versucht, die Ärzte und Kunden zwischenzuspeichern. Ist dies fehlgeschlagen, dann ist [messages!=null]. Dadurch können die Methoden des Controllers feststellen, ob die Anwendung korrekt initialisiert wurde;
- Zeilen 67–118: die vom [web/jSON]-Dienst bereitgestellten URLs. Alle Methoden geben eine JSON-Zeichenkette des folgenden Typs [Response<T>] zurück:
![]() |
package rdvmedecins.web.models;
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 9: ein Fehlercode: 0 bedeutet, dass kein Fehler vorliegt;
- Zeile 11: Wenn [status!=0], dann ist [messages] eine Liste von Fehlermeldungen;
- Zeile 13: ein T-Objekt, das in der Antwort gekapselt ist. T ist null, falls ein Fehler vorliegt;
Dieses Objekt wird in JSON serialisiert, bevor es an den Browser des Clients gesendet wird;
- Zeile 67: Die exponierte URL lautet [/getAllDoctors]. Der Client muss eine [GET]-Methode verwenden, um seine Anfrage zu stellen (method = RequestMethod.GET). Würde diese URL über einen POST-Request angefordert, würde sie abgelehnt und Spring MVC würde einen HTTP-Fehlercode an den Web-Client senden. Die Methode selbst gibt die Antwort an den Client zurück (Zeile 68). Dies ist eine Zeichenkette (Zeile 67). Der HTTP-Header [Content-type: application/json; charset=UTF-8] wird an den Client gesendet, um anzuzeigen, dass dieser eine JSON-Zeichenkette erhalten wird (Zeile 67);
- Zeile 77: Die URL ist mit {idMedecin} konfiguriert. Dieser Parameter wird mithilfe der Annotation [@PathVariable] in Zeile 79 abgerufen;
- Zeile 79: Der Parameter [long idMedecin] erhält seinen Wert aus dem Parameter {idMedecin} in der URL [@PathVariable("idMedecin")]. Der Parameter in der URL und der in der Methode können unterschiedliche Namen haben . Beachten Sie, dass [@PathVariable("idMedecin")] vom Typ String ist (die gesamte URL ist ein String), während der Parameter [long idMedecin] vom Typ [long] ist. Die Typkonvertierung erfolgt automatisch. Wenn diese Typkonvertierung fehlschlägt, wird ein HTTP-Fehlercode zurückgegeben;
- Zeile 105: Die Annotation [@RequestBody] bezieht sich auf den Request-Body. Bei einer GET-Anfrage gibt es fast nie einen Body (es ist jedoch möglich, einen einzufügen). Bei einer POST-Anfrage gibt es normalerweise einen (es ist jedoch möglich, ihn wegzulassen). Für die URL [ajouterRv] sendet der Web-Client in seinem POST-Request die folgende JSON-Zeichenkette:
Die Syntax [@RequestBody PostAjouterRv post] (Zeile 105) bedeutet in Verbindung mit der Tatsache, dass die Methode JSON erwartet [consumes = "application/json; charset=UTF-8"] (Zeile 103), dass die vom Web-Client gesendete JSON-Zeichenkette in ein Objekt vom Typ [PostAjouterRv] deserialisiert wird. Dies geschieht wie folgt:
package rdvmedecins.web.models;
public class PostAjouterRv {
// pOST DATA
private String jour;
private long idClient;
private long idCreneau;
// getters and setters
...
}
Auch hier erfolgen die erforderlichen Typkonvertierungen automatisch;
- Die Zeilen 107–109 enthalten einen ähnlichen Mechanismus für die URL [/supprimerRv]. Die gesendete JSON-Zeichenkette lautet wie folgt:
und der Typ [PostSupprimerRv] lautet wie folgt:
package rdvmedecins.web.models;
public class PostSupprimerRv {
// pOST DATA
private long idRv;
// getters and setters
...
}
8.4.11.7. Die URL [/getAllDoctors]
Die URL [/getAllMedecins] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins() throws JsonProcessingException {
// the answer
Response<List<Medecin>> response;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// list of doctors
try {
response = new Response<>(0, null, application.getAllMedecins());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
}
// answer
return jsonMapper.writeValueAsString(response);
}
- Zeilen 9–10: Wir prüfen, ob die Anwendung korrekt initialisiert wurde (messages == null). Ist dies nicht der Fall, geben wir eine Antwort mit status = -1 und body = messages zurück;
- Zeile 13: Andernfalls fordern wir die Liste der Ärzte von der Klasse [ApplicationModel] an;
- Zeile 19: Wir senden die JSON-Zeichenkette der Antwort mithilfe des JSON-Mappers [jsonMapper], da die Klasse [Medecin] keinen JSON-Filter hat. Die Antwort kann fehlerfrei sein (Zeile 14) oder einen Fehler enthalten (Zeile 16). Die Methode [application.getAllMedecins()] löst keine Ausnahme aus, da sie lediglich eine zwischengespeicherte Liste zurückgibt. Dennoch behalten wir diese Ausnahmebehandlung bei, für den Fall, dass die Ärzte nicht mehr zwischengespeichert sind;
Wir haben den Fall, in dem die Anwendung falsch initialisiert wurde, noch nicht dargestellt. Stoppen wir das MySQL5-DBMS, starten wir den Webdienst und fordern wir dann die URL [/getAllMedecins] an:

Wir erhalten tatsächlich eine Fehlermeldung. Unter normalen Umständen erhalten wir die folgende Ansicht:
![]() |
8.4.11.8. Die URL [/getAllClients]
Die URL [/getAllClients] wird von der folgenden Methode im [RdvMedecinsController] verarbeitet:
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllClients() throws JsonProcessingException {
// the answer
Response<List<Client>> response;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// customer list
try {
response = new Response<>(0, null, application.getAllClients());
} catch (RuntimeException e) {
response = new Response<>(1, Static.getErreursForException(e), null);
}
// answer
return jsonMapper.writeValueAsString(response);
}
Sie ähnelt der Methode [getAllMedecins], die wir bereits behandelt haben. Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
8.4.11.9. Die URL [/getAllSlots/{doctorId}]
Die URL [/getAllSlots/{doctorId}] wird von der folgenden Methode des Controllers [RdvMedecinsController] verarbeitet:
// list of physician slots
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllCreneaux(@PathVariable("idMedecin") long idMedecin) throws JsonProcessingException {
// the answer
Response<List<Creneau>> response;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
}
// we get the doctor back
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
Medecin médecin = responseMedecin.getBody();
// doctor's slots
try {
response = new Response<>(0, null, application.getAllCreneaux(médecin.getId()));
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// answer
return jsonMapperShortCreneau.writeValueAsString(response);
}
- Zeile 12: Der durch den Parameter [id] identifizierte Arzt wird von einer lokalen Methode angefordert:
private Response<Medecin> getMedecin(long id) {
// we get the doctor back
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// existing doctor?
if (médecin == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
return new Response<Medecin>(2, messages, null);
}
// ok
return new Response<Medecin>(0, null, médecin);
}
Diese Methode gibt einen Statuswert im Bereich [0,1,2] zurück. Kehren wir zum Code für die Methode [getAllCreneaux] zurück:
- Zeilen 13–14: Wenn status ≠ 0 ist, erstellen wir eine Antwort mit einem Fehler;
- Zeile 16: Wir rufen den Arzt ab;
- Zeile 19: Wir rufen die Zeitfenster dieses Arztes ab;
- Zeile 25: Wir senden ein [List<Creneau>]-Objekt als Antwort. Erinnern wir uns an die Definition der Klasse [Creneau]:
@Entity
@Table(name = "creneaux")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of a RV slot
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// a slot is linked to a doctor
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// foreign key
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
...
}
- Zeile 13: Der Arzt wird im Modus [FetchType.LAZY] abgerufen;
Erinnern Sie sich an die JPQL-Abfrage, die die Methode [getAllCreneaux] in der [DAO]-Schicht implementiert:
@Query("select c from Creneau c where c.medecin.id=?1")
Die Notation [c.medecin.id] erzwingt eine Verknüpfung zwischen den Tabellen [CRENEAUX] und [MEDECINS]. Infolgedessen gibt die Abfrage alle Termine des Arztes zurück, wobei der Arzt in jedem einzelnen enthalten ist. Wenn wir diese Termine in JSON serialisieren, erscheint die JSON-Zeichenkette des Arztes in jedem einzelnen. Das ist unnötig. Um die Serialisierung zu steuern, benötigen wir zwei Dinge:
- Zugriff auf das zu serialisierende Objekt;
- Konfiguration des zu serialisierenden Objekts;
Punkt 1 wird dadurch gelöst, dass der für das Objekt geeignete JSON-Konverter in den Controller injiziert wird:
@Autowired
private ObjectMapper jsonMapperShortCreneau;
Punkt 2 wird erreicht, indem der im Projekt [rdvmedecins-metier-dao] definierten Klasse [Creneau] eine Annotation hinzugefügt wird:
![]() |
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
...
- Zeile 3: eine Annotation aus der Jackson-JSON-Bibliothek. Sie erstellt einen Filter namens [creneauFilter]. Mit diesem Filter können wir programmgesteuert festlegen, welche Felder serialisiert werden sollen und welche nicht;
Die Serialisierung des [Creneau]-Objekts erfolgt in der folgenden Zeile der [getAllCreneaux]-Methode:
// réponse
return jsonMapperShortCreneau.writeValueAsString(response);
Der JSON-Mapper [jsonMapperShortCreneau] wurde in der Klasse [WebConfig] wie folgt definiert:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
- Zeile 5: Der Filter mit dem Namen [creneauFilter] ist mit dem Filter [creneauFilter] aus Zeile 4 verknüpft. Dieser Filter serialisiert das [Creneau]-Objekt ohne dessen Feld [medecin];
Das von der Methode [getAllCreneaux] zurückgegebene Ergebnis ist eine JSON-Zeichenkette vom Typ [Response<List<Creneau>].
Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
oder diese, falls der Slot nicht existiert:
![]() |
Aus diesem Beispiel lässt sich folgende Regel ableiten:
- Webserver-/JSON-Methoden geben ein Objekt vom Typ [Response<T>] zurück, das in JSON serialisiert wird;
- wenn der Typ T einen oder mehrere JSON-Filter hat, wird ein Mapper mit denselben Filtern verwendet, um ihn zu serialisieren;
8.4.11.10. Die URL [/getRvMedecinJour/{idMedecin}/{jour}]
Die URL [/getRvMedecinJour/{idMedecin}/{jour}] wird von der folgenden Methode des Controllers [RdvMedecinsController] verarbeitet:
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// the answer
Response<List<Rv>> response=null;
boolean erreur = false;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// check the date
Date jourAgenda = null;
if (!erreur) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<List<Rv>>(3, messages, null);
erreur = true;
}
}
Response<Medecin> responseMedecin = null;
if (!erreur) {
// we get the doctor back
responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
Medecin médecin = responseMedecin.getBody();
// list of appointments
try {
response = new Response<>(0, null, application.getRvMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// answer
return jsonMapperLongRv.writeValueAsString(response);
}
- Wir müssen die JSON-Zeichenkette vom Typ [Response<List<Rv>>] zurückgeben. Die Klasse [Rv] verfügt über ein Feld [Rv.creneau]. Wenn dieses Feld serialisiert wird, stoßen wir auf den JSON-Filter [creneauFilter];
- Zeile 47: Das Objekt vom Typ [Response<List<Rv>>] aus Zeile 7 wird in JSON serialisiert;
Betrachten wir den Fall, in dem die Liste der Termine in Zeile 42 abgerufen wurde. Die Klasse [Rv] im Projekt [rdvmedecins-metier-dao] ist wie folgt definiert:
@Entity
@Table(name = "rv")
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of an Rv
@Temporal(TemporalType.DATE)
private Date jour;
// an appointment is linked to a customer
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_client")
private Client client;
// an appointment is linked to a time slot
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_creneau")
private Creneau creneau;
// foreign keys
@Column(name = "id_client", insertable = false, updatable = false)
private long idClient;
@Column(name = "id_creneau", insertable = false, updatable = false)
private long idCreneau;
...
}
- Zeile 11: Der Client wird im Modus [FetchType.LAZY] abgerufen;
- Zeile 18: Der Slot wird im Modus [FetchType.LAZY] abgerufen;
Erinnern wir uns an die JPQL-Abfrage, die die Termine abruft:
@Query("select rv from Rv rv left join fetch rv.client c left join fetch rv.creneau cr where cr.medecin.id=?1 and rv.jour=?2")
Joins werden explizit durchgeführt, um die Felder [client] und [slot] abzurufen. Darüber hinaus erhalten wir aufgrund des Joins [cr.doctor.id=?1] auch den Arzt. Der Arzt erscheint daher in der JSON-Zeichenkette für jeden Termin. Diese doppelten Informationen sind jedoch unnötig. Wir haben gesehen, wie dieses Problem mithilfe eines JSON-Filters auf das [Creneau]-Objekt gelöst werden kann. Aufgrund der [FetchType.LAZY]-Modi der Felder [client] und [slot] in der Klasse [Rv] werden wir bald feststellen, dass ein JSON-Filter auf die Klasse [RV] im Projekt [rdvmedecins-metier-dao] angewendet werden muss:
@Entity
@Table(name = "rv")
@JsonFilter("rvFilter")
public class Rv extends AbstractEntity {
...
Wir steuern die Serialisierung des [Rv]-Objekts mithilfe des [rvFilter]-Filters. Offensichtlich brauchen wir in diesem Fall keinen Filter, da wir alle Felder des [Rv]-Objekts benötigen. Da wir jedoch festgelegt haben, dass die Klasse über einen JSON-Filter verfügt, müssen wir diesen für jede Serialisierung eines Objekts vom Typ [Rv] definieren; andernfalls erhalten wir eine Ausnahme. Dazu verwenden wir den folgenden JSON-Mapper, der in der Klasse [rdvMedecinsController] definiert ist:
@Autowired
private ObjectMapper jsonMapperLongRv;
Dieser Mapper ist in der Konfigurationsklasse [WebConfig] wie folgt definiert:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
return jsonMapperLongRv;
}
- Zeile 4: Wir legen fest, dass alle Felder des [Rv]-Objekts serialisiert werden müssen;
- Zeile 5: Wir legen fest, dass im [Creneau]-Objekt das Feld [medecin] nicht serialisiert werden soll;
- Zeile 6: Wir fügen die beiden Filter [rvFilter] und [creneauFilter] zu den JSON-Filtern des Objekts [jsonMapperLongRv] hinzu;
Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
oder diese an einem Tag ohne Termin:
![]() |
oder diese mit einem falschen Tag:
![]() |
oder diese mit einem falschen Arzt:
![]() |
8.4.11.11. Die URL [/getAgendaMedecinJour/{idMedecin}/{jour}]
Die URL [/getAgendaMedecinJour/{idMedecin}/{jour}] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin)
throws JsonProcessingException {
// the answer
Response<AgendaMedecinJour> response = null;
boolean erreur = false;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// check the date
Date jourAgenda = null;
if (!erreur) {
// check the date
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
erreur = true;
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(3, messages, null);
}
}
// we get the doctor back
Medecin médecin = null;
if (!erreur) {
// we get the doctor back
Response<Medecin> responseMedecin = getMedecin(idMedecin);
if (responseMedecin.getStatus() != 0) {
response = new Response<>(responseMedecin.getStatus(), responseMedecin.getMessages(), null);
} else {
médecin = responseMedecin.getBody();
}
}
// get your diary back
if (!erreur) {
try {
response = new Response<>(0, null, application.getAgendaMedecinJour(médecin.getId(), jourAgenda));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(4, Static.getErreursForException(e1), null);
}
}
// answer
return jsonMapperLongRv.writeValueAsString(response);
}
- Zeilen 6, 49: Wir geben die JSON-Zeichenkette vom Typ [AgendaMedecinJour] zurück, die in einem [Response]-Objekt gekapselt ist;
Der Typ [AgendaMedecinJour] ist wie folgt definiert:
public class AgendaMedecinJour implements Serializable {
// fields
private Medecin medecin;
private Date jour;
private CreneauMedecinJour[] creneauxMedecinJour;
Der Typ [CreneauMedecinJour] sieht wie folgt aus:
public class CreneauMedecinJour implements Serializable {
private static final long serialVersionUID = 1L;
// fields
private Creneau creneau;
private Rv rv;
Die Felder [creneau] und [rv] verfügen über JSON-Filter, die konfiguriert werden müssen. Genau das geschieht in Zeile 49 der Methode [getAgendaMedecinJour] unter Verwendung des zuvor vorgestellten JSON-Mappers [jsonMapperLongRv]:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(
new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter", creneauFilter));
return jsonMapperLongRv;
}
Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
Oben sehen wir, dass Dr. PELISSIER am 28.01.2015 um 8:20 Uhr einen Termin mit Frau Brigitte BISTROU hat;
oder diese, falls das Datum falsch ist:
![]() |
oder diese, wenn die Arzt-ID ungültig ist:
![]() |
8.4.11.12. Die URL [/getMedecinById/{id}]
Die URL [/getMedecinById/{id}] wird von der folgenden Methode im [RdvMedecinsController] verarbeitet:
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getMedecinById(@PathVariable("id") long id) throws JsonProcessingException {
// the answer
Response<Medecin> response;
// application status
if (messages != null) {
response = new Response<Medecin>(-1, messages, null);
} else {
response = getMedecin(id);
}
// answer
return jsonMapper.writeValueAsString(response);
}
- Zeilen 5, 13: Die Methode gibt eine JSON-Zeichenkette vom Typ [Doctor] zurück. Dieser Typ hat keine JSON-Filter-Annotation. Daher wird in Zeile 14 der JSON-Mapper ohne Filter verwendet;
Zeile 10: Die Methode [getMedecin] lautet wie folgt:
private Response<Medecin> getMedecin(long id) {
// we get the doctor back
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (RuntimeException e1) {
return new Response<Medecin>(1, Static.getErreursForException(e1), null);
}
// existing doctor?
if (médecin == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le médecin d'id [%s] n'existe pas", id));
return new Response<Medecin>(2, messages, null);
}
// ok
return new Response<Medecin>(0, null, médecin);
}
Die Ergebnisse lauten wie folgt:
![]() |
oder diese, falls die Arzt-ID falsch ist:
![]() |
8.4.11.13. Die URL [/getClientById/{id}]
Die URL [/getClientById/{id}] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getClientById(@PathVariable("id") long id) throws JsonProcessingException {
// the answer
Response<Client> response;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
response = getClient(id);
}
// answer
return jsonMapper.writeValueAsString(response);
}
- Zeilen 5, 13: Die Methode gibt eine JSON-Zeichenkette vom Typ [Client] zurück. Dieser Typ hat keine JSON-Filter-Annotationen. Daher wird in Zeile 13 der JSON-Mapper ohne Filter verwendet;
Zeile 11: Die Methode [getClient] lautet wie folgt:
private Response<Client> getClient(long id) {
// we get the customer back
Client client = null;
try {
client = application.getClientById(id);
} catch (RuntimeException e1) {
return new Response<Client>(1, Static.getErreursForException(e1), null);
}
// existing customer?
if (client == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le client d'id [%s] n'existe pas", id));
return new Response<Client>(2, messages, null);
}
// ok
return new Response<Client>(0, null, client);
}
Die Ergebnisse lauten wie folgt:
![]() |
oder diese, falls die Kunden-ID falsch ist:
![]() |
8.4.11.14. Die URL [/getCreneauById/{id}]
Die URL [/getCreneauById/{id}] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getCreneauById(@PathVariable("id") long id) throws JsonProcessingException {
// the answer
Response<Creneau> response;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// we give back the slot
response = getCreneau(id);
}
// answer
return jsonMapperShortCreneau.writeValueAsString(response);
}
- Zeilen 5, 14: Die Methode gibt eine JSON-Zeichenkette vom Typ [Response<Creneau>] zurück;
Zeile 8: Die Methode [getCreneau] sieht wie folgt aus:
private Response<Creneau> getCreneau(long id) {
// we get the slot back
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (RuntimeException e1) {
return new Response<Creneau>(1, Static.getErreursForException(e1), null);
}
// existing niche?
if (créneau == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le créneau d'id [%s] n'existe pas", id));
return new Response<Creneau>(2, messages, null);
}
// ok
return new Response<Creneau>(0, null, créneau);
}
Sehen wir uns den Code für die Entität [Creneau] noch einmal an:
@Entity
@Table(name = "creneaux")
@JsonFilter("creneauFilter")
public class Creneau extends AbstractEntity {
private static final long serialVersionUID = 1L;
// characteristics of a RV slot
private int hdebut;
private int mdebut;
private int hfin;
private int mfin;
// a slot is linked to a doctor
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "id_medecin")
private Medecin medecin;
// foreign key
@Column(name = "id_medecin", insertable = false, updatable = false)
private long idMedecin;
- Zeilen 14–16: Da sich das Feld [doctor] im Modus [fetch = FetchType.LAZY] befindet, wird es beim Abrufen eines Slots über dessen [id] nicht abgerufen. Es ist daher notwendig, es von der Serialisierung auszuschließen. Ohne diesen Ausschluss tritt eine Ausnahme auf. Dies liegt daran, dass das Serialisierungsobjekt [mapper] die Methode [getMedecin] aufruft, um das Feld [medecin] abzurufen. Bei einer JPA/Hibernate-Implementierung gibt der Modus [fetch = FetchType.LAZY] des Feldes [medecin] jedoch ein [Creneau]-Objekt zurück, dessen Methode [getMedecin] so programmiert ist, dass sie den Arzt aus dem JPA-Kontext abruft. Dies wird als [Proxy]-Objekt bezeichnet. Erinnern wir uns nun an die Architektur der Webanwendung:
![]() |
Der Controller befindet sich im Block [Controllers / Actions]. Innerhalb dieses Blocks gilt das Konzept eines JPA-Kontexts nicht mehr. Der JPA-Kontext wird während der Operationen in der [DAO]-Schicht erstellt und besteht darüber hinaus nicht fort. Wenn der Controller also versucht, auf den JPA-Kontext zuzugreifen, tritt eine Ausnahme auf, die anzeigt, dass dieser geschlossen ist. Um diese Ausnahme zu vermeiden, müssen Sie die Serialisierung des Feldes [medecin] der Klasse [Rv] verhindern. Genau das leistet der JSON-Mapper [jsonMapperShortCreneau]:
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
oder diese, falls die Slot-Nummer falsch ist:
![]() |
8.4.11.15. Die URL [/getRvById/{id}]
Die URL [/getRvById/{id}] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvById(@PathVariable("id") long id) throws JsonProcessingException {
// the answer
Response<Rv> response;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
} else {
// we recover rv
response = getRv(id);
}
// answer
return jsonMapperShortRv.writeValueAsString(response);
}
- Zeilen 5, 14: Die Methode gibt eine JSON-Zeichenkette vom Typ [Response<Rv>] zurück;
Zeile 11: Die Methode [getRv] lautet wie folgt:
private Response<Rv> getRv(long id) {
// we recover the Rv
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (RuntimeException e1) {
return new Response<Rv>(1, Static.getErreursForException(e1), null);
}
// Existing Rv?
if (rv == null) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("Le rendez-vous d'id [%s] n'existe pas", id));
return new Response<Rv>(2, messages, null);
}
// ok
return new Response<Rv>(0, null, rv);
}
Die Klasse [Rv] verfügt über zwei Felder, die mit [fetch = FetchType.LAZY] annotiert sind: die Felder [creneau] und [client]. Diese Felder werden daher beim Abrufen eines [Rv] über seinen Primärschlüssel nicht abgerufen. Aus denselben Gründen wie zuvor müssen sie daher von der Serialisierung ausgeschlossen werden. Genau das leistet der folgende [jsonMapperShortRv]-Mapper, der in der [WebConfig]-Klasse definiert ist:
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
oder diese, falls die Terminnummer falsch ist:
![]() |
8.4.11.16. Die URL [/ajouterRv]
Die URL [/addAppt] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String ajouterRv(@RequestBody PostAjouterRv post) throws JsonProcessingException {
// the answer
Response<Rv> response = null;
boolean erreur = false;
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// retrieve posted values
String jour;
long idCreneau = -1;
long idClient = -1;
Date jourAgenda = null;
if (!erreur) {
// retrieve posted values
jour = post.getJour();
idCreneau = post.getIdCreneau();
idClient = post.getIdClient();
// check the date
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
List<String> messages = new ArrayList<String>();
messages.add(String.format("La date [%s] est invalide", jour));
response = new Response<>(6, messages, null);
erreur = true;
}
}
// we get the slot back
Response<Creneau> responseCréneau = null;
if (!erreur) {
// we get the slot back
responseCréneau = getCreneau(idCreneau);
if (responseCréneau.getStatus() != 0) {
erreur = true;
response = new Response<>(responseCréneau.getStatus(), responseCréneau.getMessages(), null);
}
}
// we get the customer back
Response<Client> responseClient = null;
Creneau créneau = null;
if (!erreur) {
créneau = (Creneau) responseCréneau.getBody();
// we get the customer back
responseClient = getClient(idClient);
if (responseClient.getStatus() != 0) {
erreur = true;
response = new Response<>(responseClient.getStatus() + 2, responseClient.getMessages(), null);
}
}
if (!erreur) {
Client client = responseClient.getBody();
// we add the Rv
try {
response = new Response<>(0, null, application.ajouterRv(jourAgenda, créneau, client));
} catch (RuntimeException e1) {
erreur = true;
response = new Response<>(5, Static.getErreursForException(e1), null);
}
}
// answer
return jsonMapperLongRv.writeValueAsString(response);
}
- Zeilen 5, 67: Die Methode muss eine JSON-Zeichenkette vom Typ [Response<Rv>] zurückgeben;
- Zeile 3: Die Annotation [@RequestBody PostAjouterRv post] ruft den POST-Body ab und speichert ihn im Parameter [PostAjouterRv post]. Dieser Body ist JSON [consumes = "application/json; charset=UTF-8"], das automatisch in den folgenden Typ [PostAjouterRv] deserialisiert wird:
public class PostAjouterRv {
// pOST DATA
private String jour;
private long idClient;
private long idCreneau;
...
- dann folgt Code, der in der einen oder anderen Form bereits aufgetaucht ist;
- Zeile 67: Einrichtung der JSON-Filter [creneauFilter] und [rvFilter]. Die Methode gibt eine JSON-Zeichenkette vom Typ [Response<Rv>] zurück, wobei Rv in Zeile 61 abgerufen wurde. Das [Rv]-Objekt kapselt sowohl ein [Creneau]-Objekt als auch ein [Client]-Objekt. Das [Creneau]-Objekt hat eine [FetchType.LAZY]-Abhängigkeit von einem [Medecin]-Objekt und wurde in den Zeilen 36–44 abgerufen. Es wurde über seinen Primärschlüssel aus dem JPA-Kontext abgerufen und ohne seine [FetchType.LAZY]-Abhängigkeit abgerufen. Letztendlich
- verfügt das [Rv]-Objekt über alle seine Abhängigkeiten. Diese können serialisiert werden;
- das [Creneau]-Objekt verfügt nicht über seine [medecin]-Abhängigkeit. Daher darf diese Abhängigkeit nicht serialisiert werden;
Der in der Klasse [WebConfig] definierte JSON-Mapper [jsonMapperLongRv] erfüllt diese Einschränkungen:
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",creneauFilter));
return jsonMapperLongRv;
}
Die mit dem [Advanced Rest Client]-Client erzielten Ergebnisse sehen wie folgt aus:
![]() |
- in [1] die POST-URL;
- in [2] die POST-Anfrage;
- in [3] der übermittelte Wert;
- in [4a] ist dieser übermittelte Wert JSON;
![]() |
- in [4b] gibt der Client an, dass er JSON sendet;
- in [5] gibt der Server an, dass er JSON zurückgibt;
![]() |
- in [6] die JSON-Antwort des Servers, die den hinzugefügten Termin darstellt. Sie enthält die ID [id] des hinzugefügten Termins;
Bei einer nicht existierenden Slot-Nummer erhalten wir Folgendes:
![]() |
8.4.11.17. Die URL [/deleteAppointment]
Die URL [/deleteAppointment] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post) throws JsonProcessingException {
// the answer
Response<Void> response = null;
boolean erreur = false;
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// application status
if (messages != null) {
response = new Response<>(-1, messages, null);
erreur = true;
}
// retrieve posted values
long idRv = post.getIdRv();
// recovering the rv
if (!erreur) {
Response<Rv> responseRv = getRv(idRv);
if (responseRv.getStatus() != 0) {
response = new Response<>(responseRv.getStatus(), responseRv.getMessages(), null);
erreur = true;
}
}
if (!erreur) {
// rv deletion
try {
application.supprimerRv(idRv);
response = new Response<Void>(0, null, null);
} catch (RuntimeException e1) {
response = new Response<>(3, Static.getErreursForException(e1), null);
}
}
// answer
return jsonMapper.writeValueAsString(response);
}
- Zeile 5: Der Typ [Void] ist die Klasse, die dem primitiven Typ [void] entspricht;
- Zeilen 5, 34: Die Methode gibt eine JSON-Zeichenkette vom Typ [Response<Void>] zurück, die keine JSON-Filter enthält. Daher verwenden wir in Zeile 34 den JSON-Mapper ohne Filter;
- Zeile 3: Die Methode nimmt den POST-Body als Parameter entgegen, d. h. den gesendeten Wert. Dieser wird im JSON-Format [content-type="application/json; charset=UTF-8"] empfangen und automatisch in den folgenden Typ [PostSupprimerRv] deserialisiert:
public class PostSupprimerRv {
// pOST DATA
private long idRv;
- Zeile 28: Wenn das Löschen erfolgreich war, wird eine Antwort mit [status=0] gesendet;
Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
![]() |
- in [5] gibt das Feld [status=0] an, dass die Löschung erfolgreich war;
Bei einer nicht existierenden Termin-ID erhalten wir Folgendes:
![]() |
Wir sind mit dem Controller fertig. Schauen wir uns nun an, wie das Projekt ausgeführt wird.
8.4.11.18. Die ausführbare Klasse des Webdienstes
![]() |
Die Klasse [Boot] [1] sieht wie folgt aus:
package rdvmedecins.web.boot;
import org.springframework.boot.SpringApplication;
import rdvmedecins.web.config.AppConfig;
public class Boot {
public static void main(String[] args) {
SpringApplication.run(AppConfig.class, args);
}
}
Zeile 10: Die statische Methode [SpringApplication.run] wird mit der Projektkonfigurationsklasse [AppConfig] als erstem Parameter ausgeführt. Diese Methode konfiguriert das Projekt automatisch, startet den in den Abhängigkeiten eingebetteten Tomcat-Server und stellt den Controller [RdvMedecinsController] darauf bereit.
Die Protokollierung wird durch die folgenden Dateien gesteuert [2]:
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- log level control -->
<root level="info"> <!-- off, info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
- Zeile 9: Die allgemeine Protokollstufe ist auf [info] gesetzt;
[application.properties]
logging.level.org.springframework.web=INFO
logging.level.org.hibernate=OFF
spring.main.show-banner=false
Die Zeilen 1–2 legen eine bestimmte Protokollierungsstufe für bestimmte Teile der Anwendung fest:
- Zeile 1: Wir möchten Protokolle aus der [Web]-Schicht;
- Zeile 2: Wir möchten keine Protokolle aus der [JPA]-Schicht;
- Zeile 3: kein Spring Boot-Banner;
Die Protokolle während der Ausführung sehen wie folgt aus:
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.groovy]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml]
11:06:04,279 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback.xml] at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs multiple times on the classpath.
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-metier-dao/target/classes/logback.xml]
11:06:04,279 |-WARN in ch.qos.logback.classic.LoggerContext[default] - Resource [logback.xml] occurs at [file:/D:/data/istia-1516/projets/springmvc-thymeleaf/dvp-final/etude-de-cas/rdvmedecins-webjson-server/target/classes/logback.xml]
11:06:04,342 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - debug attribute not set
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender]
11:06:04,342 |-INFO in ch.qos.logback.core.joran.action.AppenderAction - Naming appender as [STDOUT]
11:06:04,357 |-INFO in ch.qos.logback.core.joran.action.NestedComplexPropertyIA - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.RootLoggerAction - Setting level of ROOT logger to INFO
11:06:04,404 |-INFO in ch.qos.logback.core.joran.action.AppenderRefAction - Attaching appender named [STDOUT] to Logger[ROOT]
11:06:04,404 |-INFO in ch.qos.logback.classic.joran.action.ConfigurationAction - End of configuration.
11:06:04,420 |-INFO in ch.qos.logback.classic.joran.JoranConfigurator@56f4468b - Registering current configuration as safe fallback point
11:06:04.732 [main] INFO rdvmedecins.web.boot.Boot - Starting Boot on Gportpers3 with PID 420 (D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server\target\classes started by usrlocal in D:\data\istia-1516\projets\springmvc-thymeleaf\dvp-final\etude-de-cas\rdvmedecins-webjson-server)
11:06:04.775 [main] INFO o.s.b.c.e.AnnotationConfigEmbeddedWebApplicationContext - Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:05.538 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat initialized with port(s): 8080 (http)
11:06:05.688 [main] INFO o.a.catalina.core.StandardService - Starting service Tomcat
11:06:05.689 [main] INFO o.a.catalina.core.StandardEngine - Starting Servlet Engine: Apache Tomcat/8.0.26
11:06:05.833 [localhost-startStop-1] INFO o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring embedded WebApplicationContext
11:06:05.833 [localhost-startStop-1] INFO o.s.web.context.ContextLoader - Root WebApplicationContext: initialization completed in 1061 ms
11:06:06.231 [localhost-startStop-1] INFO o.s.o.j.LocalContainerEntityManagerFactoryBean - Building JPA container EntityManagerFactory for persistence unit 'default'
11:06:09.234 [localhost-startStop-1] INFO o.s.s.web.DefaultSecurityFilterChain - Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@12d14fa, org.springframework.security.web.context.SecurityContextPersistenceFilter@29823fb6, org.springframework.security.web.header.HeaderWriterFilter@662d93b2, org.springframework.security.web.authentication.logout.LogoutFilter@2d81ee0, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52aa47ad, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@60bd7a74, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5a374232, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7ddb4452, org.springframework.security.web.session.SessionManagementFilter@2cd9855f, org.springframework.security.web.access.ExceptionTranslationFilter@2263f0a2, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@192ce7f6]
11:06:09.255 [localhost-startStop-1] INFO o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/*]
11:06:09.255 [localhost-startStop-1] INFO o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'springSecurityFilterChain' to: [/*]
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/authenticate],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.authenticate(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAgendaMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAgendaMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllCreneaux/{idMedecin}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getAllCreneaux(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvMedecinJour/{idMedecin}/{jour}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvMedecinJour(long,java.lang.String,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getMedecinById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Medecin> rdvmedecins.web.controllers.RdvMedecinsController.getMedecinById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getClientById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<rdvmedecins.entities.Client> rdvmedecins.web.controllers.RdvMedecinsController.getClientById(long,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/supprimerRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.Void> rdvmedecins.web.controllers.RdvMedecinsController.supprimerRv(rdvmedecins.web.models.PostSupprimerRv,javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllClients],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Client>> rdvmedecins.web.controllers.RdvMedecinsController.getAllClients(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/ajouterRv],methods=[POST],consumes=[application/json;charset=UTF-8]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.ajouterRv(rdvmedecins.web.models.PostAjouterRv,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getCreneauById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getCreneauById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getAllMedecins],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.util.List<rdvmedecins.entities.Medecin>> rdvmedecins.web.controllers.RdvMedecinsController.getAllMedecins(javax.servlet.http.HttpServletResponse,java.lang.String)
11:06:09.536 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerMapping - Mapped "{[/getRvById/{id}],methods=[GET]}" onto public rdvmedecins.web.models.Response<java.lang.String> rdvmedecins.web.controllers.RdvMedecinsController.getRvById(long,javax.servlet.http.HttpServletResponse,java.lang.String) throws com.fasterxml.jackson.core.JsonProcessingException
...
11:06:09.677 [main] INFO o.s.w.s.m.m.a.RequestMappingHandlerAdapter - Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@2ea6137: startup date [Wed Oct 14 11:06:04 CEST 2015]; root of context hierarchy
11:06:09.770 [main] INFO o.a.coyote.http11.Http11NioProtocol - Initializing ProtocolHandler ["http-nio-8080"]
11:06:09.786 [main] INFO o.a.coyote.http11.Http11NioProtocol - Starting ProtocolHandler ["http-nio-8080"]
11:06:09.802 [main] INFO o.a.tomcat.util.net.NioSelectorPool - Using a shared selector for servlet write/read
11:06:09.817 [main] INFO o.s.b.c.e.t.TomcatEmbeddedServletContainer - Tomcat started on port(s): 8080 (http)
11:06:09.817 [main] INFO rdvmedecins.web.boot.Boot - Started Boot in 5.319 seconds (JVM running for 6.053)
- Zeile 18: Der Tomcat-Server ist aktiv;
- Zeile 21: Der Spring-Kontext wird initialisiert;
- Zeilen 27–38: Die vom Webdienst bereitgestellten URLs werden ermittelt;
- Zeile 44: Der Tomcat-Server ist bereit und wartet auf Anfragen am Port 8080;
Wenn wir die Datei [application.properties] wie folgt ändern:
logging.level.org.springframework.web: OFF
logging.level.org.hibernate:OFF
spring.main.show-banner=false
erhalten wir die folgenden Protokolle:
Wenn wir außerdem die Datei [logback.xml] wie folgt ändern:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- log level control -->
<root level="off"> <!-- off, info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
Die folgenden Protokolle werden ausgegeben:
Wir sehen also, dass wir eine gewisse Kontrolle über die Protokolle haben, die in der Konsole angezeigt werden. Die Stufe [info] ist oft die geeignete Protokollstufe.
Wir verfügen nun über einen funktionsfähigen Webdienst, der über einen Webclient abgefragt werden kann. Wir werden uns nun mit der Absicherung dieses Dienstes befassen: Wir möchten, dass nur bestimmte Personen Arzttermine verwalten können. Dazu verwenden wir das Spring Security-Framework, eine Komponente des Spring-Ökosystems.
8.4.12. Einführung in Spring Security
Wir werden erneut ein Spring-Handbuch importieren, indem wir die folgenden Schritte 1 bis 3 ausführen:
![]() |
![]() |
Das Projekt umfasst folgende Elemente:
- Im Ordner [templates] finden Sie die HTML-Seiten des Projekts;
- [Application]: ist die ausführbare Klasse des Projekts;
- [MvcConfig]: ist die Spring-MVC-Konfigurationsklasse;
- [WebSecurityConfig]: ist die Spring Security-Konfigurationsklasse;
8.4.12.1. Maven-Konfiguration
Projekt [3] ist ein Maven-Projekt. Sehen wir uns die Datei [pom.xml] an, um die Abhängigkeiten zu überprüfen:
<?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-securing-web</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.10.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- tag::security[] -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- end::security[] -->
</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>
</project>
- Zeilen 10–14: Das Projekt ist ein Spring Boot-Projekt;
- Zeilen 17–20: Abhängigkeit vom [Thymeleaf]-Framework;
- Zeilen 22–25: Abhängigkeit vom Spring Security-Framework;
8.4.12.2. Thymeleaf-Ansichten
![]() |
Die Ansicht [home.html] sieht wie folgt aus:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click <a th:href="@{/hello}">here</a> to see a greeting.
</p>
</body>
</html>
- Zeile 12: Das Attribut [th:href="@{/hello}"] generiert das [href]-Attribut des Tags <a>. Der Wert [@{/hello}] generiert den Pfad [<context>/hello], wobei [context] der Kontext der Webanwendung ist;
Der generierte HTML-Code lautet wie folgt:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<h1>Welcome!</h1>
<p>
Click
<a href="/hello">here</a>
to see a greeting.
</p>
</body>
</html>
Die Ansicht [hello.html] sieht wie folgt aus:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
</body>
</html>
- Zeile 9: Das Attribut [th:inline="text"] generiert den Text des Tags <h1>. Dieser Text enthält einen $-Ausdruck, der ausgewertet werden muss. Das Element [[${#httpServletRequest.remoteUser}]] ist der Wert des Attributs [RemoteUser] der aktuellen HTTP-Anfrage. Dies ist der Name des angemeldeten Benutzers;
- Zeile 10: ein HTML-Formular. Das Attribut [th:action="@{/logout}"] generiert das Attribut [action] des Tags [form]. Der Wert [@{/logout}] generiert den Pfad [<context>/logout], wobei [context] der Kontext der Webanwendung ist;
Der generierte HTML-Code lautet wie folgt:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
<h1>Hello user!</h1>
<form method="post" action="/logout">
<input type="submit" value="Sign Out" />
<input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
</form>
</body>
</html>
- Zeile 8: die Übersetzung von „Hallo [[${#httpServletRequest.remoteUser}]]!“;
- Zeile 9: die Übersetzung von @{/logout};
- Zeile 11: ein verstecktes Feld mit dem Namen (Attribut „name“) _csrf;
Die endgültige Ansicht [login.html] sieht wie folgt aus:
![]() |
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
<div th:if="${param.error}">Invalid username and password.</div>
<div th:if="${param.logout}">You have been logged out.</div>
<form th:action="@{/login}" method="post">
<div>
<label> User Name : <input type="text" name="username" />
</label>
</div>
<div>
<label> Password: <input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
</form>
</body>
</html>
- Zeile 9: Das Attribut [th:if="${param.error}"] stellt sicher, dass das <div>-Tag nur generiert wird, wenn die URL, die die Anmeldeseite anzeigt, den Parameter [error] enthält (http://context/login?error);
- Zeile 10: Das Attribut [th:if="${param.logout}"] stellt sicher, dass das <div>-Tag nur generiert wird, wenn die URL, die die Anmeldeseite anzeigt, den Parameter [logout] enthält (http://context/login?logout);
- Zeilen 11–23: ein HTML-Formular;
- Zeile 11: Das Formular wird an die URL [<context>/login] gesendet, wobei <context> der Kontext der Webanwendung ist;
- Zeile 13: ein Eingabefeld mit dem Namen [username];
- Zeile 17: ein Eingabefeld mit dem Namen [password];
Der generierte HTML-Code lautet wie folgt:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example </title>
</head>
<body>
<div>
You have been logged out.
</div>
<form method="post" action="/login">
<div>
<label>
User Name :
<input type="text" name="username" />
</label>
</div>
<div>
<label>
Password:
<input type="password" name="password" />
</label>
</div>
<div>
<input type="submit" value="Sign In" />
</div>
<input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
</form>
</body>
</html>
Beachten Sie in Zeile 28, dass Thymeleaf ein verstecktes Feld namens [_csrf] hinzugefügt hat.
8.4.12.3. Spring MVC-Konfiguration
![]() |
Die Klasse [MvcConfig] konfiguriert das Spring MVC-Framework:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home").setViewName("home");
registry.addViewController("/").setViewName("home");
registry.addViewController("/hello").setViewName("hello");
registry.addViewController("/login").setViewName("login");
}
}
- Zeile 7: Die Annotation [@Configuration] macht die Klasse [MvcConfig] zu einer Konfigurationsklasse;
- Zeile 8: Die Klasse [MvcConfig] erweitert die Klasse [WebMvcConfigurerAdapter], um bestimmte Methoden zu überschreiben;
- Zeile 10: Neudefinition einer Methode aus der übergeordneten Klasse;
- Zeilen 11–16: Die Methode [addViewControllers] ermöglicht die Zuordnung von URLs zu HTML-Ansichten. Dort werden folgende Zuordnungen vorgenommen:
URL | Ansicht |
/templates/home.html | |
/templates/hello.html | |
/templates/login.html |
Die Endung [html] und der Ordner [templates] sind die von Thymeleaf verwendeten Standardwerte. Sie können über die Konfiguration geändert werden. Der Ordner [templates] muss sich im Stammverzeichnis des Klassenpfads des Projekts befinden:
![]() |
In [1] oben sind die Ordner [java] und [resources] beide Quellordner. Das bedeutet, dass sich ihr Inhalt im Stammverzeichnis des Klassenpfads des Projekts befindet. Daher befinden sich in [2] die Ordner [hello] und [templates] im Stammverzeichnis des Klassenpfads.
8.4.12.4. Spring Security-Konfiguration
![]() |
Die Klasse [WebSecurityConfig] konfiguriert das Spring Security-Framework:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
- Zeile 9: Die Annotation [@Configuration] macht die Klasse [WebSecurityConfig] zu einer Konfigurationsklasse;
- Zeile 10: Die Annotation [@EnableWebSecurity] macht die Klasse [WebSecurityConfig] zu einer Spring Security-Konfigurationsklasse;
- Zeile 11: Die Klasse [WebSecurity] erweitert die Klasse [WebSecurityConfigurerAdapter], um bestimmte Methoden zu überschreiben;
- Zeile 12: Neudefinition einer Methode aus der übergeordneten Klasse;
- Zeilen 13–16: Die Methode [configure(HttpSecurity http)] wird überschrieben, um Zugriffsrechte für die verschiedenen URLs der Anwendung zu definieren;
- Zeile 14: Die Methode [http.authorizeRequests()] ermöglicht es, URLs mit Zugriffsrechten zu verknüpfen. Dort werden folgende Zuordnungen vorgenommen:
URL | Regel | Code |
Zugriff ohne Authentifizierung | | |
Nur authentifizierter Zugriff |
- Zeile 15: definiert die Authentifizierungsmethode. Die Authentifizierung erfolgt über ein URL-Formular [/login], das für alle zugänglich ist [http.formLogin().loginPage("/login").permitAll()]. Auch die Abmeldung ist für alle zugänglich;
- Zeilen 19–21: definieren die Methode [configure(AuthenticationManagerBuilder auth)] neu, die Benutzer verwaltet;
- Zeile 20: Die Authentifizierung erfolgt über fest programmierte Benutzer [auth.inMemoryAuthentication()]. Ein Benutzer wird hier mit dem Benutzernamen [user], dem Passwort [password] und der Rolle [USER] definiert. Benutzern mit derselben Rolle können dieselben Berechtigungen erteilt werden;
8.4.12.5. Ausführbare Klasse
![]() |
Die Klasse [Application] sieht wie folgt aus:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {
public static void main(String[] args) throws Throwable {
SpringApplication.run(Application.class, args);
}
}
- Zeile 8: Die Annotation [@EnableAutoConfiguration] weist Spring Boot (Zeile 3) an, die Konfiguration durchzuführen, die der Entwickler nicht explizit eingerichtet hat;
- Zeile 9: macht die Klasse [Application] zu einer Spring-Konfigurationsklasse;
- Zeile 10: weist das System an, das Verzeichnis mit der [Application]-Klasse nach Spring-Komponenten zu durchsuchen. Die beiden Klassen [MvcConfig] und [WebSecurityConfig] werden somit gefunden, da sie die Annotation [@Configuration] tragen;
- Zeile 13: die [main]-Methode der ausführbaren Klasse;
- Zeile 14: Die statische Methode [SpringApplication.run] wird mit der Konfigurationsklasse [Application] als Parameter ausgeführt. Wir sind diesem Vorgang bereits begegnet und wissen, dass der in den Maven-Abhängigkeiten des Projekts eingebettete Tomcat-Server gestartet und das Projekt darauf bereitgestellt wird. Wir haben gesehen, dass vier URLs verarbeitet wurden [/, /home, /login, /hello] und dass einige durch Zugriffsrechte geschützt waren.
8.4.12.6. Testen der Anwendung
Beginnen wir mit der Anfrage der URL [/], die eine der vier akzeptierten URLs ist. Sie ist mit der Ansicht [/templates/home.html] verknüpft:
![]() |
Die angeforderte URL [/] ist für jeden zugänglich. Deshalb konnten wir sie abrufen. Der Link [hier] lautet wie folgt:
Die URL [/hello] wird aufgerufen, wenn wir auf den Link klicken. Diese ist geschützt:
URL | Regel | Code |
Zugriff ohne Authentifizierung | | |
Nur authentifizierter Zugriff |
Sie müssen authentifiziert sein, um darauf zugreifen zu können. Spring Security leitet den Browser des Clients dann auf die Authentifizierungsseite weiter. Basierend auf der gezeigten Konfiguration ist dies die Seite unter der URL [/login]. Diese Seite ist für alle zugänglich:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Wir erhalten also [1]:
![]() |
Der Quellcode für die resultierende Seite lautet wie folgt:
- In Zeile 7 erscheint ein verstecktes Feld, das auf der ursprünglichen Seite [login.html] nicht vorhanden ist. Thymeleaf hat es hinzugefügt. Dieser Code, bekannt als CSRF (Cross-Site Request Forgery), dient dazu, eine Sicherheitslücke zu schließen. Dieses Token muss zusammen mit der Authentifizierung an Spring Security zurückgesendet werden, damit es akzeptiert wird;
Wir erinnern uns, dass von Spring Security nur das Paar aus Benutzername und Passwort erkannt wird. Wenn wir in [2] etwas anderes eingeben, erhalten wir dieselbe Seite mit einer Fehlermeldung in [3]. Spring Security hat den Browser auf die URL [http://localhost:8080/login?error] umgeleitet. Das Vorhandensein des Parameters [error] hat die Anzeige des Tags ausgelöst:
<div th:if="${param.error}">Invalid username and password.</div>
Geben wir nun die erwarteten Benutzer-/Passwort-Werte ein [4]:
![]() |
- In [4] melden wir uns an;
- In [5] leitet Spring Security uns zur URL [/hello] weiter, da dies die URL ist, die wir angefordert hatten, als wir zur Anmeldeseite weitergeleitet wurden. Die Identität des Benutzers wurde durch die folgende Zeile in [hello.html] angezeigt:
<h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
Seite [5] zeigt das folgende Formular an:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out" />
</form>
Wenn Sie auf die Schaltfläche [Abmelden] klicken, wird eine POST-Anfrage an die URL [/logout] gesendet. Wie die URL [/login] ist auch diese URL für jeden zugänglich:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
In unserer URL-/View-Zuordnung haben wir für die URL [/logout] nichts definiert. Was wird passieren? Probieren wir es aus:
![]() |
- In [6] klicken wir auf die Schaltfläche [Abmelden];
- in [7] sehen wir, dass wir zur URL [http://localhost:8080/login?logout] weitergeleitet wurden. Spring Security hat diese Weiterleitung angefordert. Das Vorhandensein des Parameters [logout] in der URL führte dazu, dass die folgende Zeile in der Ansicht angezeigt wurde:
<div th:if="${param.logout}">You have been logged out.</div>
8.4.12.7. Fazit
Im vorangegangenen Beispiel hätten wir die Webanwendung zuerst schreiben und sie später sichern können. Spring Security ist nicht-intrusiv. Sie können Sicherheit für eine bereits geschriebene Webanwendung implementieren. Darüber hinaus haben wir folgende Punkte festgestellt:
- Es ist möglich, eine Authentifizierungsseite zu definieren;
- die Authentifizierung muss mit dem von Spring Security ausgegebenen CSRF-Token einhergehen;
- Wenn die Authentifizierung fehlschlägt, werden Sie mit einem zusätzlichen Fehlerparameter in der URL auf die Authentifizierungsseite weitergeleitet;
- Wenn die Authentifizierung erfolgreich ist, werden Sie zu der Seite weitergeleitet, die zum Zeitpunkt der Authentifizierung angefordert wurde. Wenn Sie die Authentifizierungsseite direkt aufrufen, ohne eine Zwischenseite zu durchlaufen, leitet Spring Security Sie zur URL [/] weiter (dieser Fall wurde nicht demonstriert);
- Sie melden sich ab, indem Sie die URL [/logout] mit einer POST-Anfrage aufrufen. Spring Security leitet Sie dann mit dem Parameter „logout“ in der URL zur Authentifizierungsseite weiter;
Alle diese Schlussfolgerungen basieren auf dem Standardverhalten von Spring Security. Dieses Verhalten kann durch Konfiguration geändert werden, indem bestimmte Methoden der Klasse [WebSecurityConfigurerAdapter] überschrieben werden.
Das vorherige Tutorial wird uns im weiteren Verlauf kaum helfen. Wir werden stattdessen Folgendes verwenden:
- eine Datenbank zum Speichern von Benutzern, deren Passwörtern und deren Rollen;
- eine HTTP-Header-basierte Authentifizierung;
Es gibt relativ wenige Tutorials für das, was wir hier tun wollen. Die Lösung, die wir vorschlagen, ist eine Kombination aus Code-Schnipseln, die wir hier und da gefunden haben.
8.4.13. Implementierung von Sicherheitsmaßnahmen im Termin-Webservice
8.4.13.1. Die Datenbank
Die Datenbank [rdvmedecins] wird aktualisiert, um Benutzer, deren Passwörter und deren Rollen aufzunehmen. Es werden drei neue Tabellen hinzugefügt:

Tabelle [USERS]: Benutzer
- ID: Primärschlüssel;
- VERSION: Spalte für die Zeilenversionierung;
- IDENTITY: eine beschreibende Kennung für den Benutzer;
- LOGIN: der Benutzername des Benutzers;
- PASSWORD: sein Passwort;
In der Tabelle USERS werden Passwörter nicht im Klartext gespeichert:
![]() |
Der zur Verschlüsselung von Passwörtern verwendete Algorithmus ist der BCRYPT-Algorithmus.
Tabelle [ROLES]: Rollen
- ID: Primärschlüssel;
- VERSION: Versionsspalte für die Zeile;
- NAME: Rollenname. Standardmäßig erwartet Spring Security Namen im Format ROLE_XX, zum Beispiel ROLE_ADMIN oder ROLE_GUEST;
![]() |
Tabelle [USERS_ROLES]: Verknüpfungstabelle zwischen USERS und ROLES
Ein Benutzer kann mehrere Rollen haben, und eine Rolle kann mehrere Benutzer umfassen. Dies ist eine Viele-zu-Viele-Beziehung, die durch die Tabelle [USERS_ROLES] dargestellt wird.
- ID: Primärschlüssel;
- VERSION: Spalte für die Zeilenversionierung;
- USER_ID: Benutzer-ID;
- ROLE_ID: Kennung einer Rolle;
![]() |
Da wir die Datenbank ändern, müssen alle Schichten des Projekts [Geschäftslogik, DAO, JPA] angepasst werden:
![]() |
8.4.13.2. Das neue STS-Projekt für [Geschäftslogik, DAO, JPA]
Das Projekt [rdvmedecins-business-dao] entwickelt sich wie folgt:
![]() |
- in [1]: das neue Projekt;
- in [2]: Die durch die Implementierung der Sicherheitsfunktionen eingeführten Änderungen wurden in einem einzigen Paket [rdvmedecins.security] zusammengefasst. Diese neuen Elemente gehören zu den Schichten [JPA] und [DAO], wurden jedoch der Einfachheit halber in demselben Paket zusammengefasst.
8.4.13.3. Die neuen [JPA]-Entitäten
![]() |
Die JPA-Schicht definiert drei neue Entitäten:
![]() |
Die Klasse [User] repräsentiert die Tabelle [USERS]:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {
private static final long serialVersionUID = 1L;
// properties
private String identity;
private String login;
private String password;
// manufacturer
public User() {
}
public User(String identity, String login, String password) {
this.identity = identity;
this.login = login;
this.password = password;
}
// identity
@Override
public String toString() {
return String.format("User[%s,%s,%s]", identity, login, password);
}
// getters and setters
....
}
- Zeile 9: Die Klasse erweitert die Klasse [AbstractEntity], die bereits für die anderen Entitäten verwendet wird;
- Zeilen 13–15: Es werden keine Spaltennamen angegeben, da diese dieselben Namen wie die zugehörigen Felder haben;
Die Klasse [Role] spiegelt die Tabelle [ROLES] wider:
package rdvmedecins.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {
private static final long serialVersionUID = 1L;
// properties
private String name;
// manufacturers
public Role() {
}
public Role(String name) {
this.name = name;
}
// identity
@Override
public String toString() {
return String.format("Role[%s]", name);
}
// getters and setters
...
}
Die Klasse [UserRole] repräsentiert die Tabelle [USERS_ROLES]:
package rdvmedecins.entities;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {
private static final long serialVersionUID = 1L;
// a UserRole refers to a User
@ManyToOne
@JoinColumn(name = "USER_ID")
private User user;
// a UserRole refers to a Role
@ManyToOne
@JoinColumn(name = "ROLE_ID")
private Role role;
// getters and setters
...
}
- Zeilen 15–17: Definieren Sie den Fremdschlüssel von der Tabelle [USERS_ROLES] zur Tabelle [USERS];
- Zeilen 19–21: Implementieren Sie den Fremdschlüssel von der Tabelle [USERS_ROLES] zur Tabelle [ROLES];
8.4.13.4. Änderungen an der [DAO]-Schicht
![]() |
Die [DAO]-Schicht wird um drei neue [Repositorys] erweitert:
![]() |
Die Schnittstelle [UserRepository] verwaltet den Zugriff auf [User]-Entitäten:
package rdvmedecins.repositories;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import rdvmedecins.entities.Role;
import rdvmedecins.entities.User;
public interface UserRepository extends CrudRepository<User, Long> {
// liste des rôles d'un utilisateur identifié par son id
@Query("select ur.role from UserRole ur where ur.user.id=?1")
Iterable<Role> getRoles(long id);
// liste des rôles d'un utilisateur identifié par son login et son mot de passe
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
Iterable<Role> getRoles(String login, String password);
// recherche d'un utilisateur via son login
User findUserByLogin(String login);
}
- Zeile 9: Die Schnittstelle [UserRepository] erweitert die Spring Data-Schnittstelle [CrudRepository] (Zeile 4);
- Zeilen 12–13: Die Methode [getRoles(User user)] ruft alle Rollen für einen Benutzer ab, der durch seine [id] identifiziert wird
- Zeilen 16–17: wie oben, jedoch für einen Benutzer, der durch seinen Benutzernamen und sein Passwort identifiziert wird;
- Zeile 20: Um einen Benutzer anhand seines Logins zu finden;
Die Schnittstelle [RoleRepository] verwaltet den Zugriff auf [Role]-Entitäten:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface RoleRepository extends CrudRepository<Role, Long> {
// search for a role by name
Role findRoleByName(String name);
}
- Zeile 5: Die Schnittstelle [RoleRepository] erweitert die Schnittstelle [CrudRepository];
- Zeile 8: Sie können nach einer Rolle anhand ihres Namens suchen;
Die Schnittstelle [userRoleRepository] verwaltet den Zugriff auf [UserRole]-Entitäten:
package rdvmedecins.security;
import org.springframework.data.repository.CrudRepository;
public interface UserRoleRepository extends CrudRepository<UserRole, Long> {
}
- Zeile 5: Die Schnittstelle [UserRoleRepository] erweitert lediglich die Schnittstelle [CrudRepository], ohne neue Methoden hinzuzufügen;
8.4.13.5. Klassen zur Benutzer- und Rollenverwaltung
![]() |
Spring Security erfordert die Erstellung einer Klasse, die die folgende [UsersDetail]-Schnittstelle implementiert:
![]() |
Diese Schnittstelle wird hier von der Klasse [AppUserDetails] implementiert:
package rdvmedecins.security;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
public class AppUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
// properties
private User user;
private UserRepository userRepository;
// manufacturers
public AppUserDetails() {
}
public AppUserDetails(User user, UserRepository userRepository) {
this.user = user;
this.userRepository = userRepository;
}
// -------------------------interface
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : userRepository.getRoles(user.getId())) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getLogin();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
// getters and setters
...
}
- Zeile 10: Die Klasse [AppUserDetails] implementiert die Schnittstelle [UserDetails];
- Zeilen 15–16: Die Klasse kapselt einen Benutzer (Zeile 15) und das Repository, das Details zu diesem Benutzer bereitstellt (Zeile 16);
- Zeilen 22–25: Der Konstruktor, der die Klasse mit einem Benutzer und dessen Repository instanziiert;
- Zeilen 28–35: Implementierung der Methode [getAuthorities] der Schnittstelle [UserDetails]. Sie muss eine Sammlung von Elementen des Typs [GrantedAuthority] oder eines abgeleiteten Typs erstellen. Hier verwenden wir den abgeleiteten Typ [SimpleGrantedAuthority] (Zeile 32), der den Namen einer der Rollen des Benutzers aus Zeile 15 kapselt;
- Zeilen 31–33: Wir durchlaufen die Liste der Rollen des Benutzers aus Zeile 15, um eine Liste von Elementen vom Typ [SimpleGrantedAuthority] zu erstellen;
- Zeilen 38–40: Implementieren Sie die Methode [getPassword] der Schnittstelle [UserDetails]. Wir geben das Passwort des Benutzers aus Zeile 15 zurück;
- Zeilen 38–40: Implementierung der Methode [getUserName] der Schnittstelle [UserDetails]. Rückgabe des Benutzernamens des Benutzers aus Zeile 15;
- Zeilen 47–50: Das Benutzerkonto läuft nie ab;
- Zeilen 52–55: Das Benutzerkonto wird niemals gesperrt;
- Zeilen 57–60: Die Anmeldedaten des Benutzers verfallen nie;
- Zeilen 62–65: Das Benutzerkonto ist immer aktiv;
Spring Security erfordert außerdem das Vorhandensein einer Klasse, die die Schnittstelle [AppUserDetailsService] implementiert:
![]() |
Diese Schnittstelle wird von der folgenden [AppUserDetailsService]-Klasse implementiert:
package rdvmedecins.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AppUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// search for user via login
User user = userRepository.findUserByLogin(login);
// found?
if (user == null) {
throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
}
// render user details
return new AppUserDetails(user, userRepository);
}
}
- Zeile 9: Die Klasse wird eine Spring-Komponente sein, sodass sie in ihrem Kontext verfügbar ist;
- Zeilen 12–13: Die [UserRepository]-Komponente wird hier injiziert;
- Zeilen 16–25: Implementierung der Methode [loadUserByUsername] der Schnittstelle [UserDetailsService] (Zeile 10). Der Parameter ist der Benutzername des Benutzers;
- Zeile 18: Der Benutzer wird anhand seines Benutzernamens gesucht;
- Zeilen 20–22: Wird der Benutzer nicht gefunden, wird eine Ausnahme ausgelöst;
- Zeile 24: Ein [AppUserDetails]-Objekt wird erstellt und zurückgegeben. Es ist tatsächlich vom Typ [UserDetails] (Zeile 16);
8.4.13.6. [DAO]-Schicht-Tests
![]() |
Zunächst erstellen wir eine ausführbare Klasse [CreateUser], die einen Benutzer mit einer Rolle anlegen kann:
package rdvmedecins.security;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;
import rdvmedecins.config.DomainAndPersistenceConfig;
import rdvmedecins.security.Role;
import rdvmedecins.security.RoleRepository;
import rdvmedecins.security.User;
import rdvmedecins.security.UserRepository;
import rdvmedecins.security.UserRole;
import rdvmedecins.security.UserRoleRepository;
public class CreateUser {
public static void main(String[] args) {
// syntax: login password roleName
// three parameters are required
if (args.length != 3) {
System.out.println("Syntaxe : [pg] user password role");
System.exit(0);
}
// parameters are retrieved
String login = args[0];
String password = args[1];
String roleName = String.format("ROLE_%s", args[2].toUpperCase());
// spring context
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DomainAndPersistenceConfig.class);
UserRepository userRepository = context.getBean(UserRepository.class);
RoleRepository roleRepository = context.getBean(RoleRepository.class);
UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
// does the role already exist?
Role role = roleRepository.findRoleByName(roleName);
// if it doesn't exist, we create it
if (role == null) {
role = roleRepository.save(new Role(roleName));
}
// does the user already exist?
User user = userRepository.findUserByLogin(login);
// if it doesn't exist, we create it
if (user == null) {
// hash the password with bcrypt
String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
// save user
user = userRepository.save(new User(login, login, crypt));
// we create the relationship with the role
userRoleRepository.save(new UserRole(user, role));
} else {
// the user already exists - does he/she have the required role?
boolean trouvé = false;
for (Role r : userRepository.getRoles(user.getId())) {
if (r.getName().equals(roleName)) {
trouvé = true;
break;
}
}
// if not found, we create the relationship with the role
if (!trouvé) {
userRoleRepository.save(new UserRole(user, role));
}
}
// closing Spring context
context.close();
}
}
- Zeile 17: Die Klasse erwartet drei Argumente, die einen Benutzer definieren: dessen Benutzername, Passwort und Rolle;
- Zeilen 25–27: Die drei Parameter werden abgerufen;
- Zeile 29: Der Spring-Kontext wird aus der Konfigurationsklasse [DomainAndPersistenceConfig] erstellt. Diese Klasse war bereits im ursprünglichen Projekt vorhanden. Sie muss wie folgt aktualisiert werden:
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories", "rdvmedecins.security" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities", "rdvmedecins.security" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
....
}
- Zeile 1: Sie müssen angeben, dass sich nun [Repository]-Komponenten im Paket [rdvmedecins.security] befinden;
- Zeile 4: Sie müssen angeben, dass sich nun JPA-Entitäten im Paket [rdvmedecins.security] befinden;
Kehren wir zum Code für die Erstellung eines Benutzers zurück:
- Zeilen 30–32: Wir rufen die Referenzen der drei [Repository]-Objekte ab, die für die Erstellung des Benutzers nützlich sein könnten;
- Zeile 34: Wir prüfen, ob die Rolle bereits existiert;
- Zeilen 36–38: Falls nicht, legen wir sie in der Datenbank an. Sie erhält einen Namen der Form [ROLE_XX];
- Zeile 40: Wir prüfen, ob der Benutzername bereits existiert;
- Zeilen 42–49: Wenn der Benutzername nicht existiert, legen wir ihn in der Datenbank an;
- Zeile 44: Wir verschlüsseln das Passwort. Hier verwenden wir die [BCrypt]-Klasse aus Spring Security (Zeile 4). Wir benötigen daher die Archive für dieses Framework. Die Datei [pom.xml] enthält eine neue Abhängigkeit:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- Zeile 46: Der Benutzer wird in der Datenbank gespeichert;
- Zeile 48: ebenso wie die Beziehung, die ihn mit seiner Rolle verknüpft;
- Zeilen 51–57: Wenn der Benutzer bereits existiert, prüfen wir, ob die Rolle, die wir ihm zuweisen möchten, bereits zu seinen Rollen gehört;
- Zeilen 59–61: Wenn die gewünschte Rolle nicht gefunden wird, wird eine Zeile in der Tabelle [USERS_ROLES] erstellt, um den Benutzer mit seiner Rolle zu verknüpfen;
- Wir haben keine Absicherung gegen mögliche Ausnahmen vorgenommen. Dies ist eine Hilfsklasse zum schnellen Anlegen eines Benutzers mit einer Rolle.
Wenn die Klasse mit den Argumenten [x x guest] ausgeführt wird, werden in der Datenbank folgende Ergebnisse erzielt:
Tabelle [USERS]
![]() |
Tabelle [ROLES]
![]() |
Tabelle [USERS_ROLES]
![]() |
Betrachten wir nun die zweite Klasse [UsersTest], bei der es sich um einen JUnit-Test handelt:
![]() |
package rdvmedecins.security;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import rdvmedecins.config.DomainAndPersistenceConfig;
import com.google.common.collect.Lists;
@SpringApplicationConfiguration(classes = DomainAndPersistenceConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {
@Autowired
private UserRepository userRepository;
@Autowired
private AppUserDetailsService appUserDetailsService;
@Test
public void findAllUsersWithTheirRoles() {
Iterable<User> users = userRepository.findAll();
for (User user : users) {
System.out.println(user);
display("Roles :", userRepository.getRoles(user.getId()));
}
}
@Test
public void findUserByLogin() {
// user [admin] is retrieved
User user = userRepository.findUserByLogin("admin");
// we check that his password is [admin]
Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
// check admin / admin role
List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
Assert.assertEquals(1L, roles.size());
Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
}
@Test
public void loadUserByUsername() {
// user [admin] is retrieved
AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
// we check that his password is [admin]
Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
// check admin / admin role
@SuppressWarnings("unchecked")
List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
Assert.assertEquals(1L, authorities.size());
Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
}
// utility method - displays items in a collection
private void display(String message, Iterable<?> elements) {
System.out.println(message);
for (Object element : elements) {
System.out.println(element);
}
}
}
- Zeilen 27–34: Sichtprüfung. Wir zeigen alle Benutzer zusammen mit ihren Rollen an;
- Zeilen 36–46: Wir überprüfen mithilfe des [UserRepository], ob der Benutzer [admin] das Passwort [admin] und die Rolle [ROLE_ADMIN] hat;
- Zeile 41: [admin] ist das Klartext-Passwort. In der Datenbank ist es mit dem BCrypt-Algorithmus verschlüsselt. Die Methode [BCrypt.checkpw] überprüft, ob das verschlüsselte Klartext-Passwort mit dem in der Datenbank übereinstimmt;
- Zeilen 48–59: Wir überprüfen mithilfe von [appUserDetailsService], ob der Benutzer [admin] das Passwort [admin] und die Rolle [ROLE_ADMIN] hat;
Die Tests werden erfolgreich mit den folgenden Protokollen ausgeführt:
8.4.13.7. Zwischenfazit
Die für Spring Security erforderlichen Klassen wurden mit minimalen Änderungen am ursprünglichen Projekt hinzugefügt. Zusammenfassend lässt sich sagen:
- Hinzufügen einer Abhängigkeit zu Spring Security in der [pom.xml]-Datei;
- Erstellung von drei zusätzlichen Tabellen in der Datenbank;
- Erstellung von JPA-Entitäten und Spring-Komponenten im Paket [rdvmedecins.security];
Dieses sehr günstige Szenario ergibt sich aus der Tatsache, dass die drei zur Datenbank hinzugefügten Tabellen unabhängig von den bestehenden Tabellen sind. Wir hätten sie sogar in einer separaten Datenbank unterbringen können. Dies war möglich, weil wir entschieden hatten, dass ein Benutzer unabhängig von Ärzten und Kunden existiert. Wären letztere potenzielle Benutzer gewesen, hätten wir Verknüpfungen zwischen der Tabelle [USERS] und den Tabellen [MEDECINS] und [CLIENTS] erstellen müssen. Dies hätte erhebliche Auswirkungen auf das bestehende Projekt gehabt.
8.4.13.8. Das STS-Projekt für die [Web]-Schicht
![]() |
Das Projekt [rdvmedecins-webjson] entwickelt sich wie folgt[1]:
![]() |
Die wichtigsten Änderungen müssen in der Datei [rdvmedecins.web.config] vorgenommen werden, wo Spring Security konfiguriert werden muss. Es gibt weitere, kleinere Änderungen in den Klassen [AppConfig] und [ApplicationModel]. Wir sind bereits auf eine Spring-Security-Konfigurationsklasse gestoßen:
package hello;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
}
}
Wir gehen genauso vor:
- Zeile 11: Definieren Sie eine Klasse, die die Klasse [WebSecurityConfigurerAdapter] erweitert;
- Zeile 13: Definieren Sie eine Methode [configure(HttpSecurity http)], die Zugriffsrechte für die verschiedenen URLs des Webdienstes festlegt;
- Zeile 19: Definieren Sie eine Methode [configure(AuthenticationManagerBuilder auth)], die Benutzer und deren Rollen definiert;
Die Spring Security-Konfiguration wird von der Klasse [SecurityConfig] verwaltet:
package rdvmedecins.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import rdvmedecins.security.AppUserDetailsService;
import rdvmedecins.web.models.ApplicationModel;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
@Autowired
private ApplicationModel application;
@Override
protected void configure(AuthenticationManagerBuilder registry) throws Exception {
// authentication is performed by bean [appUserDetailsService]
// the password is encrypted using the BCrypt hash algorithm
registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// CSRF
http.csrf().disable();
// secure application?
if (application.isSecured()) {
// the password is transmitted by the header Authorization: Basic xxxx
http.httpBasic();
// the HTTP OPTIONS method must be authorized for all
http.authorizeRequests() //
.antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
// only the ADMIN role can use the application
http.authorizeRequests() //
.antMatchers("/", "/**") // all URL
.hasRole("ADMIN");
// no session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
}
- Zeile 15: Die Klasse [SecurityConfig] ist eine Spring-Konfigurationsklasse;
- Zeile 16: Einrichtung der Projektsicherheit;
- Zeilen 19–20: Die Klasse [AppUserDetails], die Zugriff auf Anwendungsbenutzer bietet, wird injiziert;
- Zeilen 21–22: Die Klasse [ApplicationModel], die als Cache für die Webanwendung dient, wird injiziert. Wir entscheiden uns, sie auch hier zu verwenden, um die Webanwendung an einer einzigen Stelle zu konfigurieren. Sie definiert in Zeile 36 den booleschen Wert [isSecured]. Dieser Wert sichert (true) oder sichert nicht (false) die Webanwendung;
- Zeilen 25–29: Die Methode [configure(HttpSecurity http)] definiert Benutzer und deren Rollen. Sie nimmt einen Typ [AuthenticationManagerBuilder] als Parameter entgegen. Dieser Parameter wird um zwei Informationen ergänzt (Zeile 28):
- einen Verweis auf den [appUserDetailsService] aus Zeile 20, der Zugriff auf registrierte Benutzer gewährt. Beachten Sie hierbei, dass nicht ausdrücklich angegeben wird, dass diese in einer Datenbank gespeichert sind. Sie könnten daher in einem Cache liegen, von einem Webdienst bereitgestellt werden usw.
- den für das Passwort verwendeten Verschlüsselungstyp. Erinnern Sie sich daran, dass wir den BCrypt-Algorithmus verwendet haben;
- Zeilen 38–47: Die Methode [configure(HttpSecurity http)] definiert Zugriffsrechte auf die URLs des Webdienstes;
- Zeile 34: Wir haben im Einführungsprojekt gesehen, dass Spring Security standardmäßig ein CSRF-Token (Cross-Site Request Forgery) verwaltet, das der Benutzer, der sich authentifizieren möchte, an den Server zurücksenden muss. Hier ist dieser Mechanismus deaktiviert. In Kombination mit dem booleschen Wert (isSecured=false) ermöglicht dies die Nutzung der Webanwendung ohne Sicherheitsmaßnahmen;
- Zeile 38: Wir aktivieren die Authentifizierung über HTTP-Header. Der Client muss den folgenden HTTP-Header senden:
wobei code die Base64-Kodierung der Zeichenfolge login:password ist. Beispielsweise lautet die Base64-Kodierung der Zeichenfolge admin:admin YWRtaW46YWRtaW4=. Daher sendet ein Benutzer mit dem Login [admin] und dem Passwort [admin] zur Authentifizierung den folgenden HTTP-Header:
- Zeilen 40–42: geben an, dass alle URLs des Webdienstes für Benutzer mit der Rolle [ROLE_ADMIN] zugänglich sind. Das bedeutet, dass ein Benutzer ohne diese Rolle nicht auf den Webdienst zugreifen kann;
- Zeile 47: Das Passwort des Benutzers kann in einer Sitzung gespeichert werden oder auch nicht. Wenn es gespeichert wird, muss sich der Benutzer nur beim ersten Mal authentifizieren. Bei nachfolgenden Anfragen werden seine Anmeldedaten nicht abgefragt. Hier haben wir uns für einen sitzungslosen Modus entschieden. Jede Anfrage muss mit Sicherheitsanmeldedaten versehen sein;
Die Klasse [AppConfig], die die gesamte Anwendung konfiguriert, wird wie folgt aktualisiert:
![]() |
package rdvmedecins.web.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@Configuration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {
}
- Die Änderung erfolgt in Zeile 11: Die Konfigurationsklasse [SecurityConfig] wird hinzugefügt;
Schließlich wird die Klasse [ApplicationModel] um einen booleschen Wert erweitert:
@Component
public class ApplicationModel implements IMetier {
...
// configuration data
private boolean secured = false;
public boolean isSecured() {
return secured;
}
- Zeile 6: Setze den booleschen Wert [secured] auf [true / false], je nachdem, ob du die Sicherheit aktivieren möchtest.
8.4.13.9. Testen des Webdienstes
Wir testen den Webdienst mit dem Chrome-Client [Advanced Rest Client]. Dazu müssen wir den HTTP-Authentifizierungsheader angeben:
wobei [code] die Base64-kodierte Zeichenfolge [login:password] ist. Um diesen Code zu generieren, können Sie das folgende Programm verwenden:
![]() |
package rdvmedecins.helpers;
import org.springframework.security.crypto.codec.Base64;
public class Base64Encoder {
public static void main(String[] args) {
// we expect two arguments: login password
if (args.length != 2) {
System.out.println("Syntaxe : login password");
System.exit(0);
}
// we retrieve the two arguments
String chaîne = String.format("%s:%s", args[0], args[1]);
// encode the string
byte[] data = Base64.encode(chaîne.getBytes());
// displays its Base64 encoding
System.out.println(new String(data));
}
}
Wenn wir dieses Programm mit den beiden Argumenten [admin admin] ausführen:
![]() |
erhalten wir folgendes Ergebnis:
Da wir nun wissen, wie man den HTTP-Authentifizierungsheader generiert, starten wir den nun sicheren Webdienst:
@Component
public class ApplicationModel implements IMetier {
...
private boolean secured = true;
Anschließend fordern wir mit dem Chrome-Client [Advanced Rest Client] die Liste aller Ärzte an:
![]() |
- In [1] fordern wir die URL der Ärzte an;
- in [2] verwenden wir eine GET-Methode;
- in [3] geben wir den HTTP-Authentifizierungsheader an. Der Code [YWRtaW46YWRtaW4=] ist die Base64-Kodierung der Zeichenfolge [admin:admin];
- in [4] senden wir die HTTP-Anfrage;
Die Antwort des Servers lautet wie folgt:
![]() |
- in [1] der HTTP-Authentifizierungsheader;
- in [2] gibt der Server eine JSON-Antwort zurück;
- in [3] eine Liste von HTTP-Headern, die sich auf die Sicherheit der Webanwendung beziehen;
Wir erhalten erfolgreich die Liste der Ärzte:
![]() |
Versuchen wir nun eine HTTP-Anfrage mit einem falschen Authentifizierungsheader. Die Antwort lautet dann wie folgt:
![]() |
- in [1] und [3]: der HTTP-Authentifizierungsheader;
- in [2]: die Antwort des Webdienstes;
Versuchen wir es nun mit dem Benutzer „user / user“. Er existiert, hat aber keinen Zugriff auf den Webdienst. Wenn wir das Base64-Kodierungsprogramm mit den beiden Argumenten [user user] ausführen:
![]() |
erhalten wir folgendes Ergebnis:
![]() |
- in [1] und [3]: der HTTP-Authentifizierungsheader;
- in [2]: die Antwort des Webdienstes. Sie unterscheidet sich von der vorherigen, die [401 Unauthorized] lautete. Diesmal hat sich der Benutzer erfolgreich authentifiziert, verfügt jedoch nicht über ausreichende Berechtigungen, um auf die URL zuzugreifen;
Ein sicherer Webservice ist nun betriebsbereit. Wir werden ihn erweitern, um domänenübergreifende Anfragen zu ermöglichen. Diese Anforderung wurde im Dokument [AngularJS / Spring 4 Tutorial] erwähnt, und obwohl sie hier nicht zutrifft, werden wir sie dennoch behandeln.
8.4.14. Implementierung domänenübergreifender Anfragen
Betrachten wir das Thema domänenübergreifende Anfragen. Im Dokument [AngularJS / Spring 4 Tutorial] entwickeln wir eine Client/Server-Anwendung, bei der der Client eine AngularJS-Anwendung ist:
![]() |
- Die HTML-/CSS-/JS-Seiten der Angular-Anwendung stammen von Server [1];
- in [2] sendet der [dao]-Dienst eine Anfrage an einen anderen Server, Server [2]. Dies wird jedoch vom Browser, auf dem die Angular-Anwendung läuft, unterbunden, da es sich um eine Sicherheitslücke handelt. Die Anwendung kann nur den Server abfragen, von dem sie stammt, d. h. Server [1];
Tatsächlich ist es unzutreffend zu sagen, dass der Browser die Angular-Anwendung daran hindert, Server [2] abzufragen. Er fragt diesen vielmehr an, ob er es einem Client erlaubt, der nicht von ihm stammt, ihn abzufragen. Diese Technik der gemeinsamen Nutzung wird als CORS (Cross-Origin Resource Sharing) bezeichnet. Server [2] erteilt die Erlaubnis, indem er bestimmte HTTP-Header sendet.
Um die Probleme zu veranschaulichen, die auftreten können, erstellen wir eine Client-Server-Anwendung, bei der:
- der Server unser Web-/JSON-Server ist;
- der Client eine einfache HTML-Seite ist, die mit JavaScript-Code ausgestattet ist, der Anfragen an den Web-/JSON-Server stellt;
8.4.14.1. Das Client-Projekt
![]() |
Das Projekt ist ein Maven-Projekt mit der folgenden [pom.xml]-Datei:
<?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>istia.st</groupId>
<artifactId>rdvmedecins-webjson-client-cors</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-client-cors</name>
<description>Client for webjson server</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.6.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.rdvmedecins.Client</start-class>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
- Zeilen 14–19: Dies ist ein Spring-Boot-Projekt;
- Zeilen 29–32: Wir verwenden die Abhängigkeit [spring-boot-starter-web], die einen Tomcat-Server und Spring MVC enthält;
Die HTML-Seite sieht wie folgt aus:
![]() |
Es wird durch den folgenden Code generiert:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Spring MVC</title>
<script type="text/javascript" src="/js/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="/js/client.js"></script>
</head>
<body>
<h2>Client du service web / jSON</h2>
<form id="formulaire">
<!-- method HTTP -->
Méthode HTTP :
<!-- -->
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<!-- -->
<input type="radio" id="post" name="method" value="post" />POST
<!-- URL -->
<br /> <br />URL cible : <input type="text" id="url" size="30"><br />
<!-- posted value -->
<br /> Chaîne jSON à poster : <input type="text" id="posted" size="50" />
<!-- validation button -->
<br /> <br /> <input type="submit" value="Valider" onclick="javascript:requestServer(); return false;"></input>
</form>
<hr />
<h2>Réponse du serveur</h2>
<div id="response"></div>
</body>
</html>
- Zeile 6: Wir importieren die jQuery-Bibliothek;
- Zeile 7: Wir importieren Code, den wir schreiben werden;
Der Code in [client.js] lautet wie folgt:
// global data
var url;
var posted;
var response;
var method;
function requestServer() {
// retrieve information from the form
var urlValue = url.val();
var postedValue = posted.val();
method = document.forms[0].elements['method'].value;
// make a manual Ajax call
if (method === "get") {
doGet(urlValue);
} else {
doPost(urlValue, postedValue);
}
}
function doGet(url) {
// make a manual Ajax call
$.ajax({
headers : {
'Authorization' : 'Basic YWRtaW46YWRtaW4='
},
url : 'http://localhost:8080' + url,
type : 'GET',
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// text result
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// system error
response.text(jqXHR.responseText);
}
})
}
function doPost(url, posted) {
// make a manual Ajax call
$.ajax({
headers : {
'Authorization' : 'Basic YWRtaW46YWRtaW4='
},
url : 'http://localhost:8080' + url,
type : 'POST',
contentType : 'application/json',
data : posted,
dataType : 'tex/plain',
beforeSend : function() {
},
success : function(data) {
// text result
response.text(data);
},
complete : function() {
},
error : function(jqXHR) {
// system error
response.text(jqXHR.responseText);
}
})
}
// document loading
$(document).ready(function() {
// retrieve page component references
url = $("#url");
posted = $("#posted");
response = $("#response");
});
Wir überlassen es dem Leser, diesen Code zu verstehen. Alles wurde bereits irgendwann einmal behandelt. Einige Zeilen verdienen jedoch eine Erklärung:
- Zeile 11:
- [document] bezieht sich auf das vom Browser geladene Dokument, das als DOM (Document Object Model) bezeichnet wird,
- [document.forms[0]] bezieht sich auf das erste Formular im Dokument; ein Dokument kann mehrere Formulare enthalten. Hier gibt es nur eines,
- [document.forms[0].elements['method']] bezieht sich auf das Formularelement mit dem Attribut [name='method']. Es gibt zwei davon:
<input type="radio" id="get" name="method" value="get" checked="checked" />GET
<input type="radio" id="post" name="method" value="post" />POST
- Zeile 11:
- [document.forms[0].elements['method'].value] ist der Wert, der für die Komponente mit dem Attribut [name='method'] gesendet wird. Wir wissen, dass der gesendete Wert dem Wert des Attributs [value] des ausgewählten Optionsfelds entspricht. Hier handelt es sich also um eine der Zeichenfolgen ['get', 'post'];
- Zeilen 23–25: Wir kommunizieren mit einem Server, der einen HTTP-Header [Authorization: Basic code] erfordert. Wir erstellen diesen Header für den Benutzer [admin / admin], der als Einziger berechtigt ist, den Server abzufragen;
- Zeile 26: Der Benutzer gibt URLs der Form [/getAllDoctors, /deleteAppointment, ...] ein. Diese URLs müssen daher vervollständigt werden;
- Zeile 28: Der Server gibt JSON zurück, ein Textformat. Wir geben den Typ [text/plain] als Antworttyp an, damit die Antwort genau so angezeigt wird, wie sie empfangen wurde;
- Zeile 33: Anzeige der Textantwort des Servers;
- Zeile 39: Zeigt etwaige Fehlermeldungen im Textformat an;
- Zeile 52: um anzugeben, dass der Client JSON sendet;
In der Client-Server-Anwendung, die wir entwickeln:
- ist der Client eine Webanwendung, die unter der URL [http://localhost:8081] erreichbar ist. Dies ist die Anwendung, die wir derzeit entwickeln;
- Der Server ist eine Webanwendung, die unter der URL [http://localhost:8080] erreichbar ist. Dies ist unser Web-/JSON-Server;
Da der Client nicht auf demselben Port wie der Server läuft, stellt sich das Problem der domänenübergreifenden Anfragen. [http://localhost:8080] und [http://localhost:8081] sind zwei verschiedene Domänen.
Die Spring Boot-Anwendung ist eine Konsolenanwendung, die von der folgenden ausführbaren Klasse [Client] gestartet wird:
package istia.st.rdvmedecins;
import org.springframework.boot.SpringApplication;
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.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
@EnableWebMvc
public class Client extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(Client.class, args);
}
// static pages
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations(new String[] { "classpath:/static/" });
}
// configuration dispatcherServlet
@Bean
public DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
@Bean
public ServletRegistrationBean servletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new ServletRegistrationBean(dispatcherServlet, "/*");
}
// embedded Tomcat server
@Bean
public EmbeddedServletContainerFactory embeddedServletContainerFactory() {
return new TomcatEmbeddedServletContainerFactory("", 8081);
}
}
- Zeile 14: Die Klasse [Client] ist eine Spring-Konfigurationsklasse;
- Zeile 15: Es wird eine Spring-MVC-Anwendung konfiguriert. Diese Annotation löst eine Reihe automatischer Konfigurationen aus;
- Zeile 16: Um bestimmte Standardwerte des Spring MVC-Frameworks zu überschreiben, müssen Sie die Klasse [WebMvcConfigurerAdapter] erweitern;
- Zeilen 23–26: Mit der Methode [addResourceHandlers] können Sie die Verzeichnisse angeben, in denen sich die statischen Ressourcen der Anwendung (HTML, CSS, JS usw.) befinden. Hier geben wir das Verzeichnis [static] an, das sich im Klassenpfad des Projekts befindet:
![]() |
- Zeilen 29–37: Konfiguration der [dispatcherServlet]-Bean, die das Spring MVC-Servlet bezeichnet;
- Zeilen 40–43: Der eingebettete Tomcat-Server läuft auf Port 8081;
8.4.14.2. Die URL [/getAllMedecins]
Wir starten:
- den Web-/JSON-Server auf Port 8080;
- den Client für diesen Server auf Port 8081;
dann rufen wir die URL [http://localhost:8081/client.html] [1] auf:
![]() |
- in [2] führen wir eine GET-Anfrage an die URL [http://localhost:8080/getAllMedecins] durch;
Wir erhalten keine Antwort vom Server. Wenn wir uns die Entwicklerkonsole (Strg-Umschalt-I) ansehen, wird ein Fehler angezeigt:
![]() |
- In [1] befinden wir uns auf der Registerkarte [Netzwerk];
- In [2] sehen wir, dass die HTTP-Anfrage nicht [GET], sondern [OPTIONS] lautet. Bei einer domänenübergreifenden Anfrage überprüft der Browser durch Senden einer HTTP-[OPTIONS]-Anfrage beim Server, ob bestimmte Bedingungen erfüllt sind. In diesem Fall handelt es sich um die durch die Kreise [5-6] gekennzeichneten Anfragen;
- In [5] fragt der Browser, ob die Ziel-URL mit einem GET erreicht werden kann. Der Anforderungsheader [Access-Control-Request-Method] fordert eine Antwort mit einem HTTP-Header [Access-Control-Allow-Methods] an, der angibt, dass die angeforderte Methode akzeptiert wird;
- In [5] sendet der Browser den HTTP-Header [Origin: http://localhost:8081]. Dieser Header fordert eine Antwort in einem HTTP-Header [Access-Control-Allow-Origin] an, der angibt, dass die angegebene Herkunft akzeptiert wird;
- In [6] fragt der Browser ab, ob die HTTP-Header [Accept] und [Authorization] akzeptiert werden. Der Request-Header [Access-Control-Request-Headers] erwartet eine Antwort mit einem HTTP-Header [Access-Control-Allow-Headers], der angibt, dass die angeforderten Header akzeptiert werden;
- In [3] tritt ein Fehler auf. Ein Klick auf das Symbol führt zu Fehler [4];
- In [4] weist die Meldung darauf hin, dass der Server den HTTP-Header [Access-Control-Allow-Origin] nicht gesendet hat, der angibt, ob die Herkunft der Anfrage akzeptiert wird;
- In [7] sehen wir, dass der Server diesen Header tatsächlich nicht gesendet hat. Infolgedessen hat der Browser die ursprünglich angeforderte HTTP-GET-Anfrage abgelehnt;
Wir müssen den Webserver/JSON anpassen. Wir nehmen eine erste Änderung in [ApplicationModel] vor, einem der Konfigurationselemente des Webdienstes:
![]() |
@Component
public class ApplicationModel implements IMetier {
...
// configuration data
private boolean corsAllowed = true;
private boolean secured = true;
...
public boolean isCorsAllowed() {
return corsAllowed;
}
- Zeile 6: Wir definieren eine boolesche Variable, die angibt, ob Clients außerhalb der Server-Domäne akzeptiert werden oder nicht;
- Zeilen 10–12: die Methode zum Abrufen dieser Informationen;
Anschließend erstellen wir einen neuen Spring MVC-Controller:
![]() |
Die Klasse [RdvMedecinsCorsController] sieht wie folgt aus:
package rdvmedecins.web.controllers;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import rdvmedecins.web.models.ApplicationModel;
@Controller
public class RdvMedecinsCorsController {
@Autowired
private ApplicationModel application;
// sending options to the customer
public void sendOptions(String origin, HttpServletResponse response) {
// Cors allowed ?
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// set header CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// certain headers are allowed
response.addHeader("Access-Control-Allow-Headers", "accept, authorization");
// we authorize GET
response.addHeader("Access-Control-Allow-Methods", "GET");
}
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
public void getAllMedecins(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
}
- Zeilen 12–13: Die Klasse [RdvMedecinsCorsController] ist ein Spring-Controller;
- Zeilen 33–36: Definieren Sie eine Aktion, die die URL [/getAllMedecins] verarbeitet, wenn sie mit der HTTP-Methode [OPTIONS] angefordert wird;
- Zeile 34: Die Methode [getAllMedecins] akzeptiert die folgenden Parameter:
- das Objekt [@RequestHeader(value = "Origin", required = false)], das den HTTP-Header [Origin] aus der Anfrage abruft. Dieser Header wurde vom Absender der Anfrage gesendet:
Wir legen fest, dass der HTTP-Header [Origin] optional ist [required = false]. Fehlt der Header in diesem Fall, hat der Parameter [String origin] den Wert null. Bei [required = true], dem Standardwert, wird eine Ausnahme ausgelöst, wenn der Header fehlt. Dieses Szenario wollten wir vermeiden;
- Zeile 34:
- das Objekt [HttpServletResponse response], das an den Client gesendet wird, der die Anfrage gestellt hat;
Diese beiden Parameter werden von Spring injiziert;
- Zeile 35: Wir delegieren die Bearbeitung der Anfrage an die Methode in den Zeilen 19–30;
- Zeilen 15–16: Das [ApplicationModel]-Objekt wird injiziert;
- Zeilen 21–23: Wenn die Anwendung so konfiguriert ist, dass sie domänenübergreifende Anfragen akzeptiert, und wenn der Absender den HTTP-Header [Origin] gesendet hat, und wenn dieser Ursprung mit [http://localhost] beginnt, dann akzeptieren wir die domänenübergreifende Anfrage; andernfalls lehnen wir sie ab;
- Zeile 25: Befindet sich der Client in der Domäne [http://localhost:port], wird der HTTP-Header gesendet:
Access-Control-Allow-Origin: http://localhost:port
was bedeutet, dass der Server die Herkunft des Clients akzeptiert;
- Zeile 25: Wir haben in der HTTP-Anfrage [OPTIONS] zwei spezifische HTTP-Header angegeben:
Als Antwort auf den HTTP-Header [Access-Control-Request-X] sendet der Server einen HTTP-Header [Access-Control-Allow-X] zurück, der angibt, was erlaubt ist. Die Zeilen 23–26 wiederholen lediglich die Anfrage des Clients, um anzuzeigen, dass diese akzeptiert wurde;
Wir sind nun bereit für weitere Tests. Wir starten die neue Version des Webdienstes und stellen fest, dass das Problem unverändert besteht. Es hat sich nichts geändert. Wenn wir in Zeile 35 oben eine Konsolenausgabe hinzufügen, wird diese nie angezeigt, was darauf hindeutet, dass die Methode [getAllMedecins] in Zeile 34 nie aufgerufen wird.
Nach einigen Recherchen stellen wir fest, dass Spring MVC [OPTIONS]-HTTP-Anfragen mithilfe seiner Standardbehandlung selbst verarbeitet. Daher antwortet immer Spring und niemals die Methode [getAllMedecins] in Zeile 34. Dieses Standardverhalten von Spring MVC kann geändert werden. Wir ändern die vorhandene [WebConfig]-Klasse:
![]() |
package rdvmedecins.web.config;
...
import org.springframework.web.servlet.DispatcherServlet;
@Configuration
public class WebConfig {
// dispatcherservlet configuration for CORS headers
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
// mapping jSON
...
- Zeilen 10–11: Die Bean [dispatcherServlet] wird verwendet, um das Servlet zu definieren, das Client-Anfragen verarbeitet. Hier handelt es sich um den Typ [DispatcherServlet], das Servlet aus dem Spring MVC-Framework;
- Zeile 12: Wir erstellen eine Instanz vom Typ [DispatcherServlet];
- Zeile 13: Wir weisen das Servlet an, [OPTIONS]-HTTP-Anfragen an die Anwendung weiterzuleiten;
- Zeile 14: Wir stellen das so konfigurierte Servlet bereit;
Wir führen die Tests mit dieser neuen Konfiguration erneut durch. Wir erhalten das folgende Ergebnis:
![]() |
- In [1] sehen wir, dass zwei HTTP-Anfragen an die URL [http://localhost:8080/getAllMedecins] gesendet wurden;
- in [2] die [OPTIONS]-Anfrage;
- in [3] die drei HTTP-Header, die wir gerade in der Antwort des Servers konfiguriert haben;
Betrachten wir nun die zweite Anfrage:
![]() |
- in [1] die untersuchte Anfrage;
- in [2] handelt es sich um die GET-Anfrage. Dank der ersten [OPTIONS]-Anfrage hat der Browser die angeforderten Informationen erhalten. Er führt nun die ursprünglich angeforderte [GET]-Anfrage aus;
- in [3] die Antwort des Servers;
- in [4] sendet der Server JSON;
- in [5] ist ein Fehler aufgetreten;
- in [6] die Fehlermeldung;
Es ist schwieriger zu erklären, was hier passiert ist. Die Antwort des Servers [3] ist normal [HTTP/1.1 200 OK]. Wir sollten daher über das angeforderte Dokument verfügen. Es ist möglich, dass der Server das Dokument tatsächlich gesendet hat, der Browser jedoch dessen Nutzung verhindert, da er verlangt, dass die Antwort auch für die GET-Anfrage den HTTP-Header [Access-Control-Allow-Origin:http://localhost:8081] enthält.
Wir ändern den Controller [RdvMedecinsController] wie folgt:
@Autowired
private RdvMedecinsCorsController rdvMedecinsCorsController;
...
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getAllMedecins(HttpServletResponse httpServletResponse,
@RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
// the answer
Response<List<Medecin>> response;
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// application status
...
- Zeilen 1–2: Der Controller [RdvMedecinsCorsController] wird injiziert;
- Zeilen 7–8: Das HttpServletResponse-Objekt, das die an den Client zu sendende Antwort kapselt, und der HTTP-Header [Origin] werden in die Parameter der Methode [getAllMedecins] injiziert;
- Zeile 12: Die Methode [sendOptions] des Controllers [RdvMedecinsCorsController] wird aufgerufen – genau dieselbe Methode, die zur Bearbeitung der HTTP-Anfrage [OPTIONS] aufgerufen wurde. Sie sendet daher dieselben HTTP-Header wie bei dieser Anfrage;
Nach dieser Änderung sind die Ergebnisse wie folgt:
![]() |
Wir haben die Liste der Ärzte erfolgreich abgerufen.
8.4.14.3. Die anderen [GET]-URLs
Wir werden uns nun die anderen URLs ansehen, die über eine GET-Anfrage abgefragt werden. In den Controllern folgt der Code für die Aktionen, die diese verarbeiten, dem gleichen Muster wie die Aktionen, die zuvor die URL [/getAllMedecins] verarbeitet haben. Der Leser kann den Code in den mit diesem Dokument bereitgestellten Beispielen überprüfen. Hier ist ein Beispiel:
in [RdvMedecinsCorsController]
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
public void getRvMedecinJour(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
in [RdvMedecinsController]
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
@ResponseBody
public String getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour,
HttpServletResponse httpServletResponse, @RequestHeader(value = "Origin", required = false) String origin)
throws JsonProcessingException {
// the answer
Response<List<Rv>> response = null;
boolean erreur = false;
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// application status
...
Hier sind einige Screenshots der Ausführung:
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
8.4.14.4. Die [POST]-URLs
Betrachten wir das folgende Szenario:
![]() |
- Wir senden einen POST-Request [1] an die URL [2];
- in [3] den gesendeten Wert. Dies ist eine JSON-Zeichenkette;
- Insgesamt versuchen wir, den Termin mit der [id] 100 zu löschen;
Wir ändern an dieser Stelle keinen Code. Das Ergebnis lautet wie folgt:
![]() |
- In [1] wird, wie bei [GET]-Anfragen, eine [OPTIONS]-Anfrage vom Browser gestellt;
- in [2] fordert er die Zugriffsberechtigung für eine [POST]-Anfrage an. Zuvor war es [GET];
- In [3] wird die Autorisierung zum Senden der HTTP-Header [accept, authorization, content-type] angefordert. Zuvor gab es nur die ersten beiden Header;
Wir ändern die Methode [RdvMedecinsCorsController.sendOptions] wie folgt:
public void sendOptions(String origin, HttpServletResponse response) {
// Cors allowed ?
if (!application.isCorsAllowed() || origin==null || !origin.startsWith("http://localhost")) {
return;
}
// set header CORS
response.addHeader("Access-Control-Allow-Origin", origin);
// certain headers are allowed
response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
// we authorize GET
response.addHeader("Access-Control-Allow-Methods", "GET, POST");
}
- Zeile 9: Wir haben den HTTP-Header [Content-Type] hinzugefügt (Groß-/Kleinschreibung spielt keine Rolle);
- Zeile 11: Wir haben die HTTP-Methode [POST] hinzugefügt;
Das bedeutet, dass [POST]-Methoden genauso behandelt werden wie [GET]-Anfragen. Hier ist ein Beispiel für die URL [/deleteAppointment]:
in [RdvMedecinsController]
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, produces = "application/json; charset=UTF-8", consumes = "application/json; charset=UTF-8")
@ResponseBody
public String supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse httpServletResponse,
@RequestHeader(value = "Origin", required = false) String origin) throws JsonProcessingException {
// the answer
Response<Void> response = null;
boolean erreur = false;
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, httpServletResponse);
// application status
if (messages != null) {
...
in [RdvMedecinsCorsController]
@RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
public void supprimerRv(@RequestHeader(value = "Origin", required = false) String origin, HttpServletResponse response) {
sendOptions(origin, response);
}
Das Ergebnis lautet wie folgt:
![]() |
Für die URL [/addRv] ergibt sich folgendes Ergebnis:
![]() |
8.4.14.5. Fazit
Unsere Anwendung unterstützt nun domänenübergreifende Anfragen. Diese können über die Konfiguration in der Klasse [ApplicationModel] aktiviert oder deaktiviert werden:
// données de configuration
private boolean corsAllowed = false;
8.5. Webservice-Client / JSON
Kehren wir zur Gesamtarchitektur der Anwendung zurück, die wir erstellen möchten:
![]() |
Der obere Teil des Diagramms ist bereits fertiggestellt. Dies ist der Web-/JSON-Server. Wir werden uns nun dem unteren Teil zuwenden, beginnend mit der [DAO]-Schicht. Wir werden diese implementieren und anschließend mit einem Konsolen-Client testen. Die Testarchitektur sieht wie folgt aus:
![]() |
8.5.1. Das Konsolen-Client-Projekt
Das STS-Projekt für den Konsolen-Client sieht wie folgt aus:
![]() |
8.5.2. Maven-Konfiguration
Die [pom.xml]-Datei für den Konsolen-Client sieht wie folgt aus:
<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.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webjson-client-console</name>
<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.6.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</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>
</dependencies>
</project>
- Zeilen 15–20: das übergeordnete Spring Boot-Projekt;
- Zeilen 24–27: Der Webserver-/JSON-Konsolen-Client basiert auf einer Komponente namens [RestTemplate], die von der Abhängigkeit [spring-web] bereitgestellt wird;
- Zeilen 29–36: Das Serialisieren und Deserialisieren von JSON-Objekten erfordert eine JSON-Bibliothek. Wir verwenden eine Variante der von Spring Web genutzten Jackson-Bibliothek;
- Zeilen 38–41: Auf der untersten Ebene kommuniziert die [RestTemplate]-Komponente über TCP/IP-Sockets mit dem Server. Wir möchten hierfür das [timeout] festlegen, d. h. die maximale Wartezeit für eine Antwort vom Server. Die [RestTemplate]-Komponente lässt eine solche Einstellung nicht zu. Um dies zu erreichen, übergeben wir eine Low-Level-Komponente, die von der Abhängigkeit [org.apache.httpcomponents.httpclient] bereitgestellt wird, an den [RestTemplate]-Konstruktor. Diese Abhängigkeit ermöglicht es uns, das Kommunikations-[timeout] festzulegen;
8.5.3. Das Paket [rdvmedecins.client.entities]
![]() |
Das Paket [rdvmedecins.client.entities] enthält alle Entitäten, die der Webservice / JSON über seine verschiedenen URLs sendet. Wir werden nicht noch einmal im Detail darauf eingehen. Es genügt zu sagen, dass den JPA-Entitäten [Client, Slot, Doctor, Appointment, Person] sowohl alle JPA-Annotationen als auch ihre JSON-Annotationen entfernt wurden. Hier ist zum Beispiel die Klasse [Appointment]:
package rdvmedecins.client.entities;
import java.util.Date;
public class Rv extends AbstractEntity {
private static final long serialVersionUID = 1L;
// day of appointment
private Date jour;
// an appointment is linked to a customer
private Client client;
// an appointment is linked to a time slot
private Creneau creneau;
// foreign keys
private long idClient;
private long idCreneau;
// default builder
public Rv() {
}
// with parameters
public Rv(Date jour, Client client, Creneau creneau) {
this.jour = jour;
this.client = client;
this.creneau = creneau;
}
// toString
public String toString() {
return String.format("Rv[%d, %s, %d, %d]", id, jour, client.id, creneau.id);
}
// getters and setters
...
}
8.5.4. Das Paket [rdvmedecins.client.requests]
![]() |
Das Paket [rdvmedecins.client.requests] enthält die beiden Klassen, deren JSON-Werte an die URLs [/ajouterRv] und [supprimerRv] gesendet werden. Sie sind identisch mit ihren serverseitigen Entsprechungen.
8.5.5. Das Paket [rdvmedecins.client.responses]
![]() |
[Response] ist der Typ aller Webservice-/JSON-Antworten. Es handelt sich um einen generischen Typ:
package rdvmedecins.client.responses;
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: Der Typ [T] variiert je nach Webservice-URL / JSON;
8.5.6. Das Paket [rdvmedecins.client.dao]
![]() |
- [IDao] ist die Schnittstelle der [DAO]-Schicht, und [Dao] ist deren Implementierung. Wir werden später auf diese Implementierung zurückkommen;
8.5.7. Das Paket [rdvmedecins.client.config]
![]() |
Die Klasse [DaoConfig] konfiguriert die Anwendung. Ihr Code lautet wie folgt:
package rdvmedecins.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;
@Configuration
@ComponentScan({ "rdvmedecins.client.dao" })
public class DaoConfig {
@Bean
public RestTemplate restTemplate() {
// creation of the RestTemplate component
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
RestTemplate restTemplate = new RestTemplate(factory);
// result
return restTemplate;
}
// mappers jSON
@Bean
public ObjectMapper jsonMapper(){
return new ObjectMapper();
}
@Bean
public ObjectMapper jsonMapperShortCreneau() {
ObjectMapper jsonMapperShortCreneau = new ObjectMapper();
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperShortCreneau.setFilters(new SimpleFilterProvider().addFilter("creneauFilter", creneauFilter));
return jsonMapperShortCreneau;
}
@Bean
public ObjectMapper jsonMapperLongRv() {
ObjectMapper jsonMapperLongRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("");
SimpleBeanPropertyFilter creneauFilter = SimpleBeanPropertyFilter.serializeAllExcept("medecin");
jsonMapperLongRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter).addFilter("creneauFilter",
creneauFilter));
return jsonMapperLongRv;
}
@Bean
public ObjectMapper jsonMapperShortRv() {
ObjectMapper jsonMapperShortRv = new ObjectMapper();
SimpleBeanPropertyFilter rvFilter = SimpleBeanPropertyFilter.serializeAllExcept("client", "creneau");
jsonMapperShortRv.setFilters(new SimpleFilterProvider().addFilter("rvFilter", rvFilter));
return jsonMapperShortRv;
}
}
- Zeile 13: Die Klasse [DaoConfig] ist eine Spring-Konfigurationsklasse;
- Zeile 14: Das Paket [rdvmedecins.client.dao] wird nach Spring-Komponenten durchsucht. Dort wird die Komponente [Dao] gefunden;
- Zeilen 17–24: Definieren Sie ein Spring-Singleton namens [restTemplate] (der Methodenname). Diese Methode gibt eine [RestTemplate]-Instanz zurück, das grundlegende Werkzeug, das Spring für die Kommunikation mit einem Webservice oder JSON bereitstellt;
- Zeile 21: Wir könnten schreiben: [RestTemplate restTemplate = new RestTemplate();]. Dies ist in den meisten Fällen ausreichend. Hier möchten wir jedoch die [timeouts] des Clients festlegen. Dazu injizieren wir eine Low-Level-Komponente vom Typ [HttpComponentsClientHttpRequestFactory] (Zeile 20) in die [RestTemplate]-Komponente, wodurch wir diese [timeouts] festlegen können. Die erforderliche Maven-Abhängigkeit wurde bereitgestellt;
- Zeilen 28–57: Definieren Sie JSON-Mapper. Dies sind die JSON-Mapper, die auf der Serverseite (siehe Abschnitt 8.4.11.3) verwendet werden, um den Typ T der [Response<T>]-Antwort zu serialisieren. Dieselben Konverter werden nun auf der Clientseite verwendet, um den Typ T zu deserialisieren;
8.5.8. Die [IDao]-Schnittstelle
Kehren wir zur Anwendungsarchitektur zurück:
![]() |
Die [DAO]-Schicht fungiert als Adapter zwischen der [console]-Schicht und den vom Webservice / JSON bereitgestellten URLs. Ihre [IDao]-Schnittstelle sieht wie folgt aus:
package rdvmedecins.client.dao;
import java.util.List;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
public interface IDao {
// Web service url
public void setUrlServiceWebJson(String url);
// timeout
public void setTimeout(int timeout);
// authentication
public void authenticate(User user);
// customer list
public List<Client> getAllClients(User user);
// list of doctors
public List<Medecin> getAllMedecins(User user);
// list of physician slots
public List<Creneau> getAllCreneaux(User user, long idMedecin);
// find a customer identified by its id
public Client getClientById(User user, long id);
// find a customer identified by its id
public Medecin getMedecinById(User user, long id);
// find an Rv identified by its id
public Rv getRvById(User user, long id);
// find a time slot identified by its id
public Creneau getCreneauById(User user, long id);
// add a RV to the list
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient);
// delete a RV
public void supprimerRv(User user, long idRv);
// list of doctor's appointments on a given day
public List<Rv> getRvMedecinJour(User user, long idMedecin, String jour);
// agenda
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
}
- Zeile 14: Die Methode zum Festlegen der Stamm-URL des Webdienstes / JSON, zum Beispiel [http://localhost:8080];
- Zeile 17: Die Methode zum Festlegen der clientseitigen [Timeouts]. Wir möchten diesen Parameter steuern, da manche HTTP-Clients sehr lange auf eine Antwort warten können, die niemals eintrifft;
- Zeile 20: Die Methode zur Authentifizierung eines Benutzers [login, passwd]. Löst eine Ausnahme aus, wenn der Benutzer nicht erkannt wird;
- Zeilen 22–53: Jede vom Webdienst / JSON bereitgestellte URL ist mit einer Methode der Schnittstelle verknüpft, deren Signatur sich aus der Signatur der serverseitigen Methode ableitet, die die bereitgestellte URL verarbeitet. Nehmen wir zum Beispiel die folgende Server-URL:
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Response<String> getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
- Zeile 1: Wir sehen, dass [idMedecin] und [jour] die URL-Parameter sind. Dies sind die Eingabeparameter für die Methode, die auf der Client-Seite mit dieser URL verknüpft ist;
- Zeile 2: Wir sehen, dass die Servermethode einen Typ [Response<String>] zurückgibt. Dieser Typ [String] ist der Typ des JSON-Werts vom Typ [AgendaMedecinJour]. Der Ergebnistyp der Methode, die auf der Client-Seite mit dieser URL verknüpft ist, lautet [AgendaMedecinJour];
Auf der Client-Seite deklarieren wir die folgende Methode:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour);
Diese Signatur funktioniert, wenn der Server eine Antwort vom Typ [int status, List<String> messages, String body] mit [status==0] sendet. In diesem Fall gilt [messages==null && body!=null]. Sie funktioniert nicht, wenn [status!=0] ist. In diesem Fall haben wir [messages!=null && body==null]. Wir müssen auf irgendeine Weise signalisieren, dass ein Fehler aufgetreten ist. Dazu lösen wir wie folgt eine Ausnahme vom Typ [RdvMedecinsException] aus:
package rdvmedecins.client.dao;
import java.util.List;
public class RdvMedecinsException extends RuntimeException {
private static final long serialVersionUID = 1L;
// error code
private int status;
// list of error messages
private List<String> messages;
public RdvMedecinsException() {
}
public RdvMedecinsException(int code, List<String> messages) {
super();
this.status = code;
this.messages = messages;
}
// getters and setters
...
}
- Zeilen 9 und 11: Die Ausnahme übernimmt die Werte der Felder [status, messages] aus dem vom Server gesendeten [Response<T>]-Objekt;
- Zeile 5: Die Klasse [RdvMedecinsException] erweitert die Klasse [RuntimeException]. Es handelt sich daher um eine nicht behandelte Ausnahme, was bedeutet, dass es nicht erforderlich ist, sie mit einem try/catch-Block zu behandeln oder sie in den Methodensignaturen der Schnittstelle zu deklarieren;
Darüber hinaus haben alle Methoden der Schnittstelle [IDao], die den Webdienst/JSON abfragen, den folgenden Typ [User] als Parameter:
package rdvmedecins.client.entities;
public class User {
// data
private String login;
private String passwd;
// manufacturers
public User() {
}
public User(String login, String passwd) {
this.login = login;
this.passwd = passwd;
}
// getters and setters
...
}
Tatsächlich muss jeder Austausch mit dem Webservice / JSON von einem HTTP-Authentifizierungsheader begleitet werden.
8.5.9. Das Paket [rdvmedecins.clients.console]
Nachdem wir nun mit der Schnittstelle der [DAO]-Schicht vertraut sind, können wir die Konsolenanwendung vorstellen.
![]() |
Die Klasse [Main] sieht wie folgt aus:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
// serializer jSON
static private ObjectMapper mapper = new ObjectMapper();
// connection timeout in milliseconds
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// we retrieve a reference on the [DAO] layer
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// set the URL of the web/json service
dao.setUrlServiceWebJson("http://localhost:8080");
// set timeouts in milliseconds
dao.setTimeout(TIMEOUT);
// Authentication
String message = "/authenticate [admin,admin]";
try {
dao.authenticate(new User("admin", "admin"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [user,user]";
try {
dao.authenticate(new User("user", "user"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [user,x]";
try {
dao.authenticate(new User("user", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [x,x]";
try {
dao.authenticate(new User("x", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
message = "/authenticate [admin,x]";
try {
dao.authenticate(new User("admin", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// customer list
message = "/getAllClients";
try {
showResponse(message, dao.getAllClients(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// list of doctors
message = "/getAllMedecins";
try {
showResponse(message, dao.getAllMedecins(new User("admin", "admin")));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// list of slots for doctor 2
message = "/getAllCreneaux/2";
try {
showResponse(message, dao.getAllCreneaux(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// customer no. 1
message = "/getClientById/1";
try {
showResponse(message, dao.getClientById(new User("admin", "admin"), 1L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// doctor no. 2
message = "/getMedecinById/2";
try {
showResponse(message, dao.getMedecinById(new User("admin", "admin"), 2L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// slot no. 3
message = "/getCreneauById/3";
try {
showResponse(message, dao.getCreneauById(new User("admin", "admin"), 3L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// rv n° 4
message = "/getRvById/4";
try {
showResponse(message, dao.getRvById(new User("admin", "admin"), 4L));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// adding an appointment
message = "/AjouterRv [idClient=4,idCreneau=8,jour=2015-01-08]";
long idRv = 0;
try {
Rv response = dao.ajouterRv(new User("admin", "admin"), "2015-01-08", 8L, 4L);
idRv = response.getId();
showResponse(message, response);
} catch (RdvMedecinsException e) {
showException(message, e);
}
// doctor's appointment list 1 on 2015-01-08
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// doctor's agenda 1 on 2015-01-08
message = "/getAgendaMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getAgendaMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// delete added rv
message = String.format("/supprimerRv [idRv=%s]", idRv);
try {
dao.supprimerRv(new User("admin", "admin"), idRv);
} catch (RdvMedecinsException e) {
showException(message, e);
}
// doctor's appointment list 1 on 2015-01-08
message = "/getRvMedecinJour/1/2015-01-08";
try {
showResponse(message, dao.getRvMedecinJour(new User("admin", "admin"), 1L, "2015-01-08"));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// closing context
context.close();
}
private static void showException(String message, RdvMedecinsException e) {
System.out.println(String.format("URL [%s]", message));
System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
for (String msg : e.getMessages()) {
System.out.println(msg);
}
}
private static <T> void showResponse(String message, T response) throws JsonProcessingException {
System.out.println(String.format("URL [%s]", message));
System.out.println(mapper.writeValueAsString(response));
}
}
- Zeile 19: der JSON-Serializer, mit dem wir die Antwort des Servers anzeigen können, Zeile 184;
- Zeile 25: Die Komponente [AnnotationConfigApplicationContext] ist eine Spring-Komponente, die Konfigurationsanmerkungen aus einer Spring-Anwendung nutzen kann. Wir übergeben die Klasse [AppConfig], die die Anwendung konfiguriert, an ihren Konstruktor;
- Zeile 26: Wir rufen eine Referenz auf die [DAO]-Schicht ab;
- Zeilen 27–30: Wir konfigurieren sie;
- Zeilen 32–169: Wir testen alle Methoden der [IDao]-Schnittstelle;
Die erzielten Ergebnisse lauten wie folgt:
09:20:56.935 [main] INFO o.s.c.a.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy
/authenticate [admin,admin] : OK
URL [/authenticate [user,user]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [user,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/authenticate [x,x]]
L'erreur n° [111] s'est produite :
403 Forbidden
URL [/authenticate [admin,x]]
L'erreur n° [111] s'est produite :
401 Unauthorized
URL [/getAllClients]
[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]
URL [/getAllMedecins]
[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]
URL [/getAllCreneaux/2]
[{"id":25,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":2},{"id":26,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":2},{"id":27,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":2},{"id":28,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":2},{"id":29,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":2},{"id":30,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":2},{"id":31,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":2},{"id":32,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":2},{"id":33,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":2},{"id":34,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":2},{"id":35,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":2},{"id":36,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":2}]
URL [/getClientById/1]
{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"}
URL [/getMedecinById/2]
{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"}
URL [/getCreneauById/3]
{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1}
URL [/getRvById/4]
L'erreur n° [2] s'est produite :
Le rendez-vous d'id [4] n'existe pas
URL [/ajouterRv [idClient=4,idCreneau=8,jour=2015-01-08]]
{"id":144,"version":0,"jour":1420671600000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":0,"idCreneau":0}
URL [/getRvMedecinJour/1/2015-01-08]
[{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}]
URL [/getAgendaMedecinJour/1/2015-01-08]
{"medecin":{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},"jour":1420671600000,"creneauxMedecinJour":[{"creneau":{"id":1,"version":1,"hdebut":8,"mdebut":0,"hfin":8,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":2,"version":1,"hdebut":8,"mdebut":20,"hfin":8,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":3,"version":1,"hdebut":8,"mdebut":40,"hfin":9,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":4,"version":1,"hdebut":9,"mdebut":0,"hfin":9,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":5,"version":1,"hdebut":9,"mdebut":20,"hfin":9,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":6,"version":1,"hdebut":9,"mdebut":40,"hfin":10,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":7,"version":1,"hdebut":10,"mdebut":0,"hfin":10,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"rv":{"id":144,"version":0,"jour":1420675200000,"client":{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"},"creneau":{"id":8,"version":1,"hdebut":10,"mdebut":20,"hfin":10,"mfin":40,"medecin":null,"idMedecin":1},"idClient":4,"idCreneau":8}},{"creneau":{"id":9,"version":1,"hdebut":10,"mdebut":40,"hfin":11,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":10,"version":1,"hdebut":11,"mdebut":0,"hfin":11,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":11,"version":1,"hdebut":11,"mdebut":20,"hfin":11,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":12,"version":1,"hdebut":11,"mdebut":40,"hfin":12,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":13,"version":1,"hdebut":14,"mdebut":0,"hfin":14,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":14,"version":1,"hdebut":14,"mdebut":20,"hfin":14,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":15,"version":1,"hdebut":14,"mdebut":40,"hfin":15,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":16,"version":1,"hdebut":15,"mdebut":0,"hfin":15,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":17,"version":1,"hdebut":15,"mdebut":20,"hfin":15,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":18,"version":1,"hdebut":15,"mdebut":40,"hfin":16,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":19,"version":1,"hdebut":16,"mdebut":0,"hfin":16,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":20,"version":1,"hdebut":16,"mdebut":20,"hfin":16,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":21,"version":1,"hdebut":16,"mdebut":40,"hfin":17,"mfin":0,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":22,"version":1,"hdebut":17,"mdebut":0,"hfin":17,"mfin":20,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":23,"version":1,"hdebut":17,"mdebut":20,"hfin":17,"mfin":40,"medecin":null,"idMedecin":1},"rv":null},{"creneau":{"id":24,"version":1,"hdebut":17,"mdebut":40,"hfin":18,"mfin":0,"medecin":null,"idMedecin":1},"rv":null}]}
URL [/getRvMedecinJour/1/2015-01-08]
[]
09:21:00.258 [main] INFO o.s.c.a.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@52feb982: startup date [Wed Oct 14 09:20:56 CEST 2015]; root of context hierarchy
Wir überlassen es dem Leser, die Ergebnisse mit dem Code abzugleichen. Der Code zeigt, wie die einzelnen Methoden der [DAO]-Schicht aufgerufen werden. Wir möchten hier nur auf einige Punkte hinweisen:
- Zeilen 2–14: zeigen, dass der Server im Falle eines Authentifizierungsfehlers je nach Situation den HTTP-Status [403 Forbidden] oder [401 Unauthorized] zurückgibt;
- Zeilen 30–31: Es wird ein Termin für Arzt Nr. 1 hinzugefügt;
- Zeilen 32–33: Wir sehen diesen Termin. Es ist der einzige für diesen Tag;
- Zeilen 34–35: Er ist auch im Kalender des Arztes sichtbar;
- Zeilen 36–37: Der Termin ist verschwunden. Der Code hat ihn inzwischen gelöscht;
Die Konsolenprotokolle werden durch die folgenden Dateien gesteuert:
![]() |
[application.properties]
logging.level.org.springframework.web=OFF
logging.level.org.hibernate=OFF
spring.main.show-banner=false
logging.level.httpclient.wire=OFF
[logback.xml]
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- log level control -->
<root level="info"> <!-- off, info, debug, warn -->
<appender-ref ref="STDOUT" />
</root>
</configuration>
8.5.10. Implementierung der [DAO]-Schicht
Nun müssen wir den Kern der [DAO]-Schicht vorstellen: die Implementierung ihrer [IDao]-Schnittstelle. Wir werden dies Schritt für Schritt tun.
![]() |
Die [IDao]-Schnittstelle wird durch die abstrakte Klasse [AbstractDao] und deren Unterklasse [Dao] implementiert.
Die übergeordnete Klasse [AbstractDao] sieht wie folgt aus:
package rdvmedecins.client.dao;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
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.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import rdvmedecins.client.entities.User;
public abstract class AbstractDao implements IDao {
// data
@Autowired
protected RestTemplate restTemplate;
protected String urlServiceWebJson;
// URL web service / jSON
public void setUrlServiceWebJson(String url) {
this.urlServiceWebJson = url;
}
public void setTimeout(int timeout) {
// set the timeout for web client requests
HttpComponentsClientHttpRequestFactory factory = (HttpComponentsClientHttpRequestFactory) restTemplate
.getRequestFactory();
factory.setConnectTimeout(timeout);
factory.setReadTimeout(timeout);
}
private String getBase64(User user) {
// encodes user and password in base 64 - requires
// java 8
String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
// generic request
protected String getResponse(User user, String url, String jsonPost) {
...
}
}
- Zeile 20: Die Klasse ist abstrakt, was verhindert, dass wir sie als Spring-Komponente kennzeichnen können. Ihre Unterklasse wird als solche gekennzeichnet;
- Zeilen 23–24: Wir injizieren die [restTemplate]-Bean, die wir in der Konfigurationsklasse [AppConfig] definiert haben;
- Zeile 25: Die Stamm-URL des Webdienstes / JSON;
- Zeilen 32–38: Festlegen des Client-Timeouts während des Wartens auf eine Antwort vom Server;
- Zeile 34: Wir rufen die Komponente [HttpComponentsClientHttpRequestFactory] ab, die wir bei der Erstellung in die [restTemplate]-Bean injiziert haben (siehe [AppConfig]);
- Zeile 36: Wir legen die maximale Wartezeit für den Client beim Aufbau einer Verbindung zum Server fest;
- Zeile 37: Wir legen die maximale Wartezeit für den Client fest, während er auf eine Antwort auf eine seiner Anfragen wartet;
Die Implementierung der Methoden für die Kommunikation mit dem Server wird in die folgende generische Methode integriert:
// generic request
protected String getResponse(User user, String url, String jsonPost) {
...
}
- Zeile 2: Die Parameter von [getResponse] lauten wie folgt:
- [User user]: der Benutzer, der die Verbindung herstellt;
- [String url]: die abzufragende URL. Dies ist der Endteil der URL; der erste Teil wird vom Feld [urlServiceWebJson] der Klasse bereitgestellt,
- [String jsonPost]: die JSON-Zeichenkette, die gesendet werden soll. Ist dieser Wert vorhanden, wird die URL mit einem POST-Request abgefragt; andernfalls erfolgt die Abfrage mit einem GET-Request;
Fahren wir fort:
// generic request
protected String getResponse(User user, String url, String jsonPost) {
// url : URL to contact
// jsonPost: the jSON value to be posted
try {
// request execution
RequestEntity<?> request;
if (jsonPost == null) {
HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url))).accept(MediaType.APPLICATION_JSON);
if (user != null) {
headersBuilder = headersBuilder.header("Authorization", getBase64(user));
}
request = headersBuilder.build();
} else {
BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
.header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
if (user != null) {
bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
}
request = bodyBuilder.body(jsonPost);
}
// execute the query
return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
}).getBody();
} catch (URISyntaxException e) {
throw new RdvMedecinsException(20, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(21, getMessagesForException(e));
}
}
- Zeilen 23–24: 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 hätten auch eine andere Methode als [exchange] wählen können. Der zweite Parameter des Aufrufs gibt den Typ der erwarteten Antwort an, in diesem Fall eine JSON-Zeichenkette. Der erste Parameter ist die [RequestEntity]-Anfrage (Zeile 7). Das Ergebnis der [exchange]-Methode ist vom Typ [ResponseEntity<String>]. Der Typ [ResponseEntity] kapselt die vollständige Antwort des Servers, einschließlich der HTTP-Header und des vom Server gesendeten [ ]-Dokuments. In ähnlicher Weise kapselt der Typ [RequestEntity] die gesamte Anfrage des Clients, einschließlich der HTTP-Header und aller übermittelten Daten;
- Zeile 23: Dies ist der Body des [ResponseEntity<String>]-Objekts, das an die aufrufende Methode zurückgegeben wird, d. h. die vom Server gesendete JSON-Zeichenkette;
- Zeilen 9–21: Wir müssen die [RequestEntity]-Anfrage erstellen. Diese unterscheidet sich je nachdem, ob wir eine GET- oder eine POST-Anfrage verwenden;
- Zeile 9: 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, die sie aufbauen, verketten:
- Die Methode [RequestEntity.get] nimmt die Ziel-URL als Parameter in Form einer URI-Instanz entgegen;
- Mit der Methode [accept] können Sie die Elemente des HTTP-Headers [Accept] definieren. Hier geben wir an, dass wir den Typ [application/json] akzeptieren, den der Server senden wird;
- das Ergebnis dieser Methodenverkettung ist ein Typ [HeadersBuilder];
- Zeilen 10–12: Wenn der Parameter [User user] nicht null ist, fügen wir den HTTP-Header [Authorization] in die Anfrage ein;
- Zeile 13: Die Methode [HeadersBuilder.build] verwendet diese Informationen, um den Typ [RequestEntity] der Anfrage zu erstellen;
- Zeile 15: Die Anfrage ist ein POST. Mit der Methode [RequestEntity.post] können Sie eine POST-Anfrage erstellen, indem Sie die verschiedenen Methoden, die sie aufbauen, verketten:
- Die Methode [RequestEntity.post] nimmt die Ziel-URL als Parameter in Form einer URI-Instanz entgegen;
- die Methode [header] ermöglicht es Ihnen, die HTTP-Header zu definieren, die Sie verwenden möchten, in diesem Fall den Autorisierungs-Header,
- die folgende [header]-Methode fügt den Header [Content-Type: application/json] in die Anfrage ein, um anzugeben, dass die gesendeten Daten als JSON-Zeichenkette ankommen;
- die Methode [accept] gibt an, dass wir den Typ [application/json] akzeptieren, den der Server senden wird;
- Zeilen 17–19: Wenn der Parameter [User user] nicht null ist, wird der HTTP-Header [Authorization] in die Anfrage aufgenommen;
- Zeile 20: Die Methode [BodyBuilder.body] legt den gesendeten Wert fest. Dies ist der zweite Parameter der generischen Methode [getResponse] (Zeile 2);
- Zeilen 25–28: Wenn ein Fehler auftritt, wird eine [RdvMedecinsException] ausgelöst;
Die Methode [getMessagesForException] in den Zeilen 26 und 28 lautet wie folgt:
// list of exception error messages
protected static List<String> getMessagesForException(Exception exception) {
// retrieve the list of exception error messages
Throwable cause = exception;
List<String> erreurs = new ArrayList<String>();
while (cause != null) {
// the message is retrieved only if it is !=null and not blank
String message = cause.getMessage();
if (message != null) {
message = message.trim();
if (message.length() != 0) {
erreurs.add(message);
}
}
// next cause
cause = cause.getCause();
}
return erreurs;
}
Die private Methode [getBase64] gibt die Base64-Kodierung der Zeichenkette 'login:passwd' für den HTTP-Authentifizierungsheader zurück:
private String getBase64(User user) {
// encodes user and password in base 64 - requires java 8
String chaîne = String.format("%s:%s", user.getLogin(), user.getPasswd());
return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
}
Die Klasse [Dao] erweitert die Klasse [AbstractDao] wie folgt:
package rdvmedecins.client.dao;
import java.io.IOException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import rdvmedecins.client.entities.AgendaMedecinJour;
import rdvmedecins.client.entities.Client;
import rdvmedecins.client.entities.Creneau;
import rdvmedecins.client.entities.Medecin;
import rdvmedecins.client.entities.Rv;
import rdvmedecins.client.entities.User;
import rdvmedecins.client.requests.PostAjouterRv;
import rdvmedecins.client.requests.PostSupprimerRv;
import rdvmedecins.client.responses.Response;
@Service
public class Dao extends AbstractDao implements IDao {
// mappers jSON
@Autowired
ObjectMapper jsonMapper;
@Autowired
private ObjectMapper jsonMapperShortCreneau;
@Autowired
private ObjectMapper jsonMapperLongRv;
@Autowired
private ObjectMapper jsonMapperShortRv;
public List<Client> getAllClients(User user) {
...
}
public List<Medecin> getAllMedecins(User user) {
...
}
...
}
- Zeile 22: Die [Dao]-Klasse ist eine Spring-Komponente. Hier wurde die Annotation [@Service] verwendet. Wir hätten auch weiterhin die bis zu diesem Punkt verwendete Annotation [@Component] verwenden können;
- Zeilen 26–36: Injektion der vier JSON-Mapper, die in der Konfigurationsklasse [DaoConfig] definiert sind;
Die Methoden der Klasse [Dao] folgen alle demselben Muster. Wir werden eine GET-Operation und eine POST-Operation näher erläutern.
Zunächst eine [GET]-Anfrage:
public AgendaMedecinJour getAgendaMedecinJour(User user, long idMedecin, String jour) {
// the answer
Response<AgendaMedecinJour> response;
// the diary
String jsonResponse = getResponse(user, String.format("%s/%s/%s", "/getAgendaMedecinJour", idMedecin, jour), null);
try {
// diary AgendaMedecinJour
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<AgendaMedecinJour>>() {
});
} catch (IOException e) {
throw new RdvMedecinsException(401, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(402, getMessagesForException(e));
}
// response analysis
int status = response.getStatus();
if (status != 0) {
throw new RdvMedecinsException(status, response.getMessages());
} else {
return response.getBody();
}
}
- Zeile 5: Die generische Methode [getResponse] wird aufgerufen. Die tatsächlich verwendeten Parameter sind wie folgt:
- 1: der Benutzer;
- 2: die Ziel-URL;
- 3: der zu übermittelnde Wert. In diesem Fall gibt es keinen;
- Zeile 5: Der Aufruf wurde nicht in einen try/catch-Block eingeschlossen. Die Methode [getResponse] kann eine [RdvMedecinsException] auslösen. Wenn diese Ausnahme ausgelöst wird, wird sie an die Methode weitergeleitet, die oben die Methode [getAgendaMedecinJour] aufgerufen hat;
- Zeile 8: Die URL [/getAgendaMedecinJour] gibt ein [Response<AgendaMedecinJour>] zurück, das serverseitig durch den JSON-Mapper [jsonMapperLongRv] in JSON serialisiert wurde. Wir verwenden denselben Mapper, um die empfangene JSON-Zeichenkette zu deserialisieren;
- Zeilen 10–13: Tritt in Zeile 9 ein Fehler auf, wird eine [RdvMedecinsException] ausgelöst;
- Zeilen 16–21: Die vom Server gesendete Antwort wird analysiert;
- Zeilen 17–18: Wenn der Server einen Fehler gemeldet hat, wird eine Ausnahme mit den vom Server bereitgestellten Informationen ausgelöst;
- Zeilen 19–21: Andernfalls wird der Terminplan des Arztes zurückgegeben;
Die untersuchte POST-Anfrage sieht wie folgt aus:
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
// the answer
Response<Rv> response;
try {
// the Rv
String jsonResponse = getResponse(user, "/ajouterRv",
jsonMapper.writeValueAsString(new PostAjouterRv(idClient, idCreneau, jour)));
// the Rv Rv
response = jsonMapperLongRv.readValue(jsonResponse, new TypeReference<Response<Rv>>() {
});
} catch (RdvMedecinsException e) {
throw e;
} catch (IOException e) {
throw new RdvMedecinsException(381, getMessagesForException(e));
} catch (RuntimeException e) {
throw new RdvMedecinsException(382, getMessagesForException(e));
}
// response analysis
int status = response.getStatus();
if (status != 0) {
throw new RdvMedecinsException(status, response.getMessages());
} else {
return response.getBody();
}
}
- Zeile 6: Die Methode [getResponse] wird mit den folgenden Parametern aufgerufen:
- 1: der Benutzer;
- 2: die Ziel-URL,
- 3: der gesendete Wert: Wir übergeben den JSON-Wert vom Typ [PostAjouter], der mit den Informationen erstellt wurde, die die Methode als Parameter erhalten hat. Wir verwenden einen JSON-Mapper ohne Filter;
- Zeile 9: Auf der Serverseite hat der JSON-Mapper [jsonMapperLongRv] die Serverantwort serialisiert. Auf der Clientseite verwenden wir denselben Mapper, um sie zu deserialisieren;
- Zeile 6: Die URL [/ajouterRv] gibt den JSON-Wert vom Typ [Response<Rv>] zurück;
- Zeilen 4–11: Hier wurde die Methode [getResponse] in einen try/catch-Block gesetzt, da die Serialisierung des gesendeten Werts eine Ausnahme auslösen kann. Die Methode [getResponse] löst wahrscheinlich eine [RdvMedecinsException] aus. In diesem Fall versuchen wir es einfach erneut (Zeilen 11–12);
Der folgende Code (Zeilen 13–24) ähnelt dem soeben besprochenen. Der einzige Unterschied zu einer GET-Operation besteht daher im zweiten Parameter der Methode [getResponse], der die JSON-Darstellung des zu sendenden Werts sein muss.
Die anderen Methoden basieren auf demselben Modell.
8.5.11. Ausnahme
Bei der Durchführung verschiedener Tests stießen wir auf eine Anomalie, die in der folgenden Klasse [Anomalie] zusammengefasst ist:
package rdvmedecins.clients.console;
import java.io.IOException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import rdvmedecins.client.config.DaoConfig;
import rdvmedecins.client.dao.IDao;
import rdvmedecins.client.dao.RdvMedecinsException;
import rdvmedecins.client.entities.User;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Anomalie {
// serializer jSON
static private ObjectMapper mapper = new ObjectMapper();
// connection timeout in milliseconds
static private int TIMEOUT = 1000;
public static void main(String[] args) throws IOException {
// we retrieve a reference on the [DAO] layer
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
IDao dao = context.getBean(IDao.class);
// set the URL of the web/json service
dao.setUrlServiceWebJson("http://localhost:8080");
// set timeouts in milliseconds
dao.setTimeout(TIMEOUT);
// Authentication
String message = "/authenticate [admin,admin]";
try {
dao.authenticate(new User("admin", "admin"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// Authentication
message = "/authenticate [admin,x]";
try {
dao.authenticate(new User("admin", "x"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// Authentication
message = "/authenticate [user,user]";
try {
dao.authenticate(new User("user", "user"));
System.out.println(String.format("%s : OK", message));
} catch (RdvMedecinsException e) {
showException(message, e);
}
// closing context
context.close();
}
private static void showException(String message, RdvMedecinsException e) {
System.out.println(String.format("URL [%s]", message));
System.out.println(String.format("L'erreur n° [%s] s'est produite :", e.getStatus()));
for (String msg : e.getMessages()) {
System.out.println(msg);
}
}
}
- Zeilen 31–38: Der Benutzer [admin, admin] wird authentifiziert;
- Zeilen 40–47: Authentifizierung des Benutzers [admin, x], der ein falsches Passwort eingegeben hat;
- Zeilen 49–56: Der Benutzer [user, user] wird authentifiziert; dieser Benutzer existiert, ist jedoch nicht autorisiert;
Hier sind die Ergebnisse:
- Zeile 2: Entgegen den Erwartungen wurde der Benutzer [admin, x] akzeptiert;
Wenn wir die Zeilen 33–38 des Codes auskommentieren, erhalten wir folgendes Ergebnis:
was das erwartete Ergebnis ist. Es scheint, als ob, sobald sich der Benutzer [admin, admin] zum ersten Mal erfolgreich angemeldet hat, sein Passwort für nachfolgende Anmeldungen nicht mehr erforderlich ist. Dies ist tatsächlich der Fall. Standardmäßig verwendet Spring Security einen Sitzungsmechanismus, der sicherstellt, dass ein Benutzer, sobald er sich einmal authentifiziert hat, dies bei nachfolgenden Anfragen nicht erneut tun muss. Sie können die [Spring Security]-Konfiguration im Webserver / JSON so ändern, dass dies nicht mehr der Fall ist:
![]() |
Die Datei [SecurityConfig] muss wie folgt geändert werden:
@Override
protected void configure(HttpSecurity http) throws Exception {
...
// no session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
- Zeile 5 legt fest, dass keine Sicherheitssitzung vorhanden sein soll;
Damit war das Problem behoben.
8.6. Spring / Thymeleaf serverseitiges Rendering
8.6.1. Einleitung
Kehren wir zur Architektur der zu erstellenden Client/Server-Anwendung zurück:
![]() |
- Der [Web2]-Web-/JSON-Server wurde erstellt;
- die [DAO]-Schicht des [Web1]-Clients wurde erstellt;
Die Beziehung zwischen dem [Web1]-Server und den Client-Browsern ist eine Client-Server-Beziehung, bei der der Server ein Web/JSON-Server ist. Tatsächlich liefert [Web1] HTML-Streams, die in einer JSON-Zeichenkette gekapselt sind. Die Client-Server-Architektur sieht wie folgt aus:
![]() |
- Wir haben eine Client-[2]/Server-[1]-Architektur, bei der Client und Server über JSON kommunizieren;
- In [1] liefert die Spring MVC/Thymeleaf-Webschicht Ansichten, Ansichtsfragmente und Daten im JSON-Format. Der Server ist daher ein Web-/JSON-Server wie Server [Web1]. Er ist zudem zustandslos;
- in [2]: Der JavaScript-Code, der in die beim Start der Anwendung geladene Ansicht eingebettet ist, ist in Schichten strukturiert:
- Die [Präsentationsschicht] verarbeitet Benutzerinteraktionen,
- die [DAO]-Schicht übernimmt den Datenzugriff über den [Web2]-Server;
- Der Client [2] speichert bestimmte Ansichten im Cache, um die Serverlast zu reduzieren;
Wir werden den mit Spring MVC/Thymeleaf implementierten Web-/JSON-Server [Web1] in mehreren Schritten aufbauen:
- Erkundung des Bootstrap-CSS-Frameworks;
- Schreiben der Ansichten;
- Erstellen des Controllers;
Anschließend erstellen wir separat den JS-Client für den Server [Web1]. Um deutlich zu machen, dass dieser Client eine gewisse Unabhängigkeit vom Server [Web1] besitzt, erstellen wir ihn mit dem Tool [WebStorm] statt mit STS.
Im weiteren Verlauf werden bestimmte Details weggelassen, da sie vom eigentlichen Schwerpunkt – der Code-Organisation – ablenken könnten. Interessierte Leser finden den vollständigen Code auf der Website zu diesem Dokument.
8.6.2. Das STS-Projekt
![]() |
- in [1], der Java-Code;
- in [2], die Ansichten;
Die Maven-Konfiguration in [pom.xml] lautet 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>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-springthymeleaf-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-springthymeleaf-server</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>istia.st.rdvmedecins</groupId>
<artifactId>rdvmedecins-webjson-client-console</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<properties>
<start-class>rdvmedecins.springthymeleaf.server.boot.Boot</start-class>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
...
</project>
- Zeilen 16–19: Das Projekt ist ein Thymeleaf-Projekt;
- Zeilen 20–24: das auf der soeben erstellten [DAO]-Schicht basiert;
Die Java-Konfiguration wird von zwei Dateien verwaltet:
![]() |
Die [web]-Ebene wird durch die folgende [WebConfig]-Datei konfiguriert:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.SpringTemplateEngine;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@EnableAutoConfiguration
public class WebConfig extends WebMvcConfigurerAdapter {
// ----------------- layer configuration [web]
@Bean
public MessageSource messageSource() {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("i18n/messages");
return messageSource;
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(true);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
@Bean
SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
// dispatcherservlet configuration for CORS headers
@Bean
public DispatcherServlet dispatcherServlet() {
DispatcherServlet servlet = new DispatcherServlet();
servlet.setDispatchOptionsRequest(true);
return servlet;
}
}
Wir sind schon einmal auf alle Elemente dieser Konfiguration gestoßen. Nur zur Erinnerung: Die Zeilen 42–47 sind erforderlich, wenn Sie den Server mit Cross-Origin-Anfragen (CORS) abfragen möchten. Das ist hier der Fall.
Die Klasse [AppConfig] konfiguriert die gesamte Anwendung:
package rdvmedecins.springthymeleaf.server.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.client.config.DaoConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// root web service / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// timeout in milliseconds
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
}
- Zeile 11: [AppConfig] importiert die Konfiguration für die [DAO]-Schicht und die [web]-Schicht;
- Zeilen 15–16: die Anmeldedaten, die es der Anwendung ermöglichen, auf den Anwendungsstartprozess zuzugreifen, um Ärzte und Kunden zwischenzuspeichern;
- Zeile 18: die URL des [Web1]-Webdienstes / JSON;
- Zeile 20: das Timeout für die HTTP-Aufrufe der Anwendung;
- Zeile 22: Ein boolescher Wert zum Aktivieren oder Deaktivieren domänenübergreifender Aufrufe;
Schließlich wird in [application.properties] der Tomcat-Server so konfiguriert, dass er auf Port 8081 läuft:
![]() |
server.port=8081
8.6.3. Anwendungsfunktionen
Diese wurden in Abschnitt 8.2 beschrieben. Wir werden sie nun noch einmal durchgehen. Über einen Browser rufen wir die URL [http://localhost:8081/boot.html] auf:
![]() |
- in [1], die Anmeldeseite der Anwendung;
- in [2] und [3] den Benutzernamen und das Passwort des Benutzers, der die Anwendung nutzen möchte. Es gibt zwei Benutzer: admin/admin (Benutzername/Passwort) mit der Rolle (ADMIN) und user/user mit der Rolle (USER). Nur die Rolle ADMIN verfügt über die Berechtigung zur Nutzung der Anwendung. Die Rolle USER wird in diesem Anwendungsfall ausschließlich zur Veranschaulichung der Serverantwort herangezogen;
- in [4] die Schaltfläche, über die Sie eine Verbindung zum Server herstellen können;
- in [5] die Sprache der Anwendung. Es gibt zwei: Französisch (Standard) und Englisch;
- in [6] die Server-URL [rdvmedecins-springthymeleaf-server];
![]() |
- in [1] melden Sie sich an;
![]() |
- Sobald Sie angemeldet sind, können Sie den gewünschten Arzt [2] und den Termin [3] auswählen. Sobald ein Arzt und ein Termin ausgewählt wurden, wird der Kalender automatisch angezeigt:
![]() |
- Sobald der Kalender des Arztes angezeigt wird, können Sie einen Termin buchen [5];
![]() |
- Wählen Sie unter [6] den Patienten für den Termin aus und bestätigen Sie Ihre Auswahl unter [7];
![]() |
Sobald der Termin bestätigt ist, gelangen Sie automatisch zurück zum Kalender, wo der neue Termin nun aufgeführt ist. Dieser Termin kann später gelöscht werden [8].
Die wichtigsten Funktionen wurden beschrieben. Sie sind einfach. Schließen wir mit den Spracheinstellungen ab:
![]() |
- In [1] wechseln Sie von Französisch zu Englisch;
![]() |
- In [2] wechselt die Ansicht zu Englisch, einschließlich des Kalenders;
8.6.4. Schritt 1: Einführung in das Bootstrap-CSS-Framework
![]() |
Im obigen Web-Client verwenden die HTML-Seiten das Bootstrap-CSS-Framework [http://getbootstrap.com/], das wir nun vorstellen werden.
8.6.4.1. Das Beispielprojekt
Das Beispielprojekt sieht wie folgt aus:
![]() |
- in [1]: das Projekt als Ganzes;
- in [2]: der Java-Code;
- in [3]: die JavaScript-Skripte;
![]() |
- in [4]: die JavaScript-Bibliotheken;
- in [5]: die Thymeleaf-Ansichten;
- in [6]: Stylesheets;
8.6.4.1.1. Maven-Konfiguration
Die Datei [pom.xml] ist für ein Thymeleaf-Maven-Projekt bestimmt:
<?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>istia.st</groupId>
<artifactId>rdvmedecins-webjson-client-bootstrap</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>rdvmedecins-webjson-client-bootstrap</name>
<description>Démos Bootstrap</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.rdvmedecins.BootstrapDemo</start-class>
<java.version>1.7</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
8.6.4.1.2. Java-Konfiguration
![]() |
Die Klasse [BootstrapDemo] konfiguriert die Spring/Thymeleaf-Anwendung:
package istia.st.rdvmedecins;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.thymeleaf.spring4.templateresolver.SpringResourceTemplateResolver;
@EnableAutoConfiguration
@ComponentScan({ "istia.st.rdvmedecins" })
public class BootstrapDemo extends WebMvcConfigurerAdapter {
public static void main(String[] args) {
SpringApplication.run(BootstrapDemo.class, args);
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setPrefix("classpath:/templates/");
templateResolver.setSuffix(".xml");
templateResolver.setTemplateMode("HTML5");
templateResolver.setCacheable(true);
templateResolver.setCharacterEncoding("UTF-8");
return templateResolver;
}
}
Wir sind dieser Art von Code bereits begegnet.
8.6.4.1.3. Der Spring-Controller
![]() |
Der [BootstrapController] sieht wie folgt aus:
package istia.st.rdvmedecins;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class BootstrapController {
@RequestMapping(value = "/bs-01", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bso1() {
return "bs-01";
}
@RequestMapping(value = "/bs-02", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs02() {
return "bs-02";
}
@RequestMapping(value = "/bs-03", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs03() {
return "bs-03";
}
@RequestMapping(value = "/bs-04", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs04() {
return "bs-04";
}
@RequestMapping(value = "/bs-05", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs05() {
return "bs-05";
}
@RequestMapping(value = "/bs-06", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs06() {
return "bs-06";
}
@RequestMapping(value = "/bs-07", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs07() {
return "bs-07";
}
@RequestMapping(value = "/bs-08", method = RequestMethod.GET, produces = "text/html; charset=UTF-8")
public String bs08() {
return "bs-08";
}
}
Die Aktionen dienen lediglich dazu, von Thymeleaf verarbeitete Ansichten anzuzeigen.
8.6.4.1.4. Die Datei [application.properties]
Die Datei [application.properties] konfiguriert den eingebetteten Tomcat-Server:
server.port=8082
8.6.4.2. Beispiel 1: Das Jumbotron
Die Aktion [/bs-01] zeigt die folgende Ansicht [bs-01.xml] an:
![]() |
Die Ansicht [bs-01.xml] sieht wie folgt aus:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
</head>
<body id="body">
<div class="container">
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- content -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- error -->
<div id="erreur" class="alert alert-danger">
<span>Ici, un texte d'erreur</span>
</div>
</div>
</body>
</html>
- Zeile 7: die CSS-Datei des Bootstrap-Frameworks;
- Zeile 8: eine lokale CSS-Datei;
- Zeile 13: displays [1];
- Zeilen 19–21: Anzeige [2];
- Zeile 11: Die CSS-Klasse [container] definiert einen Anzeigebereich innerhalb des Browsers;
- Zeile 19: Die CSS-Klasse [alert] zeigt einen farbigen Bereich an. Die Klasse [alert-danger] verwendet eine vordefinierte Farbe. Es gibt mehrere davon [alert-info, alert-warning,...];
Das Jumbotron [1] wird durch die folgende Ansicht [jumbotron.xml] generiert:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1>
Les Médecins
<br />
associés
</h1>
</div>
</div>
</div>
</section>
- Zeile 4: Der Bereich hat die CSS-Klasse [jumbotron];
- Zeile 5: Die Klasse [row] definiert eine Zeile mit 12 Spalten;
- Zeile 6: Die Klasse [col-md-2] definiert einen zweispaltigen Bereich innerhalb der Zeile;
- Zeile 7: In diesen beiden Spalten wird ein Bild platziert;
- Zeilen 9–15: In den verbleibenden 10 Spalten wird Text platziert;
8.6.4.3. Beispiel #2: Die Navigationsleiste
Die Aktion [/bs-02] zeigt die folgende Ansicht [bs-02.xml] an:
![]() |
Neu ist die Navigationsleiste [1] mit ihrem Eingabeformular und den Schaltflächen:
Die Ansicht [bs-02.xml] sieht wie folgt aus:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- scripts JS -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/js/bs-02.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar1"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- content -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- Zeile 10: Wir importieren jQuery;
- Zeile 11: ein lokales JS-Skript;
- Zeile 16: die Navigationsleiste;
Die Navigationsleiste wird durch die folgende Ansicht [navbar1.xml] generiert:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- identification form -->
<div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" />
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" />
</div>
<button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
</div>
</div>
</div>
</div>
</section>
![]() |
- Zeile 3: Die Klasse [navbar] gestaltet die Navigationsleiste. Die Klasse [navbar-inverse] verleiht ihr einen schwarzen Hintergrund. Die Klasse [navbar-fixed-top] sorgt dafür, dass die Navigationsleiste beim Scrollen der vom Browser angezeigten Seite oben auf dem Bildschirm bleibt;
- Zeilen 5–13: definieren den Bereich [1]. Dabei handelt es sich typischerweise um eine Reihe von Klassen, die ich nicht verstehe. Ich verwende die Komponente so, wie sie ist;
- Zeilen 14–26: Definieren einen „responsiven“ Bereich der Navigationsleiste. Auf einem Smartphone wird dieser Bereich zu einem Menübereich zusammengeklappt;
- Zeile 15: ein Bild, das derzeit ausgeblendet ist;
- Zeilen 17–25: Die Klasse [navbar-form] gestaltet ein Formular in der Navigationsleiste. Die Klasse [navbar-right] positioniert es rechts in der Navigationsleiste;
- Zeilen 21–23: die beiden Eingabefelder des Formulars aus Zeile 17 [2]. Sie befinden sich innerhalb einer [form-group]-Klasse, die die Elemente eines Formulars umschließt, und jedes von ihnen hat die [form-control]-Klasse;
- Zeile 24: Die Klasse [btn], die eine Schaltfläche definiert, wird durch die Klasse [btn-success] ergänzt, die ihr die grüne Farbe verleiht;
- Zeile 24: Wenn die Schaltfläche [Login] angeklickt wird, wird die folgende JS-Funktion ausgeführt:
function connecter() {
showInfo("Connexion demandée...");
}
function showInfo(message) {
$("#info").text(message);
}
Hier ist ein Beispiel:

8.6.4.4. Beispiel #3: Die Listen-Schaltfläche
Die Aktion [/bs-03] zeigt die folgende Ansicht [bs-03.xml] an:
![]() |
- Die neue Funktion ist die Listen-Schaltfläche [1], auch als „Dropdown“ bekannt;
Der Code für die Ansicht [bs-03.xml] lautet wie folgt:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap core JavaScript ================================================== -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script src="resources/vendor/bootstrap.js"></script>
<!-- local script -->
<script type="text/javascript" src="resources/js/bs-03.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar2"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- content -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- Zeile 11: Die Dropdown-Schaltfläche benötigt die Bootstrap-JS-Datei;
- Zeile 18: die neue Navigationsleiste;
Die Ansicht [navbar2.xml] sieht wie folgt aus:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- identification form -->
<div class="navbar-form navbar-right" role="form" id="formulaire" method="post">
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" />
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" />
</div>
<button type="button" class="btn btn-success" onclick="javascript:connecter()">Connexion</button>
<!-- languages -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langues</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')">Français</a>
</li>
<li>
<a href="javascript:setLang('en')">English</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initNavBar2();
/*]]>*/
</script>
</section>
- Zeilen 25–40: Definieren Sie die Dropdown-Schaltfläche;
- Zeile 27: Die Klasse [btn-danger] verleiht ihr die rote Farbe;
- Zeilen 32–39: die Listenelemente. Jedes ist ein Link, der mit einer JavaScript-Funktion verknüpft ist;
- Zeilen 46–51: ein JavaScript-Skript, das nach dem Laden des Dokuments ausgeführt wird;
Das JS-Skript [bs-03.js] lautet wie folgt:
function initNavBar2() {
// dropdown des langues
$('.dropdown-toggle').dropdown();
}
function connecter() {
showInfo("Connexion demandée...");
}
function setLang(lang) {
var msg;
switch (lang) {
case 'fr':
msg = "Vous avez choisi la langue française...";
break;
case 'en':
msg = "You have selected english language...";
break;
}
showInfo(msg);
}
function showInfo(message) {
$("#info").text(message);
}
- Zeilen 1–4: Die Funktion, die das [dropdown] initialisiert. [$('.dropdown-toggle')] lokalisiert das Element mit der Klasse [dropdown-toggle]. Dies ist die Dropdown-Schaltfläche (Zeile 28 der Ansicht). Die JS-Funktion [dropdown()] – definiert in der JS-Datei [bootstrap.js] – wird darauf angewendet. Erst nach diesem Vorgang verhält sich die Schaltfläche wie eine Dropdown-Schaltfläche;
- Zeilen 10–21: Die Funktion, die ausgeführt wird, wenn eine Sprache ausgewählt wird;
Hier ein Beispiel:

8.6.4.5. Beispiel #4: ein Menü
Die Aktion [/bs-04] zeigt die folgende Ansicht [bs-04.xml] an:
![]() |
Ein Menü [1] wurde hinzugefügt.
Die Ansicht [bs-04.xml] sieht wie folgt aus:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap core JavaScript ================================================== -->
<script src="resources/vendor/jquery-2.1.1.min.js"></script>
<script src="resources/vendor/bootstrap.js"></script>
<!-- local script -->
<script type="text/javascript" src="resources/js/bs-04.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar3"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- content -->
<div id="content">
<h1>Ici un contenu</h1>
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- Zeile 18: Füge eine neue Navigationsleiste ein;
Die Ansicht [navbar3.xml] sieht wie folgt aus:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<ul class="nav navbar-nav">
<li class="active" id="lnkAfficherAgenda">
<a href="javascript:afficherAgenda()">Agenda </a>
</li>
<li class="active" id="lnkAccueil">
<a href="javascript:retourAccueil()">Retour Accueil </a>
</li>
<li class="active" id="lnkRetourAgenda">
<a href="javascript:retourAgenda()">Retour Agenda </a>
</li>
<li class="active" id="lnkValiderRv">
<a href="javascript:validerRv()">Valider </a>
</li>
</ul>
<!-- right-hand buttons -->
<div class="navbar-form navbar-right" role="form">
<!-- disconnect -->
<button type="button" class="btn btn-success" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- languages -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langues</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')">Français</a>
</li>
<li>
<a href="javascript:setLang('en')">English</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initNavBar3();
/*]]>*/
</script>
</section>
- Zeilen 16–29: Erstellen des Menüs mit vier Optionen, die jeweils mit einem JS-Skript verknüpft sind;
- Zeilen 55–60: ein Skript, das beim Laden der Seite ausgeführt wird;
Das JS-Skript [bs-04.js] lautet wie folgt:
...
function initNavBar3() {
// dropdown des langues
$('.dropdown-toggle').dropdown();
// l'moving image
loading = $("#loading");
loading.hide();
}
function afficherAgenda() {
showInfo("option [Agenda] cliquée...");
}
function retourAccueil() {
showInfo("option [Retour accueil] cliquée...");
}
function retourAgenda() {
showInfo("option [Retour agenda] cliquée...");
}
function validerRv() {
showInfo("option [Valider] cliquée...");
}
function setMenu(show) {
// les liens du menu
var lnkAfficherAgenda = $("#lnkAfficherAgenda");
var lnkAccueil = $("#lnkAccueil");
var lnkValiderRv = $("#lnkValiderRv");
var lnkRetourAgenda = $("#lnkRetourAgenda");
// on les met dans un dictionnaire
var options = {
"lnkAccueil" : lnkAccueil,
"lnkAfficherAgenda" : lnkAfficherAgenda,
"lnkValiderRv" : lnkValiderRv,
"lnkRetourAgenda" : lnkRetourAgenda
}
// on cache tous les liens
for ( var key in options) {
options[key].hide();
}
// on affiche ceux qui sont demandés
for (var i = 0; i < show.length; i++) {
var option = show[i];
options[option].show();
}
}
- Zeilen 2–18: die Funktion zur Initialisierung der Seite;
- Zeile 4: Anzeige der Schaltfläche zur Sprachauswahl;
- Zeilen 6–7: Das animierte Bild wird ausgeblendet;
- Zeilen 26–48: eine [setMenu]-Funktion, mit der Sie festlegen können, welche Optionen sichtbar sein sollen;
Öffnen wir die Entwicklerkonsole (Strg-Umschalt-I) und geben wir den folgenden Code ein [1]:
![]() |
Kehren Sie dann zum Browser zurück. Das Menü hat sich geändert [2]:
8.6.4.6. Beispiel #5: Eine Dropdown-Liste
Die Aktion [/bs-05] zeigt die folgende Ansicht [bs-05.xml] an:
![]() |
Die neue Funktion befindet sich unter [1]. Hier verwenden wir eine Komponente, die außerhalb von Bootstrap bereitgestellt wird: [bootstrap-select] [http://silviomoreto.github.io/bootstrap-select/].
Der Code für die Ansicht [bs-05.xml] lautet wie folgt:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<!-- local script -->
<script type="text/javascript" src="resources/js/bs-05.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar3"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- content -->
<div id="content" th:include="choixmedecin">
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- Zeile 8: das für die Dropdown-Liste erforderliche CSS;
- Zeile 13: die für die Dropdown-Liste erforderliche JS-Datei;
- Zeile 24: die Dropdown-Liste;
Die Ansicht [choixmedecin.xml] sieht wie folgt aus:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
</div>
<!-- local script -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initChoixMedecin();
/*]]>*/
</script>
</section>
- Zeilen 7–12: Dies ist ein normales [select]-Element, das jedoch die spezifische Klasse [combobox] trägt. Das Attribut [data-style="btn-primary"] verleiht der Komponente ihre blaue Farbe;
- Zeilen 16–21: Ein Skript, das beim Laden der Seite ausgeführt wird;
Die JS-Datei [bs-05.js] sieht wie folgt aus:
...
function afficherAgenda() {
var idMedecin = $('#idMedecin option:selected').val();
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin);
}
function initChoixMedecin() {
// le select des médecins
$('#idMedecin').selectpicker();
// le menu
setMenu([ "lnkAfficherAgenda" ]);
}
- Zeilen 7–12: die Funktion, die beim Laden der Seite ausgeführt wird;
- Zeile 9: Die Anweisung, die das [select]-Element der Seite in eine Bootstrap-Dropdown-Liste umwandelt. [$('#idMedecin')] verweist auf das [select]-Element (Zeile 7 der Ansicht [choixmedecin]) und die JS-Funktion [selectpicker] stammt aus der JS-Datei [bootstrap-select.js];
- Zeile 11: Es wird nur eine der Menüoptionen angezeigt;
- Zeilen 2–5: die JavaScript-Funktion, die ausgeführt wird, wenn auf die Menüoption [Agenda] geklickt wird;
- Zeile 3: Wir rufen den Wert der ausgewählten Option in der Dropdown-Liste ab: [$('#idMedecin option:selected')] findet zunächst die Komponente [id=idMedecin] und dann innerhalb dieser Komponente die ausgewählte Option. Die Operation [..].val() ruft anschließend den Wert des gefundenen Elements ab, d. h. das [value]-Attribut der ausgewählten Option;
Hier ist ein Beispiel für die Auswahl eines Arztes:
![]() |
8.6.4.7. Beispiel Nr. 6: Ein Kalender
Die Aktion [/bs-06] zeigt die folgende Ansicht [bs-06.xml] an:

Die Auswahl eines Arztes oder eines Datums löst eine JS-Funktion aus, die sowohl den ausgewählten Arzt als auch das ausgewählte Datum anzeigt. Hier ist ein Beispiel:
![]() |
Über die Schaltfläche „Sprachliste“ können Sie den Kalender (und nur den Kalender) auf Englisch umstellen:

Dies ist das komplexeste Beispiel in dieser Reihe. Der Kalender ist eine [bootstrap-datepicker]-Komponente [http://eternicode.github.io/bootstrap-datepicker].
Die Ansicht [bs-06.xml] sieht wie folgt aus:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<!-- local script -->
<script type="text/javascript" src="resources/js/bs-06.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar3"></div>
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron"></div>
<!-- content -->
<div id="content" th:include="choixmedecinjour">
</div>
<!-- info -->
<div class="alert alert-warning">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- Zeile 8: die CSS-Datei für die [bootstrap-datepicker]-Komponente;
- Zeile 16: die JS-Datei für die [bootstrap-datepicker]-Komponente;
- Zeile 17: die JS-Datei zur Verwaltung eines französischen Kalenders. Standardmäßig ist sie auf Englisch;
- Zeile 15: die JS-Datei für eine Bibliothek namens [moment], die Zugriff auf zahlreiche Zeitberechnungsfunktionen bietet [http://momentjs.com/];
- Zeile 28: die Kalenderansicht;
Die Ansicht [choixmedecinjour.xml] sieht wie folgt aus:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2>Médecin</h2>
<select id="idMedecin" class="combobox" data-style="btn-primary">
<option value="1">Mme Marie Pélissier</option>
<option value="2">Mr Jean Pardon</option>
<option value="3">Mlle Jeanne Jirou</option>
<option value="4">Mr Paul Macou</option>
</select>
</div>
<div class="col-md-3">
<h2>Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- local script -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initChoixMedecinJour();
/*]]>*/
</script>
</section>
- Zeilen 17–23: der Kalender;
- Zeile 18: Die Klasse [btn-primary] verleiht ihm seine blaue Farbe;
- Zeile 18: Das Attribut [disabled="true"] verhindert die manuelle Datumsangabe. Sie müssen den Kalender verwenden;
- Zeile 16: Der Kalender wurde in einem Abschnitt [id="calendar_container"] platziert. Um die Sprache des Kalenders zu ändern, müssen Sie ihn löschen und anschließend neu generieren. Löschen Sie also den Inhalt der Komponente [id="calendar_container"] und platzieren Sie dort den neuen Kalender mit der neuen Sprache;
- Zeilen 28–33: der Code zur Initialisierung der Seite;
Die JS-Datei [bs-06.js] lautet wie folgt:
...
var calendar_infos = {};
function initChoixMedecinJour() {
// calendrier
var calendar_container = $("#calendar_container");
calendar_infos = {
"container" : calendar_container,
"html" : calendar_container.html(),
"today" : moment().format('YYYY-MM-DD'),
"langue" : "fr"
}
// création calendrier
updateCalendar();
// le select des médecins
$('#idMedecin').selectpicker();
$('#idMedecin').change(function(e) {
afficherAgenda();
})
// le menu
setMenu([]);
}
- Zeile 2: Der Kalender wird von mehreren JS-Funktionen verwaltet. Die Variable [calendar_infos] sammelt Informationen über den Kalender. Sie ist global, damit die verschiedenen Funktionen darauf zugreifen können;
- Zeile 6: Wir identifizieren den Kalender-Container;
- Zeilen 7–12: die für den Kalender gespeicherten Informationen;
- Zeile 8: ein Verweis auf seinen Container,
- Zeile 9: der HTML-Code des Kalenders. Mit diesen beiden Informationen können wir den Kalender entfernen und neu generieren;
- Zeile 10: das heutige Datum im Format [yyyy-mm-dd],
- Zeile 11: die Sprache des Kalenders;
- Zeile 14: Erstellung des Kalenders;
- Zeile 16: das Dropdown-Menü „Ärzte“;
- Zeilen 17–19: Jedes Mal, wenn sich der in diesem Dropdown-Menü ausgewählte Wert ändert, wird die Methode [displayCalendar] ausgeführt;
- Zeile 21: kein Menü in der Navigationsleiste;
Die Funktion [updateCalendar] sieht wie folgt aus:
function updateCalendar(renew) {
if (renew) {
// régénération du calendrier actuel
calendar_infos.container.html(calendar_infos.html);
}
// initialisation du calendrier
var calendar = $("#calendar");
var settings = {
format : "yyyy-mm-dd",
startDate : calendar_infos.today,
language : calendar_infos.langue,
};
calendar.datepicker(settings);
// sélection de la date courante
if (calendar_infos.date) {
calendar.datepicker('setDate', calendar_infos.date)
}
// évts
calendar.datepicker().on('hide', function(e) {
// affichage jour sélectionné
displayJour();
});
calendar.datepicker().on('changeDate', function(e) {
// on note la nouvelle date
calendar_infos.date = moment(calendar.datepicker('getDate')).format("YYYY-MM-DD");
// affichage infos agenda
afficherAgenda();
// affichage jour sélectionné
displayJour();
});
// affichage jour sélectionné
displayJour();
}
- Zeile 1: Die Funktion [updateCalendar] akzeptiert einen Parameter, der vorhanden sein kann oder auch nicht. Ist er vorhanden, wird der Kalender (Zeile 4) auf der Grundlage der in [calendar_infos] enthaltenen Informationen neu generiert;
- Zeile 7: Der Kalender wird referenziert;
- Zeilen 8–12: seine Initialisierungsparameter;
- Zeile 9: Das Format der verarbeiteten Datumsangaben [yyyy-mm-dd],
- Zeile 10: das erste Datum, das im Kalender ausgewählt werden kann. Hier das heutige Datum. Frühere Daten können nicht ausgewählt werden;
- Zeile 11: die Sprache des Kalenders. Es gibt zwei: ['en'] und ['fr'];
- Zeile 13: Der Kalender wird konfiguriert;
- Zeilen 15–17: Wenn das Datum aus [calendar_infos] initialisiert wurde, wird dieses Datum als aktuelles Kalenderdatum festgelegt;
- Zeilen 19–22: Jedes Mal, wenn der Kalender geschlossen wird, wird das ausgewählte Datum angezeigt;
- Zeilen 23–30: Bei jeder Datumsänderung im Kalender:
- Zeile 25: Das ausgewählte Datum wird in [calendar_infos] gespeichert,
- Zeile 27: Wir zeigen Informationen zum Kalender an,
- Zeile 29: Der ausgewählte Tag wird angezeigt;
- Zeile 32: Der ausgewählte Tag wird angezeigt, sofern vorhanden;
Die Methode [displayJour], die den ausgewählten Tag anzeigt, lautet wie folgt:
// affiche le jour sélectionné
function displayJour() {
if (calendar_infos.date) {
var displayjour = $("#displayjour");
moment.locale(calendar_infos.langue);
jour = moment(calendar_infos.date).format('LL');
displayjour.val(jour);
}
}
- Zeile 3: Wenn bereits ein Datum ausgewählt wurde (anfangs ist im Kalender kein Datum ausgewählt);
- Zeile 4: Wir suchen die Komponente, in die wir das Datum eingeben werden;
- Zeile 5: Dieses Datum kann auf Englisch oder Französisch geschrieben werden. Wir legen die Sprache der Bibliothek fest [moment];
- Zeile 6: Das ausgewählte Datum wird in der gewählten Sprache und im Langformat angezeigt;
- Zeile 7: Dieses Datum wird angezeigt;
Hier sind zwei Beispiele:
![]() | ![]() |
Wenn sich das Datum oder der Tag ändert, wird die Methode [displayCalendar] ausgeführt:
function afficherAgenda() {
// on affiche médecin et date
var idMedecin = $('#idMedecin option:selected').val();
if (calendar_infos.date) {
showInfo("Vous avez sélectionné le médecin d'id=" + idMedecin + " et le jour " + calendar_infos.date);
}
}
8.6.4.8. Beispiel #7: Eine „responsive“ HTML-Tabelle
Hinweis: „Responsive“ ist ein Begriff, der angibt, dass sich eine Komponente an die Größe des Bildschirms anpassen kann, auf dem sie angezeigt wird. Wir zeigen Ihnen ein Beispiel dafür.
Die Aktion [/bs-07] zeigt die folgende Ansicht [bs-07.xml] an (Vollbild):
![]() |
Die neue Funktion ist die HTML-Tabelle [1]. Diese Tabelle wird von der JS-Bibliothek [footable] verwaltet: [https://github.com/fooplugins/FooTable].
Wenn Sie die Größe des Browserfensters ändern, erhalten Sie Folgendes:
![]() |
- Die HTML-Tabelle hat sich an die Bildschirmgröße angepasst;
- in [1] müssen Sie auf das [+]-Zeichen klicken, um den Link [Buch] zu sehen;
- in [2] sehen Sie, was angezeigt wird, wenn Sie auf das [+]-Zeichen klicken;
Die Ansicht [bs-07.xml] sieht wie folgt aus:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="resources/vendor/footable.js"></script>
<!-- local script -->
<script type="text/javascript" src="resources/js/bs-07.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar3" />
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron" />
<!-- content -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda" />
<!-- info -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- Zeile 10: das CSS für die [footable]-Bibliothek;
- Zeile 19: das JavaScript der [footable]-Bibliothek;
- Zeile 31: die HTML-Tabelle für einen Kalender;
Die Ansicht [agenda.xml] sieht wie folgt aus:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div class="row alert alert-danger">
<div class="col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span>Créneau horaire</span>
</th>
<th>
<span>Client</span>
</th>
<th data-hide="phone">
<span>Action</span>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class='status-metro status-active'>
9h00-9h20
</span>
</td>
<td>
<span></span>
</td>
<td>
<a href="javascript:reserver(14)" class="status-metro status-active">
Réserver
</a>
</td>
</tr>
<tr>
<td>
<span class='status-metro status-suspended'>
9h20-9h40
</span>
</td>
<td>
<span>Mme Paule MARTIN</span>
</td>
<td>
<a href="javascript:supprimer(17)" class="status-metro status-suspended">
Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initAgenda();
/*]]>*/
</script>
</body>
</html>
- Zeile 4: Platziert die Tabelle in einer Zeile [row] und einem farbigen Kasten [alert alert-danger];
- Zeile 5: Die Tabelle erstreckt sich über 6 Spalten [col-md-6];
- Zeile 6: Die HTML-Tabelle wird von Bootstrap formatiert [class='table'];
- Zeile 9: Das Attribut [data-toggle] gibt die Spalte an, die das Symbol [+/-] enthält, mit dem die Zeile ein- und ausgeblendet wird;
- Zeile 15: Das Attribut [data-hide='phone'] legt fest, dass die Spalte ausgeblendet werden soll, wenn der Bildschirm die Größe eines Smartphone-Bildschirms hat. Der Wert 'tablet' kann ebenfalls verwendet werden;
- Zeile 31: Dem Link [Book] ist eine JS-Funktion zugeordnet;
- Zeile 46: Dem Link [Delete] ist eine JS-Funktion zugeordnet;
- Zeilen 56–61: Initialisierung der Seite;
Einige der oben verwendeten CSS-Klassen stammen aus der CSS-Datei [bootstrapDemo.css]:
@CHARSET "UTF-8";
#creneaux th {
text-align: center;
}
#creneaux td {
text-align: center;
font-weight: bold;
}
.status-metro {
display: inline-block;
padding: 2px 5px;
color:#fff;
}
.status-metro.status-active {
background: #43c83c;
}
.status-metro.status-suspended {
background: #fa3031;
}
Die [status-*]-Stile stammen aus einem Beispiel für die Verwendung der [footable]-Tabelle, das auf der Website der Bibliothek zu finden ist.
In der JS-Datei [bs-07.js] wird die Seite wie folgt initialisiert:
function initAgenda() {
// time slot table
$("#creneaux").footable();
}
Das war's. [$("#creneaux")] bezieht sich auf die HTML-Tabelle, die wir responsiv gestalten möchten. Außerdem sind hier die JS-Funktionen, die mit den beiden Links [Reservieren] und [Löschen] verknüpft sind:
function reserver(idCreneau) {
showInfo("Réservation du créneau n° " + idCreneau);
}
function supprimer(idRv) {
showInfo("Suppression du rv n° " + idRv);
}
8.6.4.9. Beispiel #8: Ein modales Fenster
Die Aktion [/bs-08] zeigt die folgende Ansicht [bs-08.xml] an:

Während zuvor durch Klicken auf den Link [Buchung] Informationen im Infofenster angezeigt wurden, zeigen wir hier ein modales Fenster an, um einen Kunden für den Termin auszuwählen:

Verwendet wird die Komponente [bootstrap-modal] [https://github.com/jschr/bootstrap-modal/].
Die Ansicht [bs-08.xml] sieht wie folgt aus:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width" />
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="resources/css/bootstrap-3.1.1-min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrap-select.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/datepicker3.css" />
<link rel="stylesheet" type="text/css" href="resources/css/footable.core.min.css" />
<link rel="stylesheet" type="text/css" href="resources/css/bootstrapDemo.css" />
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="resources/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="resources/vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="resources/vendor/bootstrap-modal.js"></script>
<script type="text/javascript" src="resources/vendor/footable.js"></script>
<!-- local script -->
<script type="text/javascript" src="resources/js/bs-08.js"></script>
</head>
<body id="body">
<div class="container">
<!-- navigation bar -->
<div th:include="navbar3" />
<!-- Bootstrap Jumbotron -->
<div th:include="jumbotron" />
<!-- content -->
<div id="content" th:include="choixmedecinjour" />
<div id="agenda" th:include="agenda-modal" />
<div th:include="resa" />
<!-- info -->
<div class="alert alert-success">
<span id="info">Ici, un texte d'information</span>
</div>
</div>
</body>
</html>
- Zeile 19: die für Modal-Boxen erforderliche JS-Datei;
- Zeile 32: Die Ansicht [agenda-modal] ist bis auf ein Detail identisch mit der Ansicht [agenda]: die JS-Funktion, die den Link [Book] verarbeitet:
<a href="javascript:showDialogResa(14)" class="status-metro status-active">Réserver</a>
Die Funktion [showDialogResa] ist für die Anzeige des Modalfensters zur Auswahl eines Kunden zuständig;
- Zeile 33: Die Ansicht [resa.xml] ist das modale Fenster zur Auswahl eines Kunden:
<!DOCTYPE HTML>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">Modal title</h4> -->
</div>
<div class="modal-body">
<div class="alert alert-info">
<h3>
<span>Prise de rendez-vous</span>
</h3>
</div>
<div class="row">
<div class="col-md-3">
<h2>Clients</h2>
<select id="idClient" class="combobox" data-style="btn-primary">
<option value="1">Mme Marguerite Planton</option>
<option value="2">Mr Maxime Franck</option>
<option value="3">Mlle Elisabeth Oron</option>
<option value="4">Mr Gaëtan Calot</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()">Annuler</button>
<button type="button" class="btn btn-primary" onclick="javascript:validateResa()">Valider</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initResa();
/*]]>*/
</script>
</section>
- Zeilen 3–37: das Modalfenster;
- Zeilen 13–30: der Inhalt dieses Fensters (was angezeigt wird);
- Zeilen 31–34: die Schaltflächen des Dialogfelds;
- Zeile 32: eine Schaltfläche [Abbrechen], die von der JS-Funktion [cancelDialogResa] verarbeitet wird;
- Zeile 33: eine Schaltfläche [Bestätigen], die von der JS-Funktion [validateResa] verarbeitet wird;
- Zeilen 39–44: das Initialisierungsskript für das Modalfenster;
Dies führt zu folgender Ansicht:
![]() |
Beachten Sie, dass das modale Fenster standardmäßig nicht angezeigt wird. Deshalb ist es beim Start der Anwendung nicht sichtbar, obwohl der entsprechende HTML-Code im Dokument vorhanden ist.
Die JS-Datei [bs-08.js] lautet wie folgt:
var idCreneau;
var idClient;
var resa;
function showDialogResa(idCreneau) {
// on mémorise l'id du créneau
this.idCreneau = idCreneau;
// on affiche le dialogue de réservation
var resa = $("#resa");
resa.modal('show');
// log
showInfo("Réservation du créneau n° " + idCreneau);
}
function cancelDialogResa() {
// on cache la boîte de dialogue
resa.modal('hide');
}
// validation résa
function validateResa() {
// on récupère les infos
var idClient = $('#idClient option:selected').val();
// on cache la boîte de dialogue
resa.modal('hide');
// infos
showInfo("Réservation du créneau n° " + idCreneau + " pour le client n° " + idClient)
}
function initResa() {
// le select des clients
$('#idClient').selectpicker();
// boîte modale
resa = $("#resa");
resa.modal({});
}
- Zeilen 30–36: die Initialisierungsfunktion für das Modalfenster;
- Zeile 32: Die Modalbox enthält eine Dropdown-Liste, die initialisiert werden muss;
- Zeilen 34–35: Initialisierung des Modals selbst;
- Zeilen 5–13: die dem Link [Book] zugewiesene JS-Funktion;
- Zeile 7: Der Funktionsparameter wird in der globalen Variablen aus Zeile 1 gespeichert;
- Zeilen 9–10: Das Modalfenster wird sichtbar gemacht;
- Zeile 12: Informationen werden in der Infobox protokolliert;
- Zeilen 15–18: Behandlung der Schaltfläche [Abbrechen]. Wir blenden das Modalfenster einfach aus (Zeile 17);
- Zeilen 21–31: die JS-Funktion, die mit der Schaltfläche [Submit] verknüpft ist;
- Zeile 23: Abrufen des Attributs [value] des ausgewählten Clients;
- Zeile 25: Ausblenden des Dialogfelds;
- Zeile 27: Wir protokollieren die beiden Informationen: die reservierte Slot-Nummer und den Client, für den sie bestimmt ist;
8.6.5. Schritt 2: Schreiben der Ansichten
Wir beschreiben nun die vom [Web1]-Server zurückgegebenen Ansichten sowie deren Vorlagen.
![]() |
8.6.5.1. Die Ansicht [navbar-start]
Sie zeigt die Navigationsleiste auf der Startseite an:

Der Code für [navbar-start.xml] lautet wie folgt:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- identification form -->
<div class="navbar-form navbar-right" role="form" id="formulaire">
<div class="form-group">
<input type="text" th:placeholder="#{service.url}" class="form-control" id="urlService" />
</div>
<div class="form-group">
<input type="text" th:placeholder="#{username}" class="form-control" id="login" />
</div>
<div class="form-group">
<input type="password" th:placeholder="#{password}" class="form-control" id="passwd" />
</div>
<button type="button" class="btn btn-success" th:text="#{login}" onclick="javascript:connecter()">Sign in</button>
<!-- languages -->
<div class="btn-group">
<button type="button" class="btn btn-danger" th:text="#{langues}">Action</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
</li>
<li>
<a href="javascript:setLang('en')" th:text="#{langues.en}" />
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initNavBarStart();
/*]]>*/
</script>
</section>
Diese Ansicht hat keine Vorlage. Sie verfügt über die folgenden Ereignisbehandler:
Ereignis | Behandler |
Klick auf die Anmeldeschaltfläche | |
Klicken Sie auf den Link [Französisch] | |
Klicken Sie auf den Link [English] |
8.6.5.2. Die Ansicht [jumbotron]
Dies ist die Ansicht, die unterhalb der Navigationsleiste [navbar-start] auf der Startseite angezeigt wird:

Der Code [jumbotron.xml] lautet wie folgt:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<!-- Bootstrap Jumbotron -->
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="resources/images/caduceus.jpg" alt="RvMedecins" />
</div>
<div class="col-md-10">
<h1 th:utext="#{application.header}" />
</div>
</div>
</div>
</section>
Die Ansicht [jumbotron] enthält keine Vorlage und keine Ereignisse.
8.6.5.3. Die Ansicht [login]
Dies ist die Ansicht, die unterhalb des Jumbotrons auf der Startseite angezeigt wird:

Ihr Code [login.xml] lautet wie folgt:
<!DOCTYPE html>
<section xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info" th:text="#{identification}">Identification
</div>
</section>
Die Ansicht verfügt weder über eine Vorlage noch über Ereignisse.
8.6.5.4. Die Ansicht [navbar-run]
Dies ist die Navigationsleiste, die nach erfolgreicher Anmeldung angezeigt wird:

Ihr Code [navbar-run.xml] lautet wie folgt:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="collapse navbar-collapse">
<img id="loading" src="resources/images/loading.gif" alt="waiting..." style="display: none" />
<!-- right-hand buttons -->
<form class="navbar-form navbar-right" role="form">
<!-- disconnect -->
<button type="button" class="btn btn-success" th:text="#{options.deconnecter}" onclick="javascript:deconnecter()">Déconnexion</button>
<!-- languages -->
<div class="btn-group">
<button type="button" class="btn btn-danger" th:text="#{langues}">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span>
<span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<a href="javascript:setLang('fr')" th:text="#{langues.fr}" />
</li>
<li>
<a href="javascript:setLang('en')" th:text="#{langues.en}" />
</li>
</ul>
</div>
</form>
</div>
</div>
</div>
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initNavBarRun();
/*]]>*/
</script>
</section>
Diese Ansicht hat keine Vorlage. Sie verfügt über die folgenden Ereignisbehandler:
Ereignis | Behandler |
Klick auf die Schaltfläche „Abmelden“ | |
Klicken Sie auf den Link [Französisch] | |
Klicken Sie auf den Link [English] |
8.6.5.5. Die Ansicht [home]
Dies ist die Ansicht, die direkt unterhalb der Navigationsleiste [navbar-run] angezeigt wird:

Der Code [home.html] lautet wie folgt:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-info" th:text="#{choixmedecinjour.title}">Veuillez choisir un médecin et une date</div>
<div class="row">
<div class="col-md-3">
<h2 th:text="#{rv.medecin}">Médecin</h2>
<select name="idMedecin" id="idMedecin" class="combobox" data-style="btn-primary">
<option th:each="medecinItem : ${rdvmedecins.medecinItems}" th:text="${medecinItem.texte}" th:value="${medecinItem.id}"/>
</select>
</div>
<div class="col-md-3">
<h2 th:text="#{rv.jour}">Date</h2>
<section id="calendar_container">
<div id="calendar" class="input-group date">
<input id="displayjour" type="text" class="form-control btn-primary" disabled="true">
<span class="input-group-addon">
<i class="glyphicon glyphicon-th"></i>
</span>
</input>
</div>
</section>
</div>
</div>
<!-- agenda -->
<div id="agenda"></div>
<!-- local script -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initChoixMedecinJour();
/*]]>*/
</script>
</html>
Die Vorlage sieht wie folgt aus:
- [rdvmedecins.medecinItems] (Zeile 8): die Liste der Ärzte;
In ihrer aktuellen Form scheint die Ansicht keine Ereignisbehandler zu haben. Tatsächlich sind diese in der Funktion [initChoixMedecinJour] definiert. Diese Funktion wurde in Abschnitt 8.6.4.7 auf Seite 466 und genauer auf Seite 469 vorgestellt. Sie enthält die folgenden Ereignisbehandler:
event | Handler |
Arzt-Auswahl | |
Datum auswählen |
8.6.5.6. Die [Kalender]-Ansicht
Die [Agenda]-Ansicht zeigt einen Tag aus dem Kalender eines Arztes an:

Der Code [agenda.xml] lautet wie folgt:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h3 class="alert alert-info" th:text="${agenda.titre}">Agenda de Mme Pélissier le 13/10/2014</h3>
<h4 class="alert alert-danger" th:if="${agenda.creneaux.length}==0" th:text="#{agenda.medecinsanscreneaux}">Ce médecin n'a pas encore de créneaux
de consultation</h4>
<th:block th:if="${agenda.creneaux.length}!=0">
<div class="row tab-content alert alert-warning">
<div class="tab-pane active col-md-6">
<table id="creneaux" class="table">
<thead>
<tr>
<th data-toggle="true">
<span th:text="#{agenda.creneauhoraire}">Créneau horaire</span>
</th>
<th>
<span th:text="#{agenda.client}">Client</span>
</th>
<th data-hide="phone">
<span th:text="#{agenda.action}">Action</span>
</th>
</tr>
</thead>
<tbody>
<tr th:each="creneau,iter : ${agenda.creneaux}">
<td>
<span th:if="${creneau.action}==1" class="status-metro status-active" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
<span th:if="${creneau.action}==2" class="status-metro status-suspended" th:text="${creneau.creneauHoraire}">Créneau horaire</span>
</td>
<td>
<span th:text="${creneau.client}">Client</span>
</td>
<td>
<a th:if="${creneau.action}==1" th:href="@{'javascript:reserverCreneau('+${creneau.id}+')'}" th:text="${creneau.commande}"
class="status-metro status-active">Réserver
</a>
<a th:if="${creneau.action}==2" th:href="@{'javascript:supprimerRv('+${creneau.idRv}+')'}" th:text="${creneau.commande}"
class="status-metro status-suspended">Supprimer
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- reservation -->
<section th:include="resa" />
</th:block>
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initAgenda();
/*]]>*/
</script>
</body>
</html>
Die Vorlage für diese Ansicht enthält nur ein Element:
- [agenda] (Zeile 4): eine etwas komplexe Vorlage, die speziell für die Anzeige des Kalenders entwickelt wurde;
Sie verfügt über die folgenden Ereignisbehandler:
event | handler |
Klick auf die Schaltfläche [Löschen] | |
Klicken Sie auf den Link [Reservieren] |
Die Ansicht [resa] in Zeile 47 ist die Ansicht, die angezeigt wird, wenn der Benutzer auf einen [Reservieren]-Link klickt:

Der Code [resa.xml] lautet wie folgt:
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div id="resa" class="modal fade">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">
</span>
</button>
<!-- <h4 class="modal-title">Modal title</h4> -->
</div>
<div class="modal-body">
<div class="alert alert-info">
<h3>
<span th:text="#{resa.titre}">Prise de rendez-vous</span>
</h3>
</div>
<div class="row">
<div class="col-md-3">
<h2 th:text="#{resa.client}">Client</h2>
<select name="idClient" id="idClient" class="combobox" data-style="btn-primary">
<option th:each="clientItem : ${clientItems}" th:text="${clientItem.texte}" th:value="${clientItem.id}" />
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-warning" onclick="javascript:cancelDialogResa()" th:text="#{resa.annuler}">Annuler</button>
<button type="button" class="btn btn-primary" onclick="javascript:validerRv()" th:text="#{resa.valider}">Valider</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- init page -->
<script th:inline="javascript">
/*<![CDATA[*/
// on initialise la page
initResa();
/*]]>*/
</script>
</body>
</html>
Das Modell enthält nur ein Element:
- [clientItems] (Zeile 24): die Liste der Clients;
Es verfügt über die folgenden Ereignisbehandler:
event | handler |
Klick auf die Schaltfläche [Abbrechen] | |
Klicken Sie auf die Schaltfläche [Bestätigen] |
8.6.5.7. Die Ansicht [Fehler]
Dies ist die Ansicht, die angezeigt wird, wenn die vom Benutzer angeforderte Aktion nicht ausgeführt werden konnte:

Der Code für [errors.xml] lautet wie folgt:
<!DOCTYPE HTML>
<section xmlns:th="http://www.thymeleaf.org">
<div class="alert alert-danger">
<h4>
<span th:text="#{erreurs.titre}">Les erreurs suivantes se sont produites :</span>
</h4>
<ul>
<li th:each="message : ${erreurs}" th:text="${message}" />
</ul>
</div>
</section>
Die Vorlage enthält nur ein Element:
- [errors] (Zeile 8): die Liste der anzuzeigenden Fehler;
Die Ansicht verfügt über keinen Ereignis-Handler.
8.6.5.8. Zusammenfassung
Die folgende Tabelle listet die Ansichten und ihre Modelle auf:
Ansicht | Modell | Ereignisbehandler |
navbar-start | ||
Jumbotron | ||
Anmelden | ||
navbar-run | ||
Startseite | ||
Kalender | ||
buchen | ||
Fehler |
8.6.6. Schritt 3: Schreiben der Aktionen
Kehren wir zur Architektur des [Web1]-Webdienstes zurück:
![]() |
Wir werden uns nun ansehen, welche URLs von [Web1] bereitgestellt werden und wie sie implementiert sind:
8.6.6.1. Die vom [Web1]-Dienst bereitgestellten URLs
Diese lauten wie folgt:
- eine URL für jede der vorherigen Ansichten oder eine Kombination daraus;
- eine URL zum Hinzufügen eines Termins;
- eine URL zum Löschen eines Termins;
Sie alle geben eine Antwort vom Typ [Response] wie folgt zurück:
public class Reponse {
// ----------------- properties
// operation status
private int status;
// the navigation bar
private String navbar;
// the jumbotron
private String jumbotron;
// the body of the page
private String content;
// the diary
private String agenda;
...
}
- Zeile 5: ein Antwortstatus: 1 (OK), 2 (Fehler);
- Zeile 7: der HTML-Stream für die Ansichten [navbar-start] oder [navbar-run], je nach Bedarf;
- Zeile 9: der HTML-Feed für die Ansicht [jumbotron];
- Zeile 13: der HTML-Feed für die Ansicht [agenda];
- Zeile 9: der HTML-Feed für die Ansichten [home], [errors] oder [login], je nach Bedarf;
Die offengelegten URLs lauten wie folgt
platziert die Ansicht [navbar-start] in [Response.navbar] | |
platziert die Ansicht [navbar-run] in [Response.navbar] | |
platziert die Ansicht [home] in [Response.content] | |
platziert die Ansicht [jumbotron] in [Response.jumbotron] | |
platziert die Ansicht [agenda] in [Response.agenda] | |
platziert die Ansicht [login] in [Response.content] | |
| |
platziert die Ansicht [navbar-run] in [Response.navbar], die Ansicht [jumbotron] in [Response.jumbotron], die Ansicht [home] in [Response.content] und die Ansicht [calendar] in [Response.calendar] | |
fügt den ausgewählten Termin hinzu und platziert die neue Agenda in [Response.agenda] | |
löscht den ausgewählten Termin und fügt den neuen Kalender in [Response.calendar] ein |
8.6.6.2. Das [ApplicationModel]-Singleton
![]() |
Die Klasse [ApplicationModel] wird als einzelne Instanz instanziiert und in den Anwendungscontroller injiziert. Der Code lautet wie folgt:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
....
}
- Zeile 6: [ApplicationModel] ist eine Spring-Komponente;
- Zeile 7: die die Schnittstelle der [DAO]-Schicht implementiert. Wir tun dies, damit die Aktionen nichts über die [DAO]-Schicht wissen müssen, sondern nur über das [ApplicationModel]-Singleton. Die Architektur von [Web1] sieht dann wie folgt aus:
![]() |
Kehren wir zum Code der Klasse [ApplicationModel] zurück:
package rdvmedecins.springthymeleaf.server.models;
import java.util.ArrayList;
...
@Component
public class ApplicationModel implements IDao {
// the [DAO] layer
@Autowired
private IDao dao;
// configuration
@Autowired
private AppConfig appConfig;
// data from the [DAO] layer
private List<ClientItem> clientItems;
private List<MedecinItem> medecinItems;
// configuration data
private String userInit;
private String mdpUserInit;
private boolean corsAllowed;
// exception
private RdvMedecinsException rdvMedecinsException;
// manufacturer
public ApplicationModel() {
}
@PostConstruct
public void init() {
// config
userInit = appConfig.getUSER_INIT();
mdpUserInit = appConfig.getMDP_USER_INIT();
dao.setTimeout(appConfig.getTIMEOUT());
dao.setUrlServiceWebJson(appConfig.getWEBJSON_ROOT());
corsAllowed = appConfig.isCORS_ALLOWED();
// caching of physician and customer drop-down lists
List<Medecin> medecins = null;
List<Client> clients = null;
try {
medecins = dao.getAllMedecins(new User(userInit, mdpUserInit));
clients = dao.getAllClients(new User(userInit, mdpUserInit));
} catch (RdvMedecinsException ex) {
rdvMedecinsException = ex;
}
if (rdvMedecinsException == null) {
// create drop-down list items
medecinItems = new ArrayList<MedecinItem>();
for (Medecin médecin : medecins) {
medecinItems.add(new MedecinItem(médecin));
}
clientItems = new ArrayList<ClientItem>();
for (Client client : clients) {
clientItems.add(new ClientItem(client));
}
}
}
// getters and setters
...
// interface implementation [IDao]
@Override
public void setUrlServiceWebJson(String url) {
dao.setUrlServiceWebJson(url);
}
@Override
public void setTimeout(int timeout) {
dao.setTimeout(timeout);
}
@Override
public Rv ajouterRv(User user, String jour, long idCreneau, long idClient) {
return dao.ajouterRv(user, jour, idCreneau, idClient);
}
...
}
- Zeile 11: Einfügen des Verweises auf die Implementierung der [DAO]-Schicht. Dieser Verweis wird dann zur Implementierung der [IDao]-Schnittstelle verwendet (Zeilen 64–80);
- Zeile 14: Einfügen der Anwendungskonfiguration;
- Zeilen 33–37: Verwendung dieser Konfiguration zur Konfiguration verschiedener Elemente der Anwendungsarchitektur;
- Zeilen 38–46: Wir speichern die Informationen, die die Dropdown-Listen für Ärzte und Kunden füllen, im Cache. Wir gehen daher davon aus, dass die Anwendung neu gestartet werden muss, wenn sich ein Arzt oder ein Kunde ändert. Damit soll gezeigt werden, dass ein Spring-Singleton als Cache für die Webanwendung dienen kann;
Die Klassen [MedecinItem] und [ClientItem] leiten sich beide von der folgenden Klasse [PersonneItem] ab:
package rdvmedecins.springthymeleaf.server.models;
import rdvmedecins.client.entities.Personne;
public class PersonneItem {
// element of a list
private Long id;
private String texte;
// manufacturer
public PersonneItem() {
}
public PersonneItem(Personne personne) {
id = personne.getId();
texte = String.format("%s %s %s", personne.getTitre(), personne.getPrenom(), personne.getNom());
}
// getters and setters
...
}
- Zeile 8: Das Feld [id] ist der Wert des Attributs [value] einer Dropdown-Listenoption;
- Zeile 9: Das Feld [text] enthält den Text, der bei einer Dropdown-Listenoption angezeigt wird;
8.6.6.3. Die Klasse [BaseController]
![]() |
Die Klasse [BaseController] ist die Oberklasse der Controller [RdvMedecinsController] und [RdvMedecinsCorsController]. Die Erstellung dieser Oberklasse war nicht zwingend erforderlich. Wir haben hier Hilfsmethoden aus der Klasse [RdvMedecinsController] zusammengefasst, von denen bis auf eine keine unverzichtbar ist. Sie lassen sich in drei Gruppen einteilen:
- Hilfsmethoden;
- Methoden, die Ansichten zusammen mit ihren Modellen rendern;
- die Methode zur Initialisierung einer Aktion
| zwei Hilfsmethoden, die eine Liste von Fehlermeldungen bereitstellen. Wir sind ihnen bereits begegnet und haben sie verwendet; |
| gibt die Ansicht [home] ohne Vorlage zurück |
| gibt die Ansicht [agenda] und deren Vorlage zurück |
| gibt die Ansicht [login] ohne Modell zurück |
| gibt die Antwort an den Client zurück, wenn die vom angeforderte Aktion mit einem Fehler endete |
| die Initialisierungsmethode für alle Aktionen des [RdvMedecinsController]-Controllers |
Betrachten wir zwei dieser Methoden.
Die Methode [getPartialViewAgenda] rendert die am komplexesten zu erzeugende Ansicht, nämlich die des Kalenders. Ihr Code lautet wie folgt:
// feed [agenda]
protected String getPartialViewAgenda(ActionContext actionContext, AgendaMedecinJour agenda, Locale locale) {
// contexts
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
// build the [agenda] page template
ViewModelAgenda modelAgenda = setModelforAgenda(agenda, springContext, locale);
// the agenda with its model
thymeleafContext.setVariable("agenda", modelAgenda);
thymeleafContext.setVariable("clientItems", application.getClientItems());
return engine.process("agenda", thymeleafContext);
}
- Zeilen 9–10: die beiden Elemente des Kalendermodells:
- Zeile 9: Der angezeigte Kalender.
- Zeile 10: die Liste der Kunden, die angezeigt wird, wenn der Benutzer einen Termin vereinbart;
Die Methode [setModelforAgenda] in Zeile 7 lautet wie folgt:
// agenda] page template
private ViewModelAgenda setModelforAgenda(AgendaMedecinJour agenda, WebApplicationContext springContext, Locale locale) {
// page title
String dateFormat = springContext.getMessage("date.format", null, locale);
Medecin médecin = agenda.getMedecin();
String titre = springContext.getMessage("agenda.titre", new String[] { médecin.getTitre(), médecin.getPrenom(),
médecin.getNom(), new SimpleDateFormat(dateFormat).format(agenda.getJour()) }, locale);
// reservation slots
ViewModelCreneau[] modelCréneaux = new ViewModelCreneau[agenda.getCreneauxMedecinJour().length];
int i = 0;
for (CreneauMedecinJour creneauMedecinJour : agenda.getCreneauxMedecinJour()) {
// doctor's slot
Creneau créneau = creneauMedecinJour.getCreneau();
ViewModelCreneau modelCréneau = new ViewModelCreneau();
modelCréneaux[i] = modelCréneau;
// id
modelCréneau.setId(créneau.getId());
// time slot
modelCréneau.setCreneauHoraire(String.format("%02dh%02d-%02dh%02d", créneau.getHdebut(), créneau.getMdebut(),
créneau.getHfin(), créneau.getMfin()));
Rv rv = creneauMedecinJour.getRv();
// customer and order
String commande;
if (rv == null) {
modelCréneau.setClient("");
commande = springContext.getMessage("agenda.reserver", null, locale);
modelCréneau.setCommande(commande);
modelCréneau.setAction(ViewModelCreneau.ACTION_RESERVER);
} else {
Client client = rv.getClient();
modelCréneau.setClient(String.format("%s %s %s", client.getTitre(), client.getPrenom(), client.getNom()));
commande = springContext.getMessage("agenda.supprimer", null, locale);
modelCréneau.setCommande(commande);
modelCréneau.setIdRv(rv.getId());
modelCréneau.setAction(ViewModelCreneau.ACTION_SUPPRIMER);
}
// next slot
i++;
}
// we render the agenda model
ViewModelAgenda modelAgenda = new ViewModelAgenda();
modelAgenda.setTitre(titre);
modelAgenda.setCreneaux(modelCréneaux);
return modelAgenda;
}
- Zeile 6: Die Agenda hat einen Titel:

oder:

Wir sehen, dass das Datumsformat von der Sprache abhängt. Wir beziehen dieses Format aus den Nachrichtendateien (Zeile 4).
- Zeilen 11–40: Für jeden Zeitabschnitt müssen wir die Ansicht anzeigen:
![]()
oder die Ansicht:
![]()
- Zeilen 19–20: Anzeige des Zeitfensters;
- Zeilen 25–28: der Fall, in dem der Zeitblock verfügbar ist. In diesem Fall muss die Schaltfläche [Reservieren] angezeigt werden;
- Zeilen 31–36: der Fall, in dem der Zeitblock belegt ist. In diesem Fall müssen sowohl der Kunde als auch die Schaltfläche [Löschen] angezeigt werden;
Die andere Methode, auf die wir näher eingehen werden, ist die Methode [getActionContext]. Sie wird zu Beginn jeder Aktion im [RdvMedecinsController] aufgerufen. Ihre Signatur lautet wie folgt:
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController)
Sie gibt den folgenden Typ [ActionContext] zurück:
public class ActionContext {
// data
private WebContext thymeleafContext;
private WebApplicationContext springContext;
private Locale locale;
private List<String> erreurs;
...
}
- Zeile 4: der Thymeleaf-Kontext der Aktion;
- Zeile 5: der Spring-Kontext der Aktion;
- Zeile 6: die Locale der Aktion;
- Zeile 7: eine Liste möglicher Fehlermeldungen;
Die Parameter lauten wie folgt:
- [lang]: die für die Aktion angeforderte Sprache, „en“ oder „fr“;
- [origin]: der HTTP-Header [origin] im Falle einer domänenübergreifenden Anfrage;
- [request]: die derzeit verarbeitete HTTP-Anfrage, die seit einiger Zeit als Aktion bezeichnet wird;
- [response]: die Antwort, die als Reaktion auf diese Anfrage gesendet wird;
- [result]: Jede Aktion von [RdvMedecinsController] erhält einen übermittelten Wert, dessen Gültigkeit geprüft wird. [result] ist das Ergebnis dieser Prüfung;
- [rdvMedecinsController]: der Controller, der die Aktionen enthält;
Die Methode [getActionContext] ist wie folgt implementiert:
// context of an action
protected ActionContext getActionContext(String lang, String origin, HttpServletRequest request,HttpServletResponse response, BindingResult result, RdvMedecinsCorsController rdvMedecinsCorsController) {
// language?
if (lang == null) {
lang = "fr";
}
// local
Locale locale = null;
if (lang.trim().toLowerCase().equals("fr")) {
// french
locale = new Locale("fr", "FR");
} else {
// everything else in English
locale = new Locale("en", "US");
}
// headers CORS
rdvMedecinsCorsController.sendOptions(origin, response);
// ActionContext
ActionContext actionContext = new ActionContext(new WebContext(request, response, request.getServletContext(),locale), WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()), locale, null);
// initialization errors
RdvMedecinsException e = application.getRdvMedecinsException();
if (e != null) {
actionContext.setErreurs(e.getMessages());
return actionContext;
}
// POST errors?
if (result != null && result.hasErrors()) {
actionContext.setErreurs(getErreursForModel(result, locale, actionContext.getSpringContext()));
return actionContext;
}
// no errors
return actionContext;
}
- Zeilen 3–15: Basierend auf dem Parameter [lang] legen wir die Sprache der Aktion fest;
- Zeile 17: Wir senden die für domänenübergreifende Anfragen erforderlichen HTTP-Header. Wir werden hier nicht ins Detail gehen. Die verwendete Technik ist die in Abschnitt 8.4.14 beschriebene;
- Zeile 19: fehlerfreie Erstellung eines [ActionContext]-Objekts;
- Zeile 21: In Abschnitt 8.6.6.2 haben wir gesehen, dass das [ApplicationModel]-Singleton auf die Datenbank zugegriffen hat, um sowohl Kunden als auch Ärzte abzurufen. Dieser Zugriff kann fehlschlagen. In diesem Fall protokollieren wir die aufgetretene Ausnahme. In Zeile 21 rufen wir diese Ausnahme ab;
- Zeilen 22–25: Wenn beim Start der Anwendung eine Ausnahme aufgetreten ist, ist keine Aktion möglich. Wir geben daher für jede Aktion ein [ActionContext]-Objekt zurück, das die Fehlermeldungen der Ausnahme enthält;
- Zeilen 27–20: Wir analysieren den Parameter [result], um festzustellen, ob der übermittelte Wert gültig war oder nicht. War er ungültig, geben wir ein [ActionContext]-Objekt mit den entsprechenden Fehlermeldungen zurück;
- Zeile 32: Fall ohne Fehler;
Wir werden nun die Aktionen des [RdvMedecinsController] untersuchen
8.6.6.4. Die Aktion [/getNavBarStart]
Die Aktion [/getNavBarStart] rendert die Ansicht [navbar-start]. Ihre Signatur lautet wie folgt:
@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
Sie gibt den folgenden [Response]-Typ zurück:
public class Reponse {
// ----------------- properties
// operation status
private int status;
// the navigation bar
private String navbar;
// the jumbotron
private String jumbotron;
// the body of the page
private String content;
// the diary
private String agenda;
...
}
und hat die folgenden Parameter:
- [PostLang postlang]: der nächste gepostete Wert:
public class PostLang {
// data
@NotNull
private String lang;
...
}
Die Klasse [PostLang] ist die Oberklasse aller übermittelten Werte. Der Grund dafür ist, dass der Client immer die Sprache angeben muss, in der die Aktion ausgeführt werden soll.
Die Methode [getNavbarStart] ist wie folgt implementiert:
// navbar-start
@RequestMapping(value = "/getNavbarStart", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarStart(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// returns the [navbar-start] view
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
return reponse;
}
- Zeile 7: Initialisierung der Aktion;
- Zeilen 10–13: Wenn die Initialisierungsmethode der Aktion Fehler gemeldet hat, werden diese in der Antwort an den Client (Zeile 12) mit dem Status 2 gesendet:
- Zeilen 15–18: Sende die Ansicht [navbar-start] mit Status 1:
Im Folgenden werden wir nur auf die neuen Funktionen eingehen.
8.6.6.5. Die Aktion [/getNavbarRun]
Die Aktion [/getNavBarRun] rendert die Ansicht [navbar-run]:
// navbar-run
@RequestMapping(value = "/getNavbarRun", method = RequestMethod.POST)
@ResponseBody
public Reponse getNavbarRun(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// returns the [navbar-run] view
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
return reponse;
}
Die Aktion kann zwei Arten von Antworten zurückgeben:
- die Antwort mit einem Fehler (Zeilen 10–13):
- die Antwort mit der Ansicht [navbar-run]:
8.6.6.6. Die Aktion [/getJumbotron]
Die Aktion [/getJumbotron] gibt die Ansicht [jumbotron] zurück:
// jumbotron
@RequestMapping(value = "/getJumbotron", method = RequestMethod.POST)
@ResponseBody
public Reponse getJumbotron(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// return view [jumbotron]
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
return reponse;
}
Die Aktion kann zwei Arten von Antworten zurückgeben:
- die Antwort mit einem Fehler (Zeilen 10–13):
- Antwort mit der [jumbotron]-Ansicht:
8.6.6.7. Die Aktion [/getLogin]
Die Aktion [/getLogin] rendert die Ansicht [login]:
@RequestMapping(value = "/getLogin", method = RequestMethod.POST)
@ResponseBody
public Reponse getLogin(@Valid @RequestBody PostLang postLang, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postLang.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// returns the [login] view
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setNavbar(engine.process("navbar-start", thymeleafContext));
reponse.setContent(getPartialViewLogin(thymeleafContext));
return reponse;
}
Die Aktion kann zwei Arten von Antworten zurückgeben:
- Die Antwort mit einem Fehler (Zeilen 9–11):
- Die Antwort mit der Ansicht [login]:
8.6.6.8. Die Aktion [/getHome]
Die Aktion [/getHome] gibt die Ansicht [home] zurück. Ihre Signatur lautet wie folgt:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
- Zeile 3: Der übermittelte Wert ist vom Typ [PostUser] wie folgt:
public class PostUser extends PostLang {
// data
@NotNull
private User user;
...
}
- Zeile 1: Die Klasse [PostUser] erweitert die Klasse [PostLang] und enthält daher eine Sprache;
- Zeile 4: Der Benutzer versucht, die Ansicht abzurufen;
Der Implementierungscode lautet wie folgt:
@RequestMapping(value = "/getAccueil", method = RequestMethod.POST)
@ResponseBody
public Reponse getAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request,
HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// the [home] view is protected
try{
// user
User user = postUser.getUser();
// we check identifiers [userName, password]
application.authenticate(user);
}catch(RdvMedecinsException e){
// an error is returned
return getViewErreurs(thymeleafContext, e.getMessages());
}
// returns the [home] view
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
- Zeilen 15–22: Beachten Sie, dass die [Startseite] geschützt ist, sodass der Benutzer authentifiziert sein muss;
Die Aktion kann zwei Arten von Antworten zurückgeben:
- die Fehlerantwort (Zeilen 11 und 21):
- Antwort mit der Ansicht [home] (Zeilen 24–27):
8.6.6.9. Die Aktion [/getNavbarRunJumbotronHome]
Die Aktion [/getNavbarRunJumbotronHome] rendert die Ansichten [navbar-run, jumbotron, home]. Sie hat die folgende Signatur:
@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser post, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- Zeile 3: Der übermittelte Wert ist vom Typ [PostUser];
Die Implementierung der Aktion sieht wie folgt aus:
// navbar+ jumbotron + home
@RequestMapping(value = "/getNavbarRunJumbotronAccueil", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueil(@Valid @RequestBody PostUser postUser, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postUser.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// the [home] view is protected
try {
// user
User user = postUser.getUser();
// we check identifiers [userName, password]
application.authenticate(user);
} catch (RdvMedecinsException e) {
// an error is returned
return getViewErreurs(thymeleafContext, e.getMessages());
}
// we send the answer
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setContent(getPartialViewAccueil(thymeleafContext));
return reponse;
}
Die Aktion kann zwei Arten von Antworten zurückgeben:
- die Antwort mit einem Fehler (Zeilen 13, 23):
- die Antwort mit den Ansichten [navbar-run, jumbotron, home] (Zeilen 26–31):
8.6.6.10. Die Aktion [/getAgenda]
Die Aktion [/getAgenda] rendert die Ansicht [agenda]. Ihre Signatur lautet wie folgt:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- Zeile 3: Der übermittelte Wert ist vom Typ [PostGetAgenda] wie folgt:
public class PostGetAgenda extends PostUser {
// data
@NotNull
private Long idMedecin;
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date jour;
...
}
- Zeile 1: Die Klasse [PostGetAgenda] erweitert die Klasse [PostUser] und enthält daher eine Sprache und einen Benutzer;
- Zeile 5: die ID des Arztes, dessen Kalender gewünscht wird;
- Zeile 8: der gewünschte Tag des Kalenders;
Die Implementierung sieht wie folgt aus:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postGetAgenda.getLang(), origin, request, response, result, rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
WebApplicationContext springContext = actionContext.getSpringContext();
Locale locale = actionContext.getLocale();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// check the validity of the post
if (result != null) {
new PostGetAgendaValidator().validate(postGetAgenda, result);
if (result.hasErrors()) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
...
}
- Bis Zeile 14 entspricht der Code nun dem Standard;
- Zeilen 16–21: Wir führen eine zusätzliche Überprüfung des übermittelten Werts durch. Das Datum muss auf oder nach dem heutigen Datum liegen. Um dies zu überprüfen, verwenden wir einen Validator:
package rdvmedecins.web.validators;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import rdvmedecins.springthymeleaf.server.requests.PostGetAgenda;
import rdvmedecins.springthymeleaf.server.requests.PostValiderRv;
public class PostGetAgendaValidator implements Validator {
public PostGetAgendaValidator() {
}
@Override
public boolean supports(Class<?> classe) {
return PostGetAgenda.class.equals(classe) || PostValiderRv.class.equals(classe);
}
@Override
public void validate(Object post, Errors errors) {
// the day chosen for the appointment
Date jour = null;
if (post instanceof PostGetAgenda) {
jour = ((PostGetAgenda) post).getJour();
} else {
if (post instanceof PostValiderRv) {
jour = ((PostValiderRv) post).getJour();
}
}
// transform dates into yyyy-MM-dd format
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String strJour = sdf.format(jour);
String strToday = sdf.format(new Date());
// the chosen day must not precede today's date
if (strJour.compareTo(strToday) < 0) {
errors.rejectValue("jour", "todayandafter.postChoixMedecinJour", null, null);
}
}
}
- Zeile 19: Der Validator funktioniert für zwei Klassen: [PostGetAgenda] und [PostValiderRv];
Kehren wir zum Code für die Aktion [/getAgenda] zurück:
@RequestMapping(value = "/getAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getAgenda(@RequestBody @Valid PostGetAgenda postGetAgenda, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
...
// action
try {
// doctor's diary
AgendaMedecinJour agenda = application.getAgendaMedecinJour(postGetAgenda.getUser(), postGetAgenda.getIdMedecin(),
new SimpleDateFormat("yyyy-MM-dd").format(postGetAgenda.getJour()));
// answer
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException e1) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, e1.getMessages());
} catch (Exception e2) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- Zeilen 9–10: Anhand der übermittelten Parameter fordern wir den Terminplan des Arztes an;
- Zeilen 12–13: Wir geben den Terminplan zurück:
- Zeilen 17, 21: Wir geben eine Antwort mit Fehlern zurück:
8.6.6.11. Die Aktion [/getNavbarRunJumbotronHomeCalendar]
Die Aktion [/getNavbarRunJumbotronHomeCalendar] rendert die Ansichten [navbar-run, jumbotron, home, calendar]. Ihre Implementierung sieht wie folgt aus:
@RequestMapping(value = "/getNavbarRunJumbotronAccueilAgenda", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse getNavbarRunJumbotronAccueilAgenda(@Valid @RequestBody PostGetAgenda post, BindingResult result,
HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(post.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// agenda
Reponse agenda = getAgenda(post, result, request, response, null);
if (agenda.getStatus() != 1) {
return agenda;
}
// we send the answer
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setNavbar(engine.process("navbar-run", thymeleafContext));
reponse.setJumbotron(engine.process("jumbotron", thymeleafContext));
reponse.setContent(getPartialViewAccueil(thymeleafContext));
reponse.setAgenda(agenda.getAgenda());
return reponse;
}
- Zeilen 15–18: Wir nutzen die vorhandene Aktion [/getAgenda], um sie aufzurufen. Anschließend prüfen wir den Status der Antwort (Zeile 16). Wird ein Fehler festgestellt, brechen wir an dieser Stelle ab und geben die Antwort zurück;
- Zeile 20: Wir senden die angeforderten Ansichten:
8.6.6.12. Die Aktion [/supprimerRv]
Mit der Aktion [/deleteRv] können Sie einen Termin löschen. Ihre Signatur lautet wie folgt:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin)
- Zeile 3: Der übermittelte Wert ist vom Typ [PostSupprimerRv] wie folgt:
public class PostSupprimerRv extends PostUser {
// data
@NotNull
private Long idRv;
..
}
- Zeile 1: Die Klasse [PostSupprimerRv] erweitert die Klasse [PostUser] und enthält daher eine Sprache und einen Benutzer;
- Zeile 5: die Nummer des zu löschenden Termins;
Die Umsetzung der Aktion erfolgt wie folgt:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse supprimerRv(@Valid @RequestBody PostSupprimerRv postSupprimerRv, BindingResult result, HttpServletRequest request, HttpServletResponse response,
@RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postSupprimerRv.getLang(), origin, request, response, result,
rdvMedecinsCorsController);
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// posted values
User user = postSupprimerRv.getUser();
long idRv = postSupprimerRv.getIdRv();
// we delete the appointment
AgendaMedecinJour agenda = null;
try {
// we get it back
Rv rv = application.getRvById(user, idRv);
Creneau creneau = application.getCreneauById(user, rv.getIdCreneau());
long idMedecin = creneau.getIdMedecin();
Date jour = rv.getJour();
// delete the associated rv
application.supprimerRv(user, idRv);
// we regenerate the doctor's diary
agenda = application.getAgendaMedecinJour(user, idMedecin, new SimpleDateFormat("yyyy-MM-dd").format(jour));
// we return the new diary
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
- Zeile 22: Den zu löschenden Termin abrufen. Falls dieser nicht existiert, wird eine Ausnahme ausgelöst;
- Zeilen 23–25: Anhand dieses Termins ermitteln wir den Arzt und den entsprechenden Tag. Diese Informationen werden benötigt, um den Terminplan des Arztes neu zu erstellen;
- Zeile 27: Der Termin wird gelöscht;
- Zeile 29: Wir fordern den neuen Zeitplan des Arztes an. Dies ist wichtig. Neben dem gerade freigewordenen Termin haben möglicherweise andere Nutzer der Anwendung Änderungen am Zeitplan vorgenommen. Es ist wichtig, dem Nutzer die aktuellste Version des Zeitplans zurückzugeben;
- Zeilen 31–34: Der Kalender wird zurückgegeben:
8.6.6.13. Die Aktion [/validerRv]
Die Aktion [/validerRv] fügt einen Termin in den Kalender eines Arztes ein. Ihre Signatur lautet wie folgt:
@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin)
- Zeile 3: Der übermittelte Wert ist vom Typ [PostValiderRv] wie folgt:
public class PostValiderRv extends PostUser {
// data
@NotNull
private Long idCreneau;
@NotNull
private Long idClient;
@NotNull
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date jour;
...
}
- Zeile 1: Die Klasse [PostValiderRv] erweitert die Klasse [PostUser] und enthält daher eine Sprache und einen Benutzer;
- Zeile 5: die Nummer des Zeitfensters;
- Zeile 7: die Kunden-ID, für die die Reservierung vorgenommen wird;
- Zeile 10: der Tag des Termins;
Die Implementierung der Aktion sieht wie folgt aus:
// appointment validation
@RequestMapping(value = "/validerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
@ResponseBody
public Reponse validerRv(@RequestBody PostValiderRv postValiderRv, BindingResult result, HttpServletRequest request, HttpServletResponse response, @RequestHeader(value = "Origin", required = false) String origin) {
// action contexts
ActionContext actionContext = getActionContext(postValiderRv.getLang(), origin, request, response, result,rdvMedecinsCorsController);
WebApplicationContext springContext = actionContext.getSpringContext();
WebContext thymeleafContext = actionContext.getThymeleafContext();
Locale locale = actionContext.getLocale();
// mistakes?
List<String> erreurs = actionContext.getErreurs();
if (erreurs != null) {
return getViewErreurs(thymeleafContext, erreurs);
}
// check the validity of the appointment date
if (result != null) {
new PostGetAgendaValidator().validate(postValiderRv, result);
if (result.hasErrors()) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, getErreursForModel(result, locale, springContext));
}
}
// posted values
User user = postValiderRv.getUser();
long idClient = postValiderRv.getIdClient();
long idCreneau = postValiderRv.getIdCreneau();
Date jour = postValiderRv.getJour();
// action
try {
// get information on the niche
Creneau créneau = application.getCreneauById(user, idCreneau);
long idMedecin = créneau.getIdMedecin();
// we add the Rv
application.ajouterRv(postValiderRv.getUser(), new SimpleDateFormat("yyyy-MM-dd").format(jour), idCreneau,idClient);
// we regenerate the agenda
AgendaMedecinJour agenda = application.getAgendaMedecinJour(user, idMedecin,
new SimpleDateFormat("yyyy-MM-dd").format(jour));
// we return the new diary
Reponse reponse = new Reponse();
reponse.setStatus(1);
reponse.setAgenda(getPartialViewAgenda(actionContext, agenda, locale));
return reponse;
} catch (RdvMedecinsException ex) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, ex.getMessages());
} catch (Exception e2) {
// returns the [errors] view
return getViewErreurs(thymeleafContext, getErreursForException(e2));
}
}
}
Der Code ähnelt dem der Aktion [/deleteRv].
8.6.7. Schritt 4: Testen des Spring/Thymeleaf-Servers
Wir werden nun die verschiedenen oben beschriebenen Aktionen mit dem Chrome-Plugin [Advanced Rest Client] testen (siehe Abschnitt 9.6).
8.6.7.1. Testkonfiguration
Alle Aktionen erwarten einen geposteten Wert. Wir werden Variationen der folgenden JSON-Zeichenkette posten:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Dieser übermittelte Wert enthält Informationen, die für die meisten Aktionen überflüssig sind. Diese werden jedoch von den Aktionen, die sie empfangen, ignoriert und verursachen keinen Fehler. Dieser übermittelte Wert hat den Vorteil, dass er die verschiedenen zu übermittelnden Werte abdeckt.
8.6.7.2. Die Aktion [/getNavbarStart]
![]() |
- in [1] die zu testende Aktion;
- in [2] der übermittelte Wert;
- in [3] ist der übermittelte Wert eine JSON-Zeichenkette;
- in [4] wird die Ansicht [navbar-start] auf Englisch angefordert;
Das erhaltene Ergebnis lautet wie folgt:
![]() |
Wir haben die Ansicht [navbar-start] auf Englisch erhalten (markierte Bereiche).
Fügen wir nun einen Fehler ein. Wir setzen das [lang]-Attribut des übermittelten Werts auf null. Wir erhalten das folgende Ergebnis:
![]() |
Wir haben eine Fehlerantwort (Status 2) erhalten, die darauf hinweist, dass das Feld [lang] erforderlich war.
8.6.7.3. Die Aktion [/getNavbarRun]
Wir rufen die Aktion [getNavbarRun] mit dem folgenden übermittelten Wert auf:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Das Ergebnis lautet wie folgt:
![]() |
8.6.7.4. Die Aktion [/getJumbotron]
Wir rufen die Aktion [getJumbotron] mit den folgenden POST-Daten auf:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Das erhaltene Ergebnis lautet wie folgt:
![]() |
8.6.7.5. Die Aktion [/getLogin]
Wir rufen die Aktion [getLogin] mit den folgenden POST-Daten auf:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Das Ergebnis lautet wie folgt:
![]() |
8.6.7.6. Die Aktion [/getAccueil]
Wir rufen die Aktion [getAccueil] mit dem folgenden übermittelten Wert auf:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Das erhaltene Ergebnis lautet wie folgt:
![]() |
Wir versuchen es erneut mit einem unbekannten Benutzer:
{"user":{"login":"x","passwd":"x"},"lang":"fr","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Das Ergebnis lautet wie folgt:
![]() |
Wir beginnen erneut mit einem bestehenden Benutzer, der nicht zur Nutzung der Anwendung berechtigt ist:
{"user":{"login":"user","passwd":"user"},"lang":"en","jour":"2015-01-22", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Das Ergebnis lautet wie folgt:
![]() |
8.6.7.7. Die Aktion [/getAgenda]
Wir rufen die Aktion [getAgenda] mit dem folgenden übermittelten Wert auf:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Das erhaltene Ergebnis lautet wie folgt:
![]() |
Wir versuchen es noch einmal mit einem Datum, das vor dem heutigen liegt:
![]() |
Wir fangen wieder mit einem nicht existierenden Arzt an:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":11, "idCreneau":2, "idClient":4, "idRv":93}
Das Ergebnis lautet wie folgt:
![]() |
8.6.7.8. Die Aktion [/getNavbarRunJumbotronAccueil]
Wir rufen die Aktion [getNavbarRunJumbotronAccueil] mit dem folgenden übermittelten Wert auf:
{"user":{"login":"admin","passwd":"admin"},"lang":"en","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Das Ergebnis lautet wie folgt:
![]() |
Das Gleiche gilt für einen unbekannten Benutzer:
![]() |
8.6.7.9. Die Aktion [/getNavbarRunJumbotronHomeCalendar]
Wir rufen die Aktion [getNavbarRunJumbotronHomeCalendar] mit dem folgenden übermittelten Wert auf:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Das Ergebnis lautet wie folgt:
![]() |
Wir geben einen Arzt ein, der nicht existiert:
![]() |
8.6.7.10. Die Aktion [/deleteAppointment]
Wir rufen die Aktion [deleteAppointment] mit dem folgenden übermittelten Wert auf:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Termin Nr. 93 existiert nicht. Das Ergebnis lautet wie folgt:
![]() |
Bei einem bestehenden Termin:
![]() |
Wir können in der Datenbank überprüfen, ob der Termin tatsächlich gelöscht wurde. Der neue Kalender wird zurückgegeben.
8.6.7.11. Die Aktion [/validateAppointment]
Wir rufen die Aktion [validateAppointment] mit dem folgenden übermittelten Wert auf:
{"user":{"login":"admin","passwd":"admin"},"lang":"fr","jour":"2015-01-28", "idMedecin":1, "idCreneau":2, "idClient":4, "idRv":93}
Das Ergebnis lautet wie folgt:
![]() |
Wir können in der Datenbank überprüfen, ob der Termin erfolgreich angelegt wurde. Der neue Kalender wurde zurückgegeben.
Wir machen dasselbe mit einer nicht existierenden Slot-Nummer:
![]() |
Wir machen dasselbe mit einer nicht vorhandenen Client-ID:
![]() |
8.6.8. Schritt 5: Schreiben des JavaScript-Clients
Kehren wir zur Architektur des Servers [Web1] zurück:
![]() |
Der Client [2] des Servers [Web1] ist ein JavaScript-Client vom Typ SPV (Single-Page Application):
- Der Client fordert die Startseite von einem Webserver (nicht unbedingt [Web1]) an;
- er fordert die folgenden Seiten über Ajax-Aufrufe vom Server [Web1] an;
Um diesen Client zu erstellen, verwenden wir das Tool [Webstorm] (siehe Abschnitt 9.8). Ich fand dieses Tool praktischer als STS. Sein Hauptvorteil besteht darin, dass es eine Code-Autovervollständigung sowie einige Refactoring-Optionen bietet. Dies hilft, viele Fehler zu vermeiden.
8.6.8.1. Das JS-Projekt
Das JS-Projekt hat die folgende Verzeichnisstruktur:
![]() |
- in [1] der gesamte JS-Client. [boot.html] ist die Startseite. Dies ist die einzige Seite, die vom Browser geladen wird;
- in [2] die Stylesheets für die Bootstrap-Komponenten;
- in [3] die wenigen Bilder, die von der Anwendung verwendet werden;
![]() |
- in [4] die JS-Skripte. Hier findet unsere Arbeit statt;
- in [5] die verwendeten JS-Bibliotheken: hauptsächlich jQuery und diejenigen für die Bootstrap-Komponenten;
8.6.8.2. Die Code-Architektur
Der Code wurde in drei Schichten unterteilt:
![]() |
- Die [Präsentations-]Ebene enthält die Funktionen zur Seiteninitialisierung [boot.xml] sowie die Funktionen für die verschiedenen Bootstrap-Komponenten. Sie wird durch die Datei [ui.js] implementiert;
- Die [events]-Schicht enthält alle Ereignisbehandler für die [presentation]-Schicht. Sie wird durch die Datei [evts.js] implementiert;
- Die [DAO]-Schicht sendet HTTP-Anfragen an den [Web1]-Server. Sie wird durch die Datei [dao.js] implementiert;
8.6.8.3. Die [presentation]-Schicht
![]() |
Die [Präsentationsschicht] wird durch die folgende [ui.js]-Datei implementiert:
//la couche [présentation]
var ui = {
// variables globales;
"agenda": "",
"resa": "",
"langue": "",
"urlService": "http://localhost:8081",
"page": "login",
"jourAgenda": "",
"idMedecin": "",
"user": {},
"login": {},
"exceptionTitle": {},
"calendar_infos": {},
"erreur": "",
"idCreneau": "",
"done": "",
// composants de la vue
"body": "",
"navbar": "",
"jumbotron": "",
"content": "",
"exception": "",
"exception_text": "",
"exception_title": "",
"loading": ""
};
// la couche des evts
var evts = {};
// la couche [dao]
var dao = {};
// ------------ document ready
$(document).ready(function () {
// initialisation document
console.log("document.ready");
// composants de la page
ui.navbar = $("#navbar");
ui.jumbotron = $("#jumbotron");
ui.content = $("#content");
ui.erreur = $("#erreur");
ui.exception = $("#exception");
ui.exception_text = $("#exception-text");
ui.exception_title = $("#exception-title");
// on mémorise la page de login pour pouvoir la restituer
ui.login.lang = ui.langue;
ui.login.navbar = ui.navbar.html();
ui.login.jumbotron = ui.jumbotron.html();
ui.login.content = ui.content.html();
// URL du service
$("#urlService").val(ui.urlService);
});
// ------------------------ Bootstrap component initialization functions
ui.initNavBarStart = function () {
...
};
ui.initNavBarRun = function () {
...
};
ui.initChoixMedecinJour = function () {
...
};
ui.updateCalendar = function (renew) {
...
};
// affiche le jour sélectionné
ui.displayJour = function () {
...
};
ui.initAgenda = function () {
...
};
ui.initResa = function () {
...
};
- Um die Ebenen voneinander zu trennen, wurde beschlossen, sie in drei Objekte zu platzieren:
- [ui] für die [Präsentations-]Ebene (Zeilen 2–27),
- [evts] für die Ereignisverwaltungsschicht (Zeile 29),
- [dao] für die [DAO]-Schicht (Zeile 31);
Diese Aufteilung der Schichten in drei Objekte hilft, eine Reihe von Konflikten bei Variablen- und Funktionsnamen zu vermeiden. Jede Schicht verwendet Variablen und Funktionen, denen das Präfix des Objekts vorangestellt ist, das die Schicht kapselt.
- Zeilen 38–44: Wir speichern die Felder, die unabhängig von den angezeigten Ansichten immer vorhanden sind. Dies vermeidet sich wiederholende und unnötige jQuery-Suchen;
- Zeilen 46–49: Die Startseite wird lokal gespeichert, damit sie wiederhergestellt werden kann, wenn sich der Benutzer abmeldet und die Sprache nicht geändert hat;
- Zeilen 54–83: Initialisierungsfunktionen für Bootstrap-Komponenten. Diese wurden alle in der Erörterung der Bootstrap-Komponenten in Abschnitt 8.6.4 behandelt;
8.6.8.4. Hilfsfunktionen der [events]-Ebene
![]() |
Die Ereignisbehandler wurden in der Datei [evts.js] abgelegt. Einige Funktionen werden von den Ereignisbehandlern regelmäßig verwendet. Wir stellen sie nun vor:
// début d'attente
evts.beginWaiting = function () {
// début attente
ui.loading = $("#loading");
ui.loading.show();
ui.exception.hide();
ui.erreur.hide();
evts.travailEnCours = true;
};
// fin d'attente
evts.stopWaiting = function () {
// fin attente
evts.travailEnCours = false;
ui.loading = $("#loading");
ui.loading.hide();
};
// affichage résultat
evts.showResult = function (result) {
// on affiche les données reçues
var data = result.data;
// on analyse le status
switch (result.status) {
case 1:
// erreur ?
if (data.status == 2) {
ui.erreur.html(data.content);
ui.erreur.show();
} else {
if (data.navbar) {
ui.navbar.html(data.navbar);
}
if (data.jumbotron) {
ui.jumbotron.html(data.jumbotron);
}
if (data.content) {
ui.content.html(data.content)
}
if (data.agenda) {
ui.agenda = $("#agenda");
ui.resa = $("#resa");
}
}
break;
case 2:
// affichage erreur
evts.showException(data);
break;
}
};
// ------------ fonctions diverses
evts.showException = function (data) {
// affichage erreur
ui.exception.show();
ui.exception_text.html(data);
ui.exception_title.text(ui.exceptionTitle[ui.langue]);
};
- Zeile 2: Die Funktion [evts.beginwaiting] wird vor jeder asynchronen [DAO]-Aktion aufgerufen;
- Zeilen 4–5: Das animierte Bild des Wartens wird angezeigt;
- Zeilen 6–7: Der Bereich für die Anzeige von Fehlern und Ausnahmen wird ausgeblendet (beides ist nicht dasselbe);
- Zeile 8: Es wird angezeigt, dass eine asynchrone Aufgabe ausgeführt wird;
- Zeile 12: Die Funktion [evts.stopwaiting] wird aufgerufen, nachdem eine asynchrone [DAO]-Aktion ihr Ergebnis zurückgegeben hat;
- Zeile 14: Wir stellen fest, dass der asynchrone Vorgang abgeschlossen ist;
- Zeile 15: Das animierte Wartebild wird ausgeblendet;
- Zeile 20: Die Funktion [evts.showResult] zeigt das Ergebnis [result] einer asynchronen [DAO]-Aktion an. Das Ergebnis ist ein JS-Objekt der folgenden Form: {'status':status,'data':data,'sendMeBack':sendMeBack}.
- Zeilen 47–50: werden verwendet, wenn [result.status == 2]. Dies tritt ein, wenn der [Web1]-Server eine Antwort mit einem HTTP-Fehlerheader (z. B. 403 Forbidden) sendet. In diesem Fall ist [data] die vom Server gesendete JSON-Zeichenkette, die den Fehler angibt;
- Zeile 25: Fall, in dem eine gültige Antwort vom [Web1]-Server empfangen wurde. Das Feld [data] enthält dann die Antwort des Servers: {'status':status,'navbar':navbar,'jumbotron':jumbotron,'agenda':agenda,'content':content};
- Zeile 27: Fall, in dem der Server [Web1] eine Fehlerantwort gesendet hat {'status':2,'navbar':null,'jumbotron':null,'agenda':null,'content':errors};
- Zeilen 28–29: Die Ansicht [Fehler] wird angezeigt;
- Zeilen 31–33: optionale Anzeige der Navigationsleiste;
- Zeilen 34–36: optionale Anzeige des Jumbotrons;
- Zeilen 37–39: Das Feld [data.content] kann angezeigt werden. Je nach Fall stellt dies eine der Ansichten [home, calendar] dar;
- Zeilen 40–43: Wenn der Kalender neu generiert wurde, werden bestimmte Verweise auf seine Komponenten abgerufen, damit sie nicht jedes Mal nachgeschlagen werden müssen, wenn sie benötigt werden;
- Zeile 54: Die Funktion [evts.showException] zeigt den in ihrem Parameter [data] enthaltenen Ausnahmetext an;
- Zeilen 57–58: Der Ausnahmetext wird angezeigt;
- Zeile 58: Der Titel der Ausnahme hängt von der aktuellen Sprache ab;
Die Datei [evts.js] enthält über 300 Zeilen Code, auf die ich nicht im Detail eingehen werde. Ich werde lediglich einige Beispiele hervorheben, um den Zweck dieser Ebene zu veranschaulichen.
8.6.8.5. Benutzeranmeldung

Die Benutzeranmeldung wird von der folgenden Funktion abgewickelt:
// ------------------------ connexion
evts.connecter = function () {
// retrieve the values to be posted
var login = $("#login").val().trim();
var passwd = $("#passwd").val().trim();
// set the server's URL
ui.urlService = $("#urlService").val().trim();
dao.setUrlService(ui.urlService);
// query parameters
var post = {
"user": {
"login": login,
"passwd": passwd
},
"lang": ui.langue
};
var sendMeBack = {
"user": {
"login": login,
"passwd": passwd
},
"caller": evts.connecterDone
};
// query
evts.execute([{
"name": "accueil-sans-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- Zeilen 4–5: Abrufen des Benutzernamens und des Passworts des Benutzers;
- Zeilen 7–8: Abrufen der URL des [Web1]-Dienstes. Diese wird sowohl in der [ui]-Schicht als auch in der [dao]-Schicht gespeichert;
- Zeilen 10–16: Der zu übermittelnde Wert: die aktuelle Sprache und der Benutzer, der sich anzumelden versucht;
- Zeilen 17–23: Das [sendMeBack]-Objekt wird an die [DAO]-Funktion übergeben, die aufgerufen wird, und diese Funktion muss es an die Funktion in Zeile 22 zurückgeben. Hier kapselt das [sendMeBack]-Objekt den Benutzer, der versucht, sich anzumelden;
- Zeilen 25–29: Die Funktion [evts.execute] ist in der Lage, eine Abfolge asynchroner Aktionen auszuführen. Hier übergeben wir eine Liste, die aus einer einzigen Aktion besteht. Ihre Felder lauten wie folgt:
- [name]: der Name der auszuführenden asynchronen Aktion,
- [post]: der Wert, der an den [Web1]-Server gesendet werden soll,
- [sendMeBack]: der Wert, den die asynchrone Aktion zusammen mit ihrem Ergebnis zurückgeben muss;
Bevor wir näher auf die Funktion [evts.execute] eingehen, betrachten wir zunächst die Funktion [evts.connecterDone] in Zeile 22. Dies ist die Funktion, an die die aufgerufene asynchrone [DAO]-Funktion ihr Ergebnis zurückgeben muss:
evts.connecterDone = function (result) {
// affichage résultat
evts.showResult(result);
// connexion réussie ?
if (result.status == 1 && result.data.status == 1) {
// page
ui.page = "accueil-sans-agenda";
// on note l'utilisateur
ui.user = result.sendMeBack.user;
}
};
- Zeile 3: Das vom [Web1]-Server zurückgegebene Ergebnis wird angezeigt;
- Zeile 5: Wenn dieses Ergebnis keine Fehler enthält, speichern wir den Typ der neuen Seite (Zeile 7) sowie den authentifizierten Benutzer (Zeile 9);
Die Funktion [evts.execute] führt eine Abfolge asynchroner Aktionen aus:
// exécution d'une suite d'actions
evts.execute = function (actions) {
// travail en cours ?
if (evts.travailEnCours) {
// on ne fait rien
return;
}
// attente
evts.beginWaiting();
// exécution des actions
dao.doActions(actions, evts.stopWaiting);
};
- Zeile 2: Der Parameter [actions] ist eine Liste von asynchronen Aktionen, die ausgeführt werden sollen;
- Zeilen 4–7: Die Ausführung wird nur akzeptiert, wenn keine andere Aktion bereits läuft;
- Zeile 9: Die Wartezeit wird eingeleitet;
- Zeile 11: Die [DAO]-Schicht wird aufgefordert, die Abfolge von Aktionen auszuführen. Der zweite Parameter ist der Name der Funktion, die ausgeführt werden soll, sobald alle Aktionen in der Abfolge ihre Ergebnisse zurückgegeben haben;
Wir werden an dieser Stelle nicht näher auf die Funktion [dao.doActions] eingehen. Wir werden uns ein anderes Ereignis ansehen.
8.6.8.6. Sprachwechsel

Der Sprachwechsel wird von der folgenden Funktion abgewickelt:
// ------------------------ changement de langue
evts.setLang = function (lang) {
// chgt de langue ?
if (lang == ui.langue) {
// on ne fait rien
return;
}
// nouvelle langue
ui.langue = lang;
// quelle page faut-il traduire ?
switch (ui.page) {
case "login":
evts.getLogin();
break;
case "accueil-sans-agenda":
evts.getAccueilSansAgenda();
break;
case "accueil-avec-agenda":
evts.getAccueilAvecAgenda(ui);
break;
}
};
- Zeile 2: Der Parameter [lang] ist die neue Sprache: 'fr' oder 'en';
- Zeilen 4–7: Wenn die neue Sprache die aktuelle ist, nichts tun;
- Zeile 9: Die neue Sprache wird gespeichert;
- Zeilen 12–20: Wenn sich die Sprache geändert hat, muss die derzeit vom Browser angezeigte Seite neu geladen werden. Es gibt drei mögliche Seiten:
- die Seite mit dem Namen [login], auf der die Anmeldeseite angezeigt wird,
- die Seite namens [home-without-calendar], die unmittelbar nach erfolgreicher Authentifizierung angezeigt wird,
- die Seite namens [home-with-calendar], die angezeigt wird, sobald der erste Kalender angezeigt wurde. Sie bleibt dann auf dem Bildschirm, bis sich der Benutzer abmeldet;
Wir werden uns mit dem Fall der Seite [home-with-calendar] befassen. Es gibt drei Versionen dieser Funktion:
![]() |
- Die Version [getAccueilAvecAgenda-one] führt eine einzige asynchrone Aktion aus;
- die Version [getAccueilAvecAgenda-parallel] führt vier asynchrone Aktionen parallel aus;
- die Version [getAccueilAvecAgenda-sequence] führt vier asynchrone Aktionen nacheinander aus;
8.6.8.7. Die Funktion [getAccueilAvecAgenda-one]
Dies ist die folgende Funktion:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// query parameters
var post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
var sendMeBack = {
"caller": evts.getAccueilAvecAgendaDone
};
// request
evts.execute([{
"name": "accueil-avec-agenda",
"post": post,
"sendMeBack": sendMeBack
}]);
};
- Zeilen 4–9: Der zu übermittelnde Wert enthält den angemeldeten Benutzer, die gewünschte Sprache, die ID des Arztes, dessen Terminplan abgefragt werden soll, und den Tag des gewünschten Termins;
- Zeilen 10–12: Das Objekt [sendMeBack] ist das Objekt, das in Zeile 11 an die Funktion zurückgegeben wird. Hier enthält es keine Informationen;
- Zeilen 14–18: Ausführung einer Abfolge asynchroner Aktionen, insbesondere derjenigen mit dem Namen [welcome-with-calendar] (Zeile 15);
- Zeile 11: Die Funktion, die ausgeführt wird, wenn die asynchrone Aktion [welcome-with-calendar] ihr Ergebnis zurückgibt;
Die Funktion [evts.getAccueilAvecAgendaDone] in Zeile 11 zeigt das Ergebnis der asynchronen Funktion [accueil-avec-agenda] an:
evts.getAccueilAvecAgendaDone = function (result) {
// affichage résultat
evts.showResult(result);
// nouvelle page ?
if (result.status == 1 && result.data.status == 1) {
ui.page = "accueil-avec-agenda";
}
};
- Zeile 1: [result] ist das Ergebnis der asynchronen Funktion namens [home-with-calendar];
- Zeile 3: Dieses Ergebnis wird angezeigt;
- Zeile 5: Wenn es sich um ein fehlerfreies Ergebnis handelt, wird die neue Seite geladen (Zeile 6);
8.6.8.8. Die Funktion [getHomeWithCalendar-parallel]
Dies ist die folgende Funktion:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// actions [navbar-run, jumbotron, home, calendar] in //
// navbar-run
var navbarRun = {
"name": "navbar-run"
};
navbarRun.post = {
"lang": ui.langue
};
navbarRun.sendMeBack = {
"caller": evts.showResult
};
// jumbotron
var jumbotron = {
"name": "jumbotron"
};
jumbotron.post = {
"lang": ui.langue
};
jumbotron.sendMeBack = {
"caller": evts.showResult
};
// home
var accueil = {
"name": "accueil"
};
accueil.post = {
"lang": ui.langue,
"user": ui.user
};
accueil.sendMeBack = {
"caller": evts.showResult
};
// agenda
var agenda = {
"name": "agenda"
};
agenda.post = {
"user": ui.user,
"lang": ui.langue,
"idMedecin": ui.idMedecin,
"jour": ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin': ui.idMedecin,
'jour': ui.jourAgenda,
"caller": evts.getAgendaDone
};
// execution actions in //
evts.execute([navbarRun, jumbotron, accueil, agenda])
};
- Zeile 51: Diesmal werden vier asynchrone Aktionen ausgeführt. Sie werden parallel ausgeführt;
- Zeilen 5–13: Definition der Aktion [navbarRun], die die Navigationsleiste [navbar-run] abruft;
- Zeile 12: Die Funktion, die ausgeführt wird, sobald die asynchrone Aktion [navbarRun] ihr Ergebnis zurückgegeben hat;
- Zeilen 15–23: Definition der Aktion [jumbotron], die die Ansicht [jumbotron] abruft;
- Zeile 22: Die Funktion, die ausgeführt werden soll, wenn die asynchrone Aktion [jumbotron] ihr Ergebnis zurückgibt;
- Zeilen 25–34: Definition der Aktion [home], die die Ansicht [home] abruft;
- Zeile 33: die Funktion, die ausgeführt werden soll, wenn die asynchrone Aktion [home] ihr Ergebnis zurückgibt;
- Zeilen 36–49: Definition der Aktion [agenda], die die Ansicht [jumbotron] abruft;
- Zeile 48: die Funktion, die ausgeführt werden soll, wenn die asynchrone Aktion [agenda] ihr Ergebnis zurückgibt;
8.6.8.9. Die Funktion [getHomeWithAgenda-sequence]
Dies ist die folgende Funktion:
// -------------------------- getAccueilAvecAgenda
evts.getAccueilAvecAgenda=function(ui) {
// actions [navbar-run, jumbotron, home, agenda] in order
// agenda
var agenda = {
"name" : "agenda"
};
agenda.post = {
"user" : ui.user,
"lang" : ui.langue,
"idMedecin" : ui.idMedecin,
"jour" : ui.jourAgenda
};
agenda.sendMeBack = {
'idMedecin' : ui.idMedecin,
'jour' : ui.jourAgenda,
"caller" : evts.getAgendaDone
};
// home
var accueil = {
"name" : "accueil"
};
accueil.post = {
"lang" : ui.langue,
"user" : ui.user
};
accueil.sendMeBack = {
"caller" : evts.showResult,
"next" : agenda
};
// jumbotron
var jumbotron = {
"name" : "jumbotron"
};
jumbotron.post = {
"lang" : ui.langue
};
jumbotron.sendMeBack = {
"caller" : evts.showResult,
"next" : accueil
};
// navbar-run
var navbarRun = {
"name" : "navbar-run"
};
navbarRun.post = {
"lang" : ui.langue
};
navbarRun.sendMeBack = {
"caller" : evts.showResult,
"next" : jumbotron
};
// execution actions in sequence
evts.execute([ navbarRun ])
};
- Zeile 54: Die Aktion [navbarRun] wird ausgeführt. Wenn sie abgeschlossen ist, fahren wir mit der nächsten fort: [jumbotron], Zeile 51. Diese Aktion wird dann nacheinander ausgeführt. Wenn sie abgeschlossen ist, fahren wir mit der nächsten fort: [home], Zeile 40. Diese wird nacheinander ausgeführt. Wenn sie abgeschlossen ist, fahren wir mit der nächsten fort: [agenda], Zeile 29. Diese wird nacheinander ausgeführt. Wenn sie abgeschlossen ist, hören wir auf, da die Aktion [agenda] keine nachfolgende Aktion hat.
8.6.8.10. Die [DAO]-Schicht
![]() |
Die Datei [dao.js] enthält alle Funktionen der [DAO]-Schicht. Wir werden diese nach und nach vorstellen:
// URL exposed by the server
dao.urls = {
"login": "/getLogin",
"accueil": "/getAccueil",
"jumbotron": "/getJumbotron",
"agenda": "/getAgenda",
"supprimerRv": "/supprimerRv",
"validerRv": "/validerRv",
"navbar-start": "/getNavbarStart",
"navbar-run": "/getNavbarRun",
"accueil-sans-agenda": "/getNavbarRunJumbotronAccueil",
"accueil-avec-agenda": "/getNavbarRunJumbotronAccueilAgenda"
};
// --------------- interface
// server url
dao.setUrlService = function (urlService) {
dao.urlService = urlService;
};
- Zeilen 16–18: die Funktion, die die Service-URL [Web1] festlegt;
- Zeilen 2–13: das Wörterbuch, das den Namen einer asynchronen Aktion mit der abzufragenden [Web1]-Server-URL verknüpft;
// ------------------ gestion générique des actions
// exécution d'une suite d'actions asynchrones
dao.doActions = function (actions, done) {
// traitement des actions
dao.actionsCount = actions.length;
dao.actionIndex = 0;
for (var i = 0; i < dao.actionsCount; i++) {
// requête DAO asynchrone
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, actions[i], done);
}
};
- Zeile 3: Die Funktion [dao.doActions] führt eine Abfolge asynchroner Aktionen [actions] aus. Der Parameter [done] ist die Funktion, die ausgeführt wird, sobald alle Aktionen ihre Ergebnisse zurückgegeben haben;
- Zeilen 7–12: Die asynchronen Aktionen werden parallel ausgeführt. Hat jedoch eine von ihnen einen Nachfolger, wird dieser Nachfolger am Ende der vorhergehenden Aktion ausgeführt;
- Zeile 9: ein [Deferred]-Objekt im Zustand [pending];
- Zeile 10: Wenn dieses Objekt in den Zustand [resolved] wechselt, wird die Funktion [dao.actionDone] ausgeführt;
- Zeile 11: Aktion #i in der Liste wird asynchron ausgeführt. Der Parameter [done] aus Zeile 3 wird als Argument übergeben;
Die Funktion [dao.actionDone], die am Ende jeder asynchronen Aktion ausgeführt wird, lautet wie folgt:
// on a reçu un résultat
dao.actionDone = function (result) {
// caller ?
var sendMeBack = result.sendMeBack;
if (sendMeBack && sendMeBack.caller) {
sendMeBack.caller(result);
}
// next ?
if (sendMeBack && sendMeBack.next) {
// requête DAO asynchrone
var deferred = $.Deferred();
deferred.done(dao.actionDone);
dao.doAction(deferred, sendMeBack.next, sendMeBack.done);
}
// fini ?
dao.actionIndex++;
if (dao.actionIndex == dao.actionsCount) {
// done ?
if (sendMeBack && sendMeBack.done) {
sendMeBack.done(result);
}
}
};
- Zeile 2: Die Funktion [dao.actionDone] erhält das Ergebnis [result] von einer der asynchronen Aktionen in der Liste der auszuführenden Aktionen;
- Zeilen 4–7: Wenn die abgeschlossene asynchrone Aktion eine Funktion angegeben hat, an die das Ergebnis zurückgegeben werden soll, wird diese Funktion aufgerufen;
- Zeilen 9–14: Wenn die abgeschlossene asynchrone Aktion einen Nachfolger hat, wird diese Aktion nacheinander ausgeführt;
- Zeile 16: Eine Aktion ist abgeschlossen. Der Zähler für abgeschlossene Aktionen wird erhöht. Eine Aktion, die eine unbestimmte Anzahl nachfolgender Aktionen hat, zählt als eine Aktion;
- Zeilen 19–21: Wenn ursprünglich eine [done]-Funktion angegeben wurde, die ausgeführt werden soll, sobald alle Aktionen in der Sequenz ihre Ergebnisse zurückgegeben haben, wird diese Funktion nun ausgeführt;
Die Methode [dao.doAction] führt eine asynchrone Aktion aus:
// exécution d'une action
dao.doAction = function (deferred, action, done) {
// fonction done à embarquer dans l'action
if (action.sendMeBack) {
action.sendMeBack.done = done;
} else {
action.sendMeBack = {
"done": done
};
}
// exécution action
dao.executePost(deferred, action.sendMeBack, dao.urls[action.name], action.post)
};
- Zeilen 4–10: Wie wir gerade gesehen haben, muss die Funktion, die das Ergebnis der auszuführenden asynchronen Aktion verarbeitet, Zugriff auf die Funktion [done] haben. Dazu platzieren wir die Funktion [done] im Objekt [sendMeBack], das Teil des Ergebnisses der asynchronen Operation sein wird;
- Zeile 12: Wir führen die Funktion [dao.executePost] aus, die eine HTTP-Anfrage an den [Web1]-Server sendet. Die Ziel-URL ist die URL, die dem Namen der auszuführenden Aktion zugeordnet ist;
Die Funktion [dao.executePost] führt eine HTTP-Anfrage aus:
// requête HTTP
dao.executePost = function (deferred, sendMeBack, url, post) {
// on fait un appel Ajax à la main
$.ajax({
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
url: dao.urlService + url,
type: 'POST',
data: JSON3.stringify(post),
dataType: 'json',
success: function (data) {
// on rend le résultat
deferred.resolve({
"status": 1,
"data": data,
"sendMeBack": sendMeBack
});
},
error: function (jqXHR, textStatus, errorThrown) {
var data;
if (jqXHR.responseText) {
data = jqXHR.responseText;
} else {
data = textStatus;
}
// on rend l'erreur
deferred.resolve({
"status": 2,
"data": data,
"sendMeBack": sendMeBack
});
}
});
};
Wir sind dieser Funktion bereits begegnet und haben sie besprochen. Beachten Sie einfach in Zeile 9, dass die Ziel-URL die Verkettung der Server-URL [Web1] mit der dem Aktionsnamen zugeordneten URL ist.
8.6.8.11. Die Startseite
![]() |

Die Startseite [boot.html] zeigt die oben abgebildete Ansicht an. Sie ist die einzige Seite, die direkt vom Browser geladen wird. Die anderen werden über Ajax-Aufrufe abgerufen. Der Code lautet wie folgt:
<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta name="viewport" content="width=device-width"/>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>RdvMedecins</title>
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="css/bootstrap-3.1.1-min.css"/>
<link rel="stylesheet" type="text/css" href="css/bootstrap-select.min.css"/>
<link rel="stylesheet" type="text/css" href="css/datepicker3.css"/>
<link rel="stylesheet" type="text/css" href="css/footable.core.min.css"/>
<!-- Custom styles for this template -->
<link rel="stylesheet" type="text/css" href="css/rdvmedecins.css"/>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="vendor/bootstrap.js"></script>
<script type="text/javascript" src="vendor/bootstrap-select.js"></script>
<script type="text/javascript" src="vendor/moment-with-locales.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker.js"></script>
<script type="text/javascript" src="vendor/bootstrap-datepicker.fr.js"></script>
<script type="text/javascript" src="vendor/footable.js"></script>
<!-- user scripts -->
<script type="text/javascript" src="js/json3.js"></script>
<script type="text/javascript" src="js/ui.js"></script>
<script type="text/javascript" src="js/evts.js"></script>
<script type="text/javascript" src="js/getAccueilAvecAgenda-sequence.js"></script>
<script type="text/javascript" src="js/dao.js"></script>
</head>
<body id="body">
<div id="navbar">
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">RdvMedecins</a>
</div>
<div class="navbar-collapse collapse">
<img id="loading" src="images/loading.gif" alt="waiting..." style="display: none"/>
<!-- identification form -->
<div class="navbar-form navbar-right" role="form" id="formulaire">
<div class="form-group">
<input type="text" placeholder="URL du serveur" class="form-control" id="urlService"/>
</div>
<div class="form-group">
<input type="text" placeholder="Utilisateur" class="form-control" id="login"/>
</div>
<div class="form-group">
<input type="password" placeholder="Mot de passe" class="form-control" id="passwd"/>
</div>
<button type="button" class="btn btn-success" onclick="javascript:evts.connecter()">Connexion</button>
<!-- languages -->
<div class="btn-group">
<button type="button" class="btn btn-danger">Langue</button>
<button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
<span class="caret"></span> <span class="sr-only">Toggle Dropdown</span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="javascript:evts.setLang('fr')">Français</a></li>
<li><a href="javascript:evts.setLang('en')">English</a></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="container">
<!-- Bootstrap Jumbotron -->
<div id="jumbotron">
<div class="jumbotron">
<div class="row">
<div class="col-md-2">
<img src="images/caduceus.jpg" alt="RvMedecins"/>
</div>
<div class="col-md-10">
<h1>
Cabinet médical<br/>Les Médecins associés
</h1>
</div>
</div>
</div>
</div>
<!-- error panels -->
<div id="erreur"></div>
<div id="exception" class="alert alert-danger" style="display: none">
<h3 id="exception-title"></h3>
<span id="exception-text"></span>
</div>
<!-- content -->
<div id="content">
<div class="alert alert-info">Authentifiez-vous pour accéder à l'application</div>
</div>
</div>
<!-- init page -->
<script>
// on initialise la page
ui.langue = 'fr';
ui.exceptionTitle['fr'] = "L'erreur suivante s'est produite côté serveur :";
ui.exceptionTitle['en'] = "The following server error was met:";
ui.initNavBarStart();
</script>
</body>
</html>
- Wir sind dieser Art von Seite bereits im Kapitel über Bootstrap (Abschnitt 8.6.4) begegnet;
- Zeilen 99–105: Initialisierung bestimmter Elemente der [Präsentations-]Ebene;
- Zeile 27: Das Skript [getAccueilAvecAgenda-sequence.js] wird verwendet. Durch Ändern des Skripts in dieser Zeile erhalten wir drei verschiedene Verhaltensweisen beim Abrufen der Seite [accueil-avec-agenda]:
- [getAccueilAvecAgenda-one.js] ruft die Seite mit einer einzigen HTTP-Anfrage ab,
- [getAccueilAvecAgenda-parallel.js] ruft die Seite mit vier gleichzeitigen HTTP-Anfragen ab,
- [getAccueilAvecAgenda-sequence.js] ruft die Seite mit vier aufeinanderfolgenden HTTP-Anfragen ab;
8.6.8.12. Tests
Es gibt verschiedene Möglichkeiten, Tests durchzuführen. Hier verwenden wir das Tool [Webstorm]:
![]() |
- In [1] öffnen wir ein Projekt. Wir wählen einfach den Ordner [2] aus, der die statische Verzeichnisstruktur (HTML, CSS, JS) der zu testenden Website enthält;
![]() |
- in [3] die statische Website;
- In [4-5] laden wir die Seite [boot.html];
![]() |
- In [5] sehen wir, dass ein von [Webstorm] eingebetteter Server die Seite [boot.html] über Port [63342] bereitgestellt hat. Dies ist ein wichtiger Punkt, den es zu verstehen gilt, da es bedeutet, dass die Skripte auf der Seite [boot.html] beliebige domänenübergreifende Anfragen an den Server [Web1] stellen, der auf [localhost:8081] läuft. Der Browser, der [boot.html] geladen hat, weiß, dass er die Seite von [localhost:63342] geladen hat. Er wird daher nicht zulassen, dass diese Seite Aufrufe an die Seite [localhost:8081] sendet, da es sich nicht um denselben Port handelt. Er wird daher die in Abschnitt 8.4.14 beschriebenen domänenübergreifenden Regeln durchsetzen. Aus diesem Grund muss die [Web1]-Anwendung so konfiguriert werden, dass sie diese domänenübergreifenden Anfragen akzeptiert. Dies wird in der [AppConfig]-Datei des Spring/Thymeleaf-Servers konfiguriert:
![]() |
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.springthymeleaf.server" })
@Import({ WebConfig.class, DaoConfig.class })
public class AppConfig {
// admin / admin
private final String USER_INIT = "admin";
private final String MDP_USER_INIT = "admin";
// root web service / json
private final String WEBJSON_ROOT = "http://localhost:8080";
// timeout in milliseconds
private final int TIMEOUT = 5000;
// CORS
private final boolean CORS_ALLOWED=true;
...
Wir überlassen es dem Leser, den JS-Client zu testen. Er sollte in der Lage sein, die in Abschnitt 8.6.3 beschriebene Funktionalität zu reproduzieren.
Sobald der JavaScript-Client validiert wurde, kann er im Ordner [Web1] des Servers bereitgestellt werden, um zu vermeiden, dass domänenübergreifende Anfragen zugelassen werden müssen:
![]() |
Oben haben wir die getestete Website in den Ordner [src/main/resources/static] kopiert. Anschließend können wir die URL [http://localhost:8081/boot.html] aufrufen:

Nun benötigen wir keine domänenübergreifenden Anfragen mehr und können Folgendes in die Konfigurationsdatei [AppConfig] des Servers [Web1] schreiben:
// CORS
private final boolean CORS_ALLOWED=false;
Die obige Anwendung funktioniert weiterhin. Wenn wir zur [WebStorm]-Anwendung zurückkehren, funktioniert diese nicht mehr:


Wenn wir zur Entwicklerkonsole (Strg-Umschalt-I) wechseln, sehen wir die Ursache des Fehlers:

Dies ist ein Fehler aufgrund einer nicht autorisierten domänenübergreifenden Anfrage.
8.6.8.13. Fazit
Wir haben die folgende JS-Architektur implementiert:
![]() |
- Die Schichten sind recht klar voneinander getrennt;
- wir haben eine Single-Page-Anwendung (SPA). Dank dieser Eigenschaft können wir nun eine native App für verschiedene mobile Plattformen (Android, iOS, Windows Phone) generieren;
- wir haben ein Modell entwickelt, das in der Lage ist, asynchrone Aktionen parallel, sequenziell oder in einer Kombination aus beidem auszuführen;
8.6.9. Schritt 6: Erstellen einer nativen App für Android
Mit dem [Phonegap] [http://phonegap.com/]-Tool können Sie aus einer HTML/JS/CSS-Anwendung eine ausführbare Datei für mobile Geräte (Android, iOS, Windows 8 usw.) erstellen. Dazu gibt es verschiedene Möglichkeiten. Wir verwenden die einfachste Methode: ein Online-Tool, das auf der Phonegap-Website [http://build.phonegap.com/apps] verfügbar ist. Dieses Tool lädt die ZIP-Datei der zu konvertierenden statischen Website hoch. Die Startseite muss den Namen [index.html] tragen. Daher benennen wir die Seite [boot.html] in [index.html] um:
![]() |
Anschließend komprimieren wir den Ordner, in diesem Fall [rdvmedecins-client-js-03]. Als Nächstes rufen wir die Phonegap-Website [http://build.phonegap.com/apps] auf:
![]() |
- Bevor Sie mit [1] beginnen, müssen Sie möglicherweise ein Konto erstellen;
- unter [1] legen wir los;
- unter [2] wählen wir einen kostenlosen Tarif, der nur eine Phonegap-App zulässt;
![]() |
- unter [3] laden wir die komprimierte App [4] hoch;
![]() |
- in [5] benennen Sie die App;
- in [6] erstellen wir sie. Dies kann etwa 1 Minute dauern. Warten Sie, bis die Symbole für die verschiedenen mobilen Plattformen anzeigen, dass der Build abgeschlossen ist;
![]() |
- Es wurden nur die Binärdateien für Android [7] und Windows [8] generiert;
- Klicken Sie auf [7], um die Android-Binärdatei herunterzuladen;
![]() |
- in [9] die heruntergeladene [apk]-Binärdatei;
Starten Sie einen [GenyMotion]-Emulator für ein Android-Tablet (siehe Abschnitt 9.9):
![]() |
Oben starten wir einen Tablet-Emulator mit Android API 19. Sobald der Emulator gestartet ist,
- entsperren Sie ihn, indem Sie das Schloss (falls vorhanden) zur Seite ziehen und dann loslassen;
- Ziehen Sie die heruntergeladene Datei [PGBuildApp-debug.apk] mit der Maus auf den Emulator und legen Sie sie dort ab. Sie wird dann installiert und ausgeführt;
![]() |
Sie müssen die URL in [1] ändern. Geben Sie dazu in einem Eingabeaufforderungsfenster den Befehl [ipconfig] ein (Zeile 1 unten), wodurch die verschiedenen IP-Adressen Ihres Computers angezeigt werden:
C:\Users\Serge Tahé>ipconfig
Configuration IP de Windows
Carte réseau sans fil Connexion au réseau local* 15 :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
Carte Ethernet Connexion au réseau local :
Suffixe DNS propre à la connexion. . . : ad.univ-angers.fr
Adresse IPv6 de liaison locale. . . . .: fe80::698b:455a:925:6b13%4
Adresse IPv4. . . . . . . . . . . . . .: 172.19.81.34
Masque de sous-réseau. . . . . . . . . : 255.255.0.0
Passerelle par défaut. . . . . . . . . : 172.19.0.254
Carte réseau sans fil Wi-Fi :
Statut du média. . . . . . . . . . . . : Média déconnecté
Suffixe DNS propre à la connexion. . . :
...
Notieren Sie sich entweder die WLAN-IP-Adresse (Zeilen 6–9) oder die IP-Adresse des lokalen Netzwerks (Zeilen 11–17). Verwenden Sie diese IP-Adresse dann in der URL des Webservers:
![]() |
Sobald dies erledigt ist, stellen Sie eine Verbindung zum Webdienst her:
![]() |
Testen Sie die Anwendung auf dem Emulator. Sie sollte funktionieren. Auf der Serverseite können Sie CORS-Header in der Klasse [ApplicationModel] zulassen oder auch nicht:
// CORS
private final boolean CORS_ALLOWED=false;
Für die Android-App spielt dies keine Rolle. Sie läuft nicht in einem Browser. Die Anforderung an CORS-Header kommt vom Browser, nicht vom Server.
8.6.10. Fazit der Fallstudie
Wir haben die folgende Architektur entwickelt:
![]() |
Es handelt sich um eine komplexe 3-Schichten-Architektur. Sie wurde entwickelt, um die [Web2]-Schicht wiederzuverwenden, die die Serverschicht der [AngularJS-Spring MVC]-Anwendung aus dem Dokument [AngularJS / Spring 4 Tutorial] unter der URL [http://tahe.developpez.com/angularjs-spring4/] war. Dies ist der einzige Grund, warum wir eine dreischichtige Architektur haben. Während in der [AngularJS-Spring MVC]-Anwendung der Client von [Web2] ein [AngularJS]-Client war, ist der Client von [Web2] hier eine zweischichtige Architektur [jQuery] / [Spring MVC / Thymeleaf]. Da wir die Anzahl der Schichten erhöht haben, werden wir etwas an Leistung einbüßen.
Die hier besprochene Anwendung wurde im Laufe der Zeit in drei verschiedenen Dokumenten entwickelt:
- [Einführung in JSF2, PrimeFaces und PrimeFaces Mobile] unter der URL [http://tahe.developpez.com/java/primefaces/]. Die Fallstudie wurde dann unter Verwendung der JSF2-/PrimeFaces-Frameworks entwickelt. PrimeFaces ist eine Bibliothek mit AJAX-fähigen Komponenten, die das Schreiben von JavaScript überflüssig macht. Die damals entwickelte Anwendung war weniger komplex als die hier untersuchte. Sie verfügte über eine klassische Webversion für Computer und eine mobile Version für Smartphones;
- [AngularJS / Spring 4 Tutorial] unter der URL [http://tahe.developpez.com/angularjs-spring4/]. Die damals entwickelte Anwendung wies dieselben Funktionen auf wie die in diesem Dokument behandelte. Die Anwendung war zudem auf Android portiert worden;
- dieses Dokument;
Aus dieser Arbeit stechen für mich folgende Punkte hervor:
- Die [Primefaces]-Anwendung war bei weitem am einfachsten zu schreiben, und ihre mobile Webversion erwies sich als sehr leistungsfähig. Sie erfordert keinerlei JavaScript-Kenntnisse. Eine native Portierung auf die Betriebssysteme verschiedener mobiler Geräte ist nicht möglich, aber ist das überhaupt notwendig? Es scheint schwierig zu sein, den Stil der Anwendung zu ändern. Wir arbeiten nämlich mit Primefaces-Stylesheets. Dies könnte ein Nachteil sein;
- Die [AngularJS-Spring MVC]-Anwendung war komplex zu programmieren. Das [AngularJS]-Framework schien ziemlich schwer zu verstehen, sobald man es beherrschen wollte. Die Architektur [Angular-Client] / [Webservice / JSON, implementiert durch Spring MVC] ist besonders übersichtlich und leistungsstark. Diese Architektur lässt sich auf jede Webanwendung übertragen. Sie erscheint mir am vielversprechendsten, da sie unterschiedliche Fähigkeiten auf der Client- und Serverseite erfordert (JS+HTML+CSS auf der Clientseite, Java oder etwas anderes auf der Serverseite), was eine parallele Entwicklung von Client und Server ermöglicht;
- Für die in diesem Dokument entwickelte Anwendung, die eine 3-Schichten-Architektur [jQuery-Client] / [Web1-Server / Spring MVC / Thymeleaf] / [Web2-Server / Spring MVC] verwendet, könnte die Technologie [jQuery+Spring MVC+Thymeleaf] für manche leichter zu verstehen sein als die von [AngularJS]. Die [DAO]-Schicht des von uns geschriebenen JavaScript-Clients ist in anderen Anwendungen wiederverwendbar;

























































































































































































































































































