2. Der Spring 4-Server
![]() |
In der obigen Architektur befassen wir uns nun mit der Erstellung des Webdienstes / JSON, der mit dem Spring 4-Framework erstellt wurde. Wir werden dies in mehreren Schritten schreiben:
- zuerst die [Business]- und [DAO]-Schichten (Data Access Object). Hier verwenden wir Spring Data;
- dann den JSON-Webservice ohne Authentifizierung. Hier verwenden wir Spring MVC;
- anschließend fügen wir die Authentifizierungskomponente mit Spring Security hinzu.
Zunächst erläutern wir die Struktur der der Anwendung zugrunde liegenden Datenbank.
2.1. 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, die Termine verwalten, sind wie folgt:
![]() |
- Ein Zeitfenster gehört zu einem Arzt – ein Arzt hat 0 oder mehr Zeitfenster;
- Ein Termin verbindet sowohl einen Kunden als auch einen Arzt über den Zeitfenster des Arztes;
- Ein Kunde hat 0 oder mehr Termine;
- Ein Zeitfenster ist mit 0 oder mehr Terminen (an verschiedenen Tagen) verknüpft.
2.1.1. Die Tabelle [MEDECINS]
Sie enthält Informationen zu den Ärzten, die von der Anwendung [RdvMedecins] verwaltet werden.
![]() | ![]() |
- ID: 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: Nachname des Arztes
- FIRST_NAME: Vorname des Arztes
- TITLE: Anrede (Frau, Frau, Herr)
2.1.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
- TITEL: Anrede (Frau, Frau, Herr)
2.1.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 die Spalte DOCTORS(ID).
- START_TIME: Startzeit des Zeitfensters
- MSTART: Startminute des Zeitfensters
- HFIN: Endzeit des Zeitfensters
- MFIN: Endminuten 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.
2.1.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 wird – Fremdschlüssel auf das Feld [ID] der Tabelle [CLIENTS]
Diese Tabelle unterliegt einer 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.
2.2. Einführung in Spring Data
Wir werden die [DAO]-Schicht des Projekts mit Spring Data implementieren, einem Teil des Spring-Ökosystems.
![]() |
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.
2.2.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.0.2.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 gehören zum Hibernate-Ökosystem (hibernate, jboss), dessen JPA-Implementierung wir hier verwenden;
- wieder andere sind Testbibliotheken (JUnit, Hamcrest);
- wieder andere sind Logging-Bibliotheken (log4j, logback, slf4j);
Wir werden sie alle behalten. Für eine Produktionsanwendung sollten wir jedoch nur die beibehalten, die notwendig sind.
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.
2.2.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 werden nach den Klassenfeldern benannt: [id, firstName, lastName], wobei zu beachten ist, dass bei den Spaltennamen der Groß-/Kleinschreibung keine Bedeutung zukommt;
Beachten Sie, dass die verwendete JPA-Implementierung niemals benannt wird.
2.2.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: Die Methode
savewird verwendet, um eine EntitätTin der Datenbank zu speichern. Sie speichert die Entität unter Verwendung des Primärschlüssels, der ihr vom DBMS zugewiesen wurde. Sie ermöglicht es auch, eine EntitätTzu aktualisieren, die durch ihren Primärschlüsselididentifiziert wird. Die Wahl zwischen diesen beiden Aktionen hängt vom Wert des Primärschlüsselsidab: Ist dieser null, erfolgt die Speichervorgang; andernfalls erfolgt die Aktualisierung; - Zeile 10: Wie oben, jedoch für eine Liste von Entitäten;
- Zeile 12: Die Methode
findOneruft eine EntitätTab, die durch ihren Primärschlüsselididentifiziert wird; - Zeile 22: Mit der Methode
deletekönnen Sie eine EntitätTlöschen, die durch ihren Primärschlüsselididentifiziert 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] sich auf den JPA-Persistenzkontext bezieht. 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.
2.2.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 keine dieser Variablen 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 im Klassenpfad gefundenen Bibliotheken zu konfigurieren. Da sich die Hibernate-Bibliotheken im Klassenpfad befinden, wird die Bean [entityManagerFactory] unter Verwendung von Hibernate implementiert. Da sich die H2-DBMS-Bibliothek im Klassenpfad befindet, wird die Bean [dataSource] unter Verwendung von 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 1: Die statische Methode [run] der Klasse [SpringApplication] im Spring Boot-Projekt wird ausgeführt. Ihr Parameter ist die Klasse, die eine Annotation [Configuration] oder [EnableAutoConfiguration] trägt. 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, das 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 12: [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 22–24: Die Tabelle [CUSTOMER] wird erstellt. 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–32: Hibernate-Protokolle, die das Einfügen von Zeilen in die Tabelle [CUSTOMER] anzeigen. Das bedeutet, dass Hibernate so konfiguriert wurde, dass Protokolle generiert werden;
- Zeilen 35–39: die fünf eingefügten Kunden;
- Zeilen 42–44: Ergebnis der Methode [findOne] der Schnittstelle;
- Zeilen 47–50: Ergebnisse der Methode [findByLastName];
- Zeilen 51 ff.: Protokolle vom Schließen des Spring-Kontexts.
2.2.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.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<!-- Spring transactions -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.0.5.RELEASE</version>
</dependency>
<!-- Spring Data -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>1.5.2.RELEASE</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<version>1.0.2.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>
- Zeilen 3–17: Spring-Kernbibliotheken;
- Zeilen 19–28: Spring-Bibliotheken zur Verwaltung von Datenbanktransaktionen;
- Zeilen 30–34: Spring Data für den Zugriff auf die Datenbank;
- Zeilen 36–40: Spring Boot zum Starten der Anwendung;
- Zeilen 48–52: das H2-DBMS;
- Zeilen 54–63: Datenbanken werden häufig mit offenen Verbindungspools verwendet, wodurch das wiederholte Öffnen und Schließen von Verbindungen vermieden wird. Hier kommt die Implementierung von [commons-dbcp] zum Einsatz;
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: Die Annotation [@ComponentScan] ermöglicht es Ihnen, die Verzeichnisse aufzulisten, in denen nach Spring-Komponenten gesucht werden soll. Spring-Komponenten sind Klassen, die mit Spring-Annotationen wie @Service, @Component, @Controller usw. versehen sind. Hier gibt es keine anderen als die in der [Config]-Klasse definierten, daher 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 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, besteht keine Abhängigkeit mehr von Spring Boot.
Die Ausführung liefert die gleichen Ergebnisse wie zuvor.
2.2.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:
2.2.7. Erstellen Sie ein neues Spring Data-Projekt
Um eine Spring Data-Projektvorlage zu erstellen, gehen Sie wie folgt vor:
![]() |
- Erstellen Sie unter [1] ein neues Projekt;
- Wählen Sie unter [2] die Option [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.1.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 profitiert somit von allen in dieser Datei definierten Beans;
- 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.
2.3. Das Eclipse-Serverprojekt
![]() |
![]() |
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;
2.4. 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.0.0.RELEASE</version>
</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>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
</dependency>
<dependency>
<groupId>commons-pool</groupId>
<artifactId>commons-pool</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<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>istia.st.spring.data.main.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<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 14–17: für Spring Data;
- Zeilen 18–22: für JUnit-Tests;
- Zeilen 23–26: JDBC-Treiber für das DBMS MySQL5;
- Zeilen 27–34: Commons DBCP-Verbindungspool;
- Zeilen 35–38: Jackson-Bibliothek für die JSON-Verarbeitung;
- Zeilen 39–43: Google Collections-Bibliothek;
Version 1.1.0.RC1 von [spring-boot-starter-parent] verwendet die folgenden Bibliotheksversionen:
2.5. JPA-Entitäten
![]() |
JPA-Entitäten sind die Objekte, die die Zeilen in den 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.AUTO)
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)) {
return false;
}
AbstractEntity other = (AbstractEntity) entity;
return this.id == other.id;
}
// 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. Durch die Annotation [@Id] wird das Feld [id] zum Primärschlüssel. Die Annotation [@GeneratedValue(strategy = GenerationType.AUTO)] gibt an, dass der Wert dieses Primärschlüssels vom DBMS generiert wird und dass kein Generierungsmodus erzwungen wird;
- 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: Es wird eine Ausnahme ausgelöst, da sich die Version (V1) von der in der Datenbank (V1+1) unterscheidet;
- 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 neu definiert: Zwei Entitäten gelten als gleich, wenn sie denselben Klassennamen und dieselbe ID-Kennung haben;
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: Endzeit des Zeitfensters (14);
- Zeile 19: Endminuten des Zeitfensters (40);
- Zeilen 22–24: der Arzt, dem der Termin 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-Zeiten) zu einem (Arzt). Das Attribut [fetch=FetchType.LAZY] legt fest, dass, wenn eine [Creneau]-Entität aus dem Persistenzkontext angefordert wird und aus der Datenbank abgerufen werden muss, die [Medecin]-Entität nicht mit abgerufen 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, was bedeutet, dass die Spalte nur gelesen werden kann;
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];
2.6. 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 die JPA-Entitäten [Client];
- [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> {
// list of physician slots
@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] wird verwendet, um 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 durch den Fremdschlüssel referenzierten Tabelle erfordert, um die referenzierte Entität abzurufen;
2.7. Die [business]-Schicht
![]() |
![]() |
- [IMetier] ist die Schnittstelle der [business]-Schicht und [Metier] ist deren Implementierung;
- [DoctorDailySchedule] und [DoctorDailySlot] sind zwei Geschäftsentitäten;
2.7.1. Die Entitäten
Die Entität [DoctorTimeSlot] 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;
2.7.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 to the list
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. Sie können 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 Informationen können wir feststellen, ob ein Zeitfenster verfügbar oder gebucht ist;
2.8. Projektkonfiguration
![]() |
Die Klasse [DomainAndPersistenceConfig] konfiguriert das gesamte Projekt:
package rdvmedecins.config;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.orm.jpa.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@EnableJpaRepositories(basePackages = { "rdvmedecins.repositories" })
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins" })
@EntityScan(basePackages = { "rdvmedecins.entities" })
@EnableTransactionManagement
public class DomainAndPersistenceConfig {
// 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;
}
// provider JPA - not required if you're happy with the default values used by Spring boot
// here we define it to enable / disable logs SQL
@Bean
public JpaVendorAdapter jpaVendorAdapter() {
HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
hibernateJpaVendorAdapter.setShowSql(false);
hibernateJpaVendorAdapter.setGenerateDdl(false);
hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
return hibernateJpaVendorAdapter;
}
// the EntityManagerFactory and TransactionManager are defined with default values by Spring boot
}
- Zeile 45: Wir definieren die Beans [EntityManagerFactory] und [TransactionManager] nicht. Stattdessen nutzen wir die Annotation [@EnableAutoConfiguration] von Spring Boot (Zeile 17);
- Zeilen 24–32: Definieren Sie die MySQL-5-Datenquelle. Dies ist ein Bean, das Spring Boot in der Regel nicht automatisch konfigurieren kann;
- Zeilen 36–43: Wir konfigurieren außerdem die JPA-Implementierung so, dass das [showSql]-Attribut von Hibernate auf „false“ gesetzt wird (Zeile 39). Standardmäßig ist es auf „true“ gesetzt;
- Derzeit sind die einzigen von Spring verwalteten Komponenten die Beans in den Zeilen 25 und 37 sowie die Beans [EntityManagerFactory] und [TransactionManager] über die automatische Konfiguration. Wir müssen die Beans aus den Schichten [business] und [DAO] hinzufügen;
- In Zeile 16 werden die Schnittstellen aus dem Paket [rdvmdecins.repositories], die von der Schnittstelle [CrudRepository] erben, zum Spring-Kontext hinzugefügt;
- Zeile 18 fügt dem Spring-Kontext alle Klassen im Paket [rdvmedecins] und deren Unterklassen hinzu, die eine Spring-Annotation aufweisen. Im Paket [rdvmdecins.metier] wird die Klasse [Metier] mit ihrer Annotation [@Service] gefunden und dem Spring-Kontext hinzugefügt;
- Zeile 45: Eine [entityManagerFactory]-Bean wird standardmäßig von Spring Boot definiert. Wir müssen dieser Bean mitteilen, wo sich die JPA-Entitäten befinden, die sie verwalten soll. Dies geschieht in Zeile 19;
- Zeile 20: legt fest, dass die Methoden von Schnittstellen, die von der Schnittstelle [CrudRepository] erben, innerhalb einer Transaktion ausgeführt werden müssen;
2.9. 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
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: neuen Termin hinzufügen. Die Methode [addAppt] gibt den Termin mit zusätzlichen Informationen zurück, darunter seine 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 hier, 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: Wir rufen den gelöschten Termin aus der Datenbank ab;
- 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:
![]() |
2.10. 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:
2.11. Einführung in Spring MVC
![]() |
Wir werden uns nun mit dem Aufbau der Webschicht befassen. Diese Schicht besteht in erster Linie aus Methoden, die bestimmte URLs verarbeiten und mit einer Textzeile im JSON-Format (JavaScript Object Notation) antworten. Diese Webschicht ist eine Webschnittstelle, die manchmal auch als Web-API bezeichnet wird. Wir werden diese Schnittstelle mit Spring MVC implementieren, einer weiteren Komponente des Spring-Ökosystems. Wir beginnen mit der Durchsicht eines der Leitfäden, die unter [http://spring.io] zu finden sind.
2.11.1. Das Demo-Projekt
![]() |
- in [1] importieren wir eines der Spring-Handbücher;
![]() |
- in [2] wählen wir das Beispiel [Rest Service] aus;
- in [3] wählen wir das Maven-Projekt aus;
- unter [4] wählen wir die endgültige Version des Leitfadens aus;
- in [5] bestätigen wir;
- in [6] das importierte Projekt;
Webdienste, auf die über Standard-URLs zugegriffen werden kann und die JSON-Text zurückgeben, werden oft als REST-Dienste (REpresentational State Transfer) bezeichnet. In diesem Dokument werde ich den Dienst, den wir erstellen werden, einfach als Web-/JSON-Dienst bezeichnen. Ein Dienst gilt als RESTful, wenn er bestimmte Regeln befolgt. Ich habe nicht versucht, mich an diese Regeln zu halten.
Betrachten wir nun das importierte Projekt, beginnend mit seiner Maven-Konfiguration.
2.11.2. Maven-Konfiguration
Die Datei [pom.xml] sieht wie folgt aus:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-releases</id>
<url>http://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-releases</id>
<url>http://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
</project>
- Zeilen 10–14: Wie im [Spring Data]-Projekt ist das übergeordnete [Spring Boot]-Projekt vorhanden;
- Zeilen 17–20: Das Artefakt [spring-boot-starter-web] enthält die für ein Spring-MVC-Projekt erforderlichen Bibliotheken. Insbesondere enthält es einen eingebetteten Tomcat-Server. Die Anwendung wird auf diesem Server ausgeführt;
- Zeilen 21–24: Die Jackson-Bibliothek verarbeitet JSON: Sie konvertiert ein Java-Objekt in eine JSON-Zeichenkette und umgekehrt;
Diese Konfiguration umfasst eine große Anzahl von Bibliotheken:
![]() | ![]() |
Oben sehen wir die drei Tomcat-Server-Archive.
2.11.3. Die Architektur eines Spring-REST-Dienstes
Spring MVC implementiert das MVC-Architekturmuster (Model–View–Controller) wie folgt:
![]() |
Die Verarbeitung einer Client-Anfrage verläuft wie folgt:
- Anfrage – die angeforderten URLs haben die Form http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... Das [Dispatcher-Servlet] ist die Spring-Klasse, die eingehende URLs verarbeitet. Es „leitet“ die URL an die Aktion weiter, die sie bearbeiten soll. Diese Aktionen sind Methoden bestimmter Klassen, die als [Controller] bezeichnet werden. Das C in MVC steht hier für die Kette [Dispatcher-Servlet, Controller, Aktion]. Wenn keine Aktion für die Bearbeitung der eingehenden URL konfiguriert wurde, antwortet das [Dispatcher-Servlet], dass die angeforderte URL nicht gefunden wurde (404 NOT FOUND-Fehler);
- Bei der Verarbeitung
- Die ausgewählte Aktion kann die Parameter verwenden, die ihr vom [Dispatcher Servlet] übergeben wurden. Diese können aus verschiedenen Quellen stammen:
- dem Pfad [/param1/param2/...] der URL,
- die URL-Parameter [p1=v1&p2=v2],
- aus Parametern, die der Browser mit seiner Anfrage übermittelt hat;
- Bei der Verarbeitung der Benutzeranfrage benötigt die Aktion möglicherweise die [Business]-Schicht [2b]. Sobald die Anfrage des Clients verarbeitet wurde, kann dies verschiedene Antworten auslösen. Ein klassisches Beispiel ist:
- eine Fehlerseite, wenn die Anfrage nicht korrekt verarbeitet werden konnte
- ansonsten eine Bestätigungsseite
- die Aktion weist an, eine bestimmte Ansicht anzuzeigen [3]. Diese Ansicht zeigt Daten an, die als View-Modell bezeichnet werden. Dies ist das M in MVC. Die Aktion erstellt dieses M-Modell [2c] und weist an, eine V-Ansicht anzuzeigen [3];
- Antwort – die ausgewählte Ansicht V verwendet das von der Aktion erstellte Modell M, um die dynamischen Teile der HTML-Antwort zu initialisieren, die sie an den Client senden muss, und sendet dann diese Antwort.
Bei einem Webdienst / JSON wird die vorstehende Architektur leicht modifiziert:
![]() |
- In [4a] wird das Modell, bei dem es sich um eine Java-Klasse handelt, durch eine JSON-Bibliothek in eine JSON-Zeichenkette umgewandelt;
- in [4b] wird diese JSON-Zeichenkette an den Browser gesendet;
2.11.4. Der C-Controller
![]() |
Die importierte Anwendung verfügt über den folgenden Controller:
package hello;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@RequestMapping("/greeting")
public @ResponseBody
Greeting greeting(@RequestParam(value = "name", required = false, defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
- Zeile 9: Die Annotation [@Controller] macht die Klasse [GreetingController] zu einem Spring-Controller, was bedeutet, dass ihre Methoden für die Verarbeitung von URLs registriert sind;
- Zeile 15: Die Annotation [@RequestMapping] gibt die von der Methode verarbeitete URL an, in diesem Fall die URL [/greeting]. Wir werden später sehen, dass diese URL parametrisiert werden kann und dass es möglich ist, diese Parameter abzurufen;
- Zeile 16: Die Annotation [@ResponseBody] gibt an, dass die Methode keine Vorlage für eine Ansicht (JSP, JSF, Thymeleaf usw.) generiert, die an den Browser des Clients gesendet wird, sondern stattdessen die Antwort an den Browser selbst generiert. Hier erzeugt sie ein Objekt vom Typ [Greeting] (Zeile 18). Auch wenn dies hier nicht sofort ersichtlich ist, wird dieses Objekt zunächst in JSON konvertiert, bevor es an den Browser gesendet wird. Das Vorhandensein einer JSON-Bibliothek in den Projektabhängigkeiten bewirkt, dass Spring Boot das Projekt automatisch auf diese Weise konfiguriert;
- Zeile 17: Die Methode [greeting] hat einen Parameter [String name]. Die Annotation [@RequestParam(value = "name", required = false, defaultValue = "World"] gibt an, dass dieser Parameter mit einem Parameter namens [name] initialisiert werden muss (@RequestParam(value = "name"). Dies kann ein GET- oder POST-Parameter sein. Dieser Parameter ist nicht erforderlich (required = false). In diesem Fall wird der Parameter [name] der Methode mit dem Wert [World] initialisiert (defaultValue = "World").
2.11.5. Das M-Modell
Das durch die vorherige Methode erzeugte M-Modell ist das folgende [Greeting]-Objekt:
![]() |
package hello;
public class Greeting {
private final long id;
private final String content;
public Greeting(long id, String content) {
this.id = id;
this.content = content;
}
public long getId() {
return id;
}
public String getContent() {
return content;
}
}
Die JSON-Umwandlung dieses Objekts erzeugt die Zeichenfolge {"id":n,"content":"text"}. Letztendlich hat die von der Controller-Methode erzeugte JSON-Zeichenfolge folgende Form:
oder
2.11.6. Projektkonfiguration
![]() |
Das Projekt wird durch die folgende [Application]-Klasse konfiguriert:
package hello;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- Zeile 11: Interessanterweise ist diese Klasse mit einer für Konsolenanwendungen spezifischen [main]-Methode ausführbar. Dies ist tatsächlich der Fall. Die [SpringApplication]-Klasse in Zeile 12 startet den in den Abhängigkeiten vorhandenen Tomcat-Server und stellt den REST-Dienst darauf bereit;
- Zeile 4: Wir sehen, dass die Klasse [SpringApplication] zum Projekt [Spring Boot] gehört;
- Zeile 12: Der erste Parameter ist die Klasse, die das Projekt konfiguriert, der zweite enthält etwaige zusätzliche Parameter;
- Zeile 8: Die Annotation [@EnableAutoConfiguration] weist Spring Boot an, das Projekt zu konfigurieren;
- Zeile 7: Die Annotation [@ComponentScan] bewirkt, dass das Verzeichnis, das die [Application]-Klasse enthält, nach Spring-Komponenten durchsucht wird. Es wird eine gefunden: die [GreetingController]-Klasse, die die Annotation [@Controller] trägt und somit eine Spring-Komponente ist;
2.11.7. Ausführen des Projekts
Führen wir das Projekt aus:
![]() |
Wir erhalten die folgenden Konsolenprotokolle:
____ _ __ _ _
- Zeile 12: Der Tomcat-Server startet auf Port 8080 (Zeile 11);
- Zeile 16: Das Servlet [DispatcherServlet] ist vorhanden;
- Zeile 19: Die Methode [GreetingController.greeting] wurde gefunden;
Um die Webanwendung zu testen, rufen wir die URL [http://localhost:8080/greeting] auf:
![]() | ![]() |
Wir erhalten die erwartete JSON-Zeichenkette. Es könnte interessant sein, die vom Server gesendeten HTTP-Header anzusehen. Dazu verwenden wir das Chrome-Plugin namens [Advanced Rest Client] (siehe Anhang):
![]() |
- in [1] die angeforderte URL;
- in [2] wird die GET-Methode verwendet;
- in [3] die JSON-Antwort;
- in [4] hat der Server angegeben, dass er eine Antwort im JSON-Format sendet;
- in [5] fordern wir dieselbe URL an, diesmal jedoch mit einer POST-Anfrage;
- in [7] werden die Informationen im [urlencoded]-Format an den Server gesendet;
- in [6] der Parameter „name“ mit seinem Wert;
- in [8] teilt der Browser dem Server mit, dass er [urlencoded]-Informationen sendet;
- in [9] die JSON-Antwort des Servers;
2.11.8. Erstellen eines ausführbaren Archivs
Es ist möglich, ein ausführbares Archiv außerhalb von Eclipse zu erstellen. Die erforderliche Konfiguration befindet sich in der Datei [pom.xml]:
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<start-class>istia.st.Application</start-class>
<java.version>1.7</java.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- Die Zeilen 9–12 definieren das Plugin, das das ausführbare Archiv erstellt;
- Zeile 3 definiert die ausführbare Klasse des Projekts;
So gehen Sie vor:
![]() |
- in [1]: Führen Sie ein Maven-Ziel aus;
- in [2]: Es gibt zwei Ziele: [clean], um den Ordner [target] aus dem Maven-Projekt zu löschen, und [package], um ihn neu zu generieren;
- in [3]: Der generierte Ordner [target] befindet sich in diesem Ordner;
- in [4]: Das Ziel wird generiert;
In den Protokollen, die in der Konsole angezeigt werden, ist es wichtig, das Plugin [spring-boot-maven-plugin] zu sehen. Dies ist das Plugin, das das ausführbare Archiv generiert.
Navigieren Sie über die Konsole zum generierten Ordner:
- Zeile 5: das generierte Archiv;
Dieses Archiv wird wie folgt ausgeführt:
Nachdem die Webanwendung nun ausgeführt wird, können Sie über einen Browser darauf zugreifen:
![]() |
2.11.9. Bereitstellung der Anwendung auf einem Tomcat-Server
Während Spring Boot im Entwicklungsmodus sehr praktisch ist, wird eine Produktionsanwendung wahrscheinlich auf einem echten Tomcat-Server bereitgestellt. So geht’s:
Ändern Sie die Datei [pom.xml] wie folgt:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.springframework</groupId>
<artifactId>gs-rest-service</artifactId>
<version>0.1.0</version>
<packaging>war</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<start-class>hello.Application</start-class>
</properties>
....
</project>
Änderungen müssen an zwei Stellen vorgenommen werden:
- Zeile 9: Sie müssen angeben, dass Sie eine WAR-Datei (Web Archive) generieren möchten;
- Zeilen 26–30: Sie müssen eine Abhängigkeit zum Artefakt [spring-boot-starter-tomcat] hinzufügen. Dieses Artefakt fügt alle Tomcat-Klassen zu den Abhängigkeiten des Projekts hinzu;
- Zeile 29: Dieses Artefakt ist [provided], was bedeutet, dass die entsprechenden Archive nicht in die generierte WAR-Datei aufgenommen werden. Stattdessen befinden sich diese Archive auf dem Tomcat-Server, auf dem die Anwendung ausgeführt wird;
Sie müssen außerdem die Webanwendung konfigurieren. Wenn keine [web.xml]-Datei vorhanden ist, erfolgt dies über eine Klasse, die [SpringBootServletInitializer] erweitert:
![]() |
Die Klasse [ApplicationInitializer] sieht wie folgt aus:
package hello;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class ApplicationInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
}
- Zeile 6: Die Klasse [ApplicationInitializer] erweitert die Klasse [SpringBootServletInitializer];
- Zeile 9: Die Methode [configure] wird überschrieben (Zeile 8);
- Zeile 10: Die Klasse, die das Projekt konfiguriert, wird bereitgestellt;
Um das Projekt auszuführen, gehen Sie wie folgt vor:
![]() |
- Führen Sie in [1] das Projekt auf einem der in der Eclipse-IDE registrierten Server aus;
- Wählen Sie unter [2] die Option [tc Server Developer] aus, die als Standardoption voreingestellt ist. Dabei handelt es sich um eine Variante von Tomcat;
Sobald dies erledigt ist, können Sie die URL [http://localhost:8080/gs-rest-service/greeting/?name=Mitchell] in einen Browser eingeben:
![]() |
Wir wissen nun, wie man ein WAR-Archiv erstellt. Im weiteren Verlauf werden wir uns weiterhin mit Spring Boot und dessen ausführbarem JAR-Archiv beschäftigen.
2.11.10. Ein neues Webprojekt erstellen
Um ein neues Webprojekt zu erstellen, gehen Sie wie folgt vor:
![]() |
- in [1]: Datei / Neu / Spring Starter-Projekt
- in [2]: Wählen Sie [Web]. Wählen Sie keine View-Bibliotheken aus, da es in einem Webservice/JSON keine Views gibt;
- Das erstellte Projekt ist ein Maven-Projekt. Geben Sie in [3] den Gruppennamen für das zu erstellende Maven-Artefakt ein; geben Sie in [4] den Artefaktnamen ein;
- Geben Sie in [5] den Namen eines Pakets ein, in dem Spring die Konfigurationsklasse des Projekts ablegen wird;
- in [6] geben Sie dem Eclipse-Projekt einen Namen – dieser kann sich von [4] unterscheiden;
![]() |
2.12. Die [Web]-Ebene
![]() |
![]() |
Wir werden die Webschicht in mehreren Schritten erstellen:
- Schritt 1: eine funktionsfähige Webschicht 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 gleichen Domain wie unser Webdienst gehört. Standardmäßig kann er nicht auf den Webdienst zugreifen, es sei denn, der Webdienst autorisiert ihn dazu. Wir werden sehen, wie das geht;
2.12.1. Maven-Konfiguration
Die [pom.xml]-Datei des Projekts sieht wie folgt aus:
<modelVersion>4.0.0</modelVersion>
<groupId>istia.st.spring4.mvc</groupId>
<artifactId>rdvmedecins-webapi-v1</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rdvmedecins-webapi-v1</name>
<description>Gestion de RV Médecins</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.0.0.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>istia.st.spring4.rdvmedecins</groupId>
<artifactId>rdvmedecins-metier-dao</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- Zeilen 7–11: das übergeordnete Maven-Projekt;
- Zeilen 13–16: Abhängigkeiten für ein Spring-MVC-Projekt;
- Zeilen 17–21: Abhängigkeiten von den Schichten [Geschäftslogik, DAO, JPA];
2.12.2. Die Webservice-Schnittstelle
![]() |
- In [1] 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;
public class Reponse {
// ----------------- properties
// operation status
private int status;
// the answer JSON
private Object data;
// ---------------constructeurs
public Reponse() {
}
public Reponse(int status, Object data) {
this.status = status;
this.data = data;
}
// methods
public void incrStatusBy(int increment) {
status += increment;
}
// ----------------------getters and setters
...
}
- Zeile 7: Antwort-Fehlercode 0: OK, alles andere: KO;
- Zeile 9: der Antworttext;
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.
![]() |
- in [0] die URL des Webdienstes;
- in [1] wird die POST-Methode verwendet;
- in [2] der JSON-Text der an den Webdienst gesendeten Informationen in der Form {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 [data] enthält die JSON-Darstellung des hinzugefügten Termins;
Das Vorhandensein des neuen Termins kann überprüft werden:
![]() |
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:
![]() |
Wie oben zu sehen ist, wird der Termin für die Patientin [Frau GERMAN] nicht mehr angezeigt.
Der Webdienst ermöglicht es Ihnen auch, Entitäten anhand ihrer ID abzurufen:
![]() |
![]() |
![]() |
![]() |
Alle diese URLs werden vom [RdvMedecinsController]-Controller verarbeitet, den wir nun vorstellen werden.
2.12.3. Das Grundgerüst des [ RdvMedecinsController]-Controllers
![]() |
Der Controller [RdvMedecinsController] sieht wie folgt aus:
package rdvmedecins.web.controllers;
import java.text.ParseException;
...
@RestController
public class RdvMedecinsController {
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// application error messages
messages = application.getMessages();
}
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
...
}
// customer list
@RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
public Reponse getAllClients() {
...
}
// list of physician slots
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
...
}
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
@RequestMapping(value = "/getClientById/{id}", method = RequestMethod.GET)
public Reponse getClientById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getRvById/{id}", method = RequestMethod.GET)
public Reponse getRvById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/getCreneauById/{id}", method = RequestMethod.GET)
public Reponse getCreneauById(@PathVariable("id") long id) {
...
}
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
...
}
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
...
}
@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getAgendaMedecinJour(
@PathVariable("idMedecin") long idMedecin,
@PathVariable("jour") String jour) {
...
}
}
- Zeile 6: Die Annotation [@RestController] macht die Klasse [RdvMedecinsController] zu einem Spring-Controller. Außerdem stellt sie sicher, dass Methoden, die URLs verarbeiten, eine Antwort generieren, die automatisch in JSON konvertiert wird;
- Zeilen 9–10: Hier wird von Spring ein Objekt vom Typ [ApplicationModel] injiziert;
- Zeile 13: 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;
- Alle Methoden geben wie folgt ein Objekt vom Typ [Response] zurück:
package rdvmedecins.web.models;
public class Reponse {
// ----------------- properties
// operation status
private int status;
// the answer
private Object data;
...
}
Dieses Objekt wird in JSON serialisiert, bevor es an den Browser des Clients gesendet wird;
- Zeile 20: Die Annotation [@RequestMapping] legt die Bedingungen für den Aufruf der Methode fest. Hier verarbeitet die Methode eine GET-Anfrage von der URL [/getAllMedecins]. Würde diese URL über einen POST-Request aufgerufen, würde sie abgelehnt und Spring MVC würde einen HTTP-Fehlercode an den Web-Client senden;
- Zeile 32: Die URL ist mit {idMedecin} konfiguriert. Dieser Parameter wird mithilfe der Annotation [@PathVariable] in Zeile 33 abgerufen;
- Zeile 33: Der einzelne 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 65: 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 65) führt in Verbindung mit der Tatsache, dass die Methode JSON erwartet [consumes = "application/json; charset=UTF-8"] (Zeile 64), dazu, dass die vom Web-Client gesendete JSON-Zeichenkette in ein Objekt vom Typ [PostAjouter] deserialisiert wird. Dieses Objekt ist wie folgt definiert:
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 69–70 enthalten einen ähnlichen Mechanismus für die URL [/deleteRv]. 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
...
}
2.12.4. Webdienstmodelle
![]() |
Wir haben bereits die Modelle [Response, PostAddAppointment, PostDeleteAppointment] vorgestellt. Das Modell [ApplicationModel] sieht wie folgt aus:
package rdvmedecins.web.models;
import java.util.Date;
...
@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;
// error messages
private List<String> messages;
@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(Rv rv) {
métier.supprimerRv(rv);
}
@Override
public AgendaMedecinJour getAgendaMedecinJour(long idMedecin, Date jour) {
return métier.getAgendaMedecinJour(idMedecin, jour);
}
}
- Zeile 6: 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 7: Die Klasse [ApplicationModel] implementiert die Schnittstelle [IMetier];
- Zeilen 10–11: Eine Referenz auf die [business]-Schicht wird von Spring injiziert;
- Zeile 19: Die Annotation [@PostConstruct] stellt sicher, dass die Methode [init] unmittelbar nach der Instanziierung der Klasse [ApplicationModel] ausgeführt wird;
- Zeilen 23–24: Die Listen der Ärzte und Kunden werden aus der [business]-Schicht abgerufen;
- Zeile 26: Wenn eine Ausnahme auftritt, speichern wir die Meldungen aus dem Ausnahmestapel im Feld in Zeile 17;
Die Klasse [ApplicationModel] erfüllt zwei Zwecke:
- als Cache zum Speichern der Listen der Ärzte und Patienten (Kunden);
- als einheitliche Schnittstelle für die Controller;
Die Architektur der Webschicht entwickelt sich wie folgt:
![]() |
- In [2b] kommunizieren die Methoden des/der Controller(s) mit dem [ApplicationModel]-Singleton;
Diese Strategie bietet Flexibilität 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.
2.12.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.text.SimpleDateFormat;
...
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;
}
// mappers Object --> Map
// --------------------------------------------------------
....
}
- 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 Ausnahmen [exception.getCause()] enthält.
Die [Static]-Klasse enthält weitere Hilfsmethoden, auf die wir zurückkommen werden, sobald wir ihnen begegnen.
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];
![]() |
2.12.6. Die [init]-Methode des Controllers
Der Controller [RdvMedecinsController] (siehe Abschnitt 2.12.3) verfügt über eine [init]-Methode, die unmittelbar nach seiner Instanziierung ausgeführt wird:
@Autowired
private ApplicationModel application;
private List<String> messages;
@PostConstruct
public void init() {
// application error messages
messages = application.getMessages();
}
- Zeile 8: Die im Anwendungscache [ApplicationModel] gespeicherten Fehlermeldungen werden lokal im Feld in Zeile 3 gespeichert. Dadurch können die Methoden feststellen, ob die Anwendung korrekt initialisiert wurde.
2.12.7. Die URL [/getAllMedecins]
Die URL [/getAllDoctors] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
// list of doctors
@RequestMapping(value = "/getAllMedecins", method = RequestMethod.GET)
public Reponse getAllMedecins() {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// list of doctors
try {
return new Reponse(0, application.getAllMedecins());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
- Zeile 5: Wir prüfen, ob die Anwendung korrekt initialisiert wurde (messages == null). Ist dies nicht der Fall, geben wir eine Antwort mit status = -1 und data = messages zurück;
- Zeile 10: Andernfalls geben wir die Liste der Ärzte mit dem Status 0 zurück. 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 nicht ordnungsgemäß initialisiert werden konnte, noch nicht dargestellt. Stoppen wir das MySQL5-DBMS, starten wir den Webdienst und rufen wir dann die URL [/getAllMedecins] auf:

Tatsächlich erhalten wir eine Fehlermeldung. Unter normalen Umständen erhalten wir die folgende Ansicht:
![]() |
2.12.8. Die URL [/getAllClients]
Die URL [/getAllClients] wird von der folgenden Methode im [RdvMedecinsController] verarbeitet:
// customer list
@RequestMapping(value = "/getAllClients")
public Reponse getAllClients() {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// customer list
try {
return new Reponse(0, application.getAllClients());
} catch (Exception e) {
return new Reponse(1, Static.getErreursForException(e));
}
}
Dies ähnelt der Methode [getAllMedecins], die wir bereits behandelt haben. Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
2.12.9. Die URL [/getAllSlots/{doctorId}]
Die URL [/getAllSlots/{doctorId}] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
// list of physician slots
@RequestMapping(value = "/getAllCreneaux/{idMedecin}", method = RequestMethod.GET)
public Reponse getAllCreneaux(@PathVariable("idMedecin") long idMedecin) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// we get the doctor back
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// doctor's slots
List<Creneau> créneaux = null;
try {
créneaux = application.getAllCreneaux(médecin.getId());
} catch (Exception e1) {
return new Reponse(3, Static.getErreursForException(e1));
}
// we return the answer
return new Reponse(0, Static.getListMapForCreneaux(créneaux));
}
- Zeile 9: Der durch den Parameter [id] identifizierte Arzt wird von einer lokalen Methode angefordert:
private Reponse getMedecin(long id) {
// we get the doctor back
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// existing doctor?
if (médecin == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, médecin);
}
Diese Methode gibt einen Statuswert im Bereich [0,1,2] zurück. Kehren wir zum Code für die Methode [getAllSlots] zurück:
- Zeilen 10–12: Wenn status ≠ 0, wird die Antwort sofort zurückgegeben;
- Zeile 13: Wir rufen den Arzt ab;
- Zeile 17: Rufen die Zeitfenster dieses Arztes ab;
- Zeile 22: Wir geben ein Objekt [Static.getListMapForCreneaux(slots)] als Antwort zurück;
Sehen wir uns die Definition der Klasse [Creneau] noch einmal an:
@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 Terminfenster des Arztes zurück, wobei der Arzt in jedem einzelnen enthalten ist. Wenn wir diese Terminfenster in JSON serialisieren, erscheint die JSON-Zeichenkette des Arztes in jedem einzelnen. Dies ist unnötig. Anstatt also ein [Creneau]-Objekt zu serialisieren, werden wir ein [Map]-Objekt serialisieren, das nur die gewünschten Felder enthält.
Kehren wir zu dem Code zurück, den wir uns zuvor angesehen haben:
// we return the answer
return new Reponse(0, Static.getListMapForCreneaux(créneaux));
Die Methode [Static.getListMapForCreneaux] sieht wie folgt aus:
// List<Creneau> --> List<Map>
public static List<Map<String, Object>> getListMapForCreneaux(List<Creneau> créneaux) {
// liste de dictionnaires <String,Object>
List<Map<String, Object>> liste = new ArrayList<Map<String, Object>>();
for (Creneau créneau : créneaux) {
liste.add(Static.getMapForCreneau(créneau));
}
// on rend la liste
return liste;
}
und die Methode [Static.getMapForCreneau] lautet wie folgt:
// Creneau --> Map
public static Map<String, Object> getMapForCreneau(Creneau créneau) {
// qq chose à faire ?
if (créneau == null) {
return null;
}
// dictionnaire <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", créneau.getId());
hash.put("hDebut", créneau.getHdebut());
hash.put("mDebut", créneau.getMdebut());
hash.put("hFin", créneau.getHfin());
hash.put("mFin", créneau.getMfin());
// on rend le dictionnaire
return hash;
}
- Zeile 8: Wir erstellen ein Dictionary;
- Zeilen 9–13: Wir fügen die Felder hinzu, die wir in der JSON-Zeichenkette behalten möchten. Das Feld [doctor] ist nicht enthalten;
- Zeile 15: Dieses Wörterbuch zurückgeben;
Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
oder diese, falls das Zeitfenster nicht existiert:
![]() |
oder diese, falls beim Zugriff auf die Datenbank ein Fehler auftritt:
![]() |
2.12.10. Die URL [/getRvMedecinJour/{idMedecin}/{jour}]
Die URL [/getRvMedecinJour/{idMedecin}/{jour}] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
// list of doctor's appointments
@RequestMapping(value = "/getRvMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
public Reponse getRvMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// check the date
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, null);
}
// we get the doctor back
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// list of appointments
List<Rv> rvs = null;
try {
rvs = application.getRvMedecinJour(médecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// we return the answer
return new Reponse(0, Static.getListMapForRvs(rvs));
}
- Zeile 31: Wir geben ein List<Map<String, Object>>-Objekt anstelle eines List<Rv>-Objekts zurück. Erinnern Sie sich an die Definition der Klasse [Rv]:
@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 [creneau] abzurufen. Aufgrund des Joins [cr.medecin.id=?1] erhalten wir zudem den Arzt. Der Arzt erscheint daher in der JSON-Zeichenkette für jeden Termin. Diese doppelten Informationen sind jedoch unnötig. Kehren wir zum Code der Methode zurück:
- Zeile 31: Wir erstellen das Wörterbuch, das in JSON serialisiert werden soll, selbst;
Das für einen Termin erstellte Wörterbuch sieht wie folgt aus:
// Rv --> Map
public static Map<String, Object> getMapForRv(Rv rv) {
// anything to do?
if (rv == null) {
return null;
}
// dictionary <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("client", rv.getClient());
hash.put("creneau", getMapForCreneau(rv.getCreneau()));
// we return the dictionary
return hash;
}
- Zeile 11: Wir rufen das Wörterbuch aus dem zuvor vorgestellten [Creneau]-Objekt ab;
Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
oder diese mit einem falschen Tag:
![]() |
oder diese mit einem falschen Arzt:
![]() |
2.12.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)
public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// check the date
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(3, new String[] { String.format("jour [%s] invalide", jour) });
}
// we get the doctor back
Reponse réponse = getMedecin(idMedecin);
if (réponse.getStatus() != 0) {
return réponse;
}
Medecin médecin = (Medecin) réponse.getData();
// get your diary back
AgendaMedecinJour agenda = null;
try {
agenda = application.getAgendaMedecinJour(médecin.getId(), jourAgenda);
} catch (Exception e1) {
return new Reponse(4, Static.getErreursForException(e1));
}
// ok
return new Reponse(0, Static.getMapForAgendaMedecinJour(agenda));
}
}
- Zeile 30 gibt ein Objekt vom Typ List<Map<String, Object>> zurück.
Die Methode [Static.getMapForAgendaMedecinJour] lautet wie folgt:
// AgendaMedecinJour --> Map
public static Map<String, Object> getMapForAgendaMedecinJour(AgendaMedecinJour agenda) {
// anything to do?
if (agenda == null) {
return null;
}
// dictionary <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("medecin", agenda.getMedecin());
hash.put("jour", new SimpleDateFormat("yyyy-MM-dd").format(agenda.getJour()));
List<Map<String, Object>> créneaux = new ArrayList<Map<String, Object>>();
for (CreneauMedecinJour créneau : agenda.getCreneauxMedecinJour()) {
créneaux.add(getMapForCreneauMedecinJour(créneau));
}
hash.put("creneauxMedecin", créneaux);
// we return the dictionary
return hash;
}
Das erstellte Wörterbuch hat drei Felder:
- [doctor]: der Arzt, dem der Terminplan gehört. Wir haben diese Information beibehalten, da sie nur einmal vorkommt, während sie in früheren Fällen in jeder JSON-Zeichenkette wiederholt wurde;
- [day]: der Tag im Kalender;
- [doctorSlots]: die Liste der verfügbaren Termine des Arztes, einschließlich aller für diesen Termin geplanten Termine;
Die in Zeile 13 verwendete Methode [getMapForCreneauMedecinJour] lautet wie folgt:
// CreneauMedecinJour --> map
public static Map<String, Object> getMapForCreneauMedecinJour(CreneauMedecinJour créneau) {
// anything to do?
if (créneau == null) {
return null;
}
// dictionary <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("creneau", getMapForCreneau(créneau.getCreneau()));
hash.put("rv", getMapForRv(créneau.getRv()));
// we return the dictionary
return hash;
}
- Zeilen 9–10: Wir verwenden die bereits besprochenen Wörterbücher für die Typen [Creneau] und [Rv], die daher keine [Medecin]-Objekte enthalten;
Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
oder diese, falls der Tag falsch ist:
![]() |
oder diese, wenn die Arzt-ID ungültig ist:
![]() |
2.12.12. Die URL [/getMedecinById/{id}]
Die URL [/getMedecinById/{id}] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
@RequestMapping(value = "/getMedecinById/{id}", method = RequestMethod.GET)
public Reponse getMedecinById(@PathVariable("id") long id) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// we get the doctor back
return getMedecin(id);
}
Zeile 8, die Methode [getMedecin] lautet wie folgt:
private Reponse getMedecin(long id) {
// we get the doctor back
Medecin médecin = null;
try {
médecin = application.getMedecinById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// existing doctor?
if (médecin == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, médecin);
}
Die Ergebnisse lauten wie folgt:
![]() |
oder diese, falls die Arzt-ID falsch ist:
![]() |
2.12.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)
public Reponse getClientById(@PathVariable("id") long id) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// we get the customer back
return getClient(id);
}
Zeile 8, die Methode [getClient] lautet wie folgt:
private Reponse getClient(long id) {
// we get the customer back
Client client = null;
try {
client = application.getClientById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// existing customer?
if (client == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, client);
}
Die Ergebnisse lauten wie folgt:
![]() |
oder diese, wenn die Kunden-ID falsch ist:
![]() |
2.12.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)
public Reponse getCreneauById(@PathVariable("id") long id) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// we get the slot back
Reponse réponse = getCreneau(id);
if (réponse.getStatus() == 0) {
réponse.setData(Static.getMapForCreneau((Creneau) réponse.getData()));
}
// result
return réponse;
}
Zeile 8, die Methode [getCreneau] lautet wie folgt:
private Reponse getCreneau(long id) {
// we get the slot back
Creneau créneau = null;
try {
créneau = application.getCreneauById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// existing niche?
if (créneau == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, créneau);
}
Die erhaltenen Ergebnisse lauten wie folgt:
![]() |
oder diese, falls die Slot-Nummer falsch ist:
![]() |
2.12.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)
public Reponse getRvById(@PathVariable("id") long id) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// recovering the rv
Reponse réponse = getRv(id);
if (réponse.getStatus() == 0) {
réponse.setData(Static.getMapForRv2((Rv) réponse.getData()));
}
// result
return réponse;
}
Zeile 8, die Methode [getRv] lautet wie folgt:
private Reponse getRv(long id) {
// we recover the Rv
Rv rv = null;
try {
rv = application.getRvById(id);
} catch (Exception e1) {
return new Reponse(1, Static.getErreursForException(e1));
}
// Existing Rv?
if (rv == null) {
return new Reponse(2, null);
}
// ok
return new Reponse(0, rv);
}
Zeile 10, die Methode [Static.getMapForRv2] lautet wie folgt:
// Rv --> Map
public static Map<String, Object> getMapForRv2(Rv rv) {
// qq chose à faire ?
if (rv == null) {
return null;
}
// dictionnaire <String,Object>
Map<String, Object> hash = new HashMap<String, Object>();
hash.put("id", rv.getId());
hash.put("idClient", rv.getIdClient());
hash.put("idCreneau", rv.getIdCreneau());
// on rend le dictionnaire
return hash;
}
Die Ergebnisse lauten wie folgt:
![]() |
oder diese, falls die Termin-ID falsch ist:
![]() |
2.12.16. Die URL [/ajouterRv]
Die URL [/ajouterRv] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
@RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// retrieve posted values
String jour = post.getJour();
long idCreneau = post.getIdCreneau();
long idClient = post.getIdClient();
// check the date
Date jourAgenda = null;
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false);
try {
jourAgenda = sdf.parse(jour);
} catch (ParseException e) {
return new Reponse(6, null);
}
// we get the slot back
Reponse réponse = getCreneau(idCreneau);
if (réponse.getStatus() != 0) {
return réponse;
}
Creneau créneau = (Creneau) réponse.getData();
// we get the customer back
réponse = getClient(idClient);
if (réponse.getStatus() != 0) {
réponse.incrStatusBy(2);
return réponse;
}
Client client = (Client) réponse.getData();
// we add the Rv
Rv rv = null;
try {
rv = application.ajouterRv(jourAgenda, créneau, client);
} catch (Exception e1) {
return new Reponse(5, Static.getErreursForException(e1));
}
// we return the answer
return new Reponse(0, Static.getMapForRv(rv));
}
Hier gibt es nichts, was wir nicht schon gesehen hätten. In Zeile 41 geben wir den Termin zurück, der in Zeile 36 hinzugefügt wurde.
Die mit dem [Advanced Rest Client] erzielten Ergebnisse sehen wie folgt aus:
![]() |
oder so, wenn wir beispielsweise eine nicht vorhandene Slot-Nummer angeben:
![]() |
![]() |
2.12.17. Die URL [/deleteAppointment]
Die URL [/deleteAppointment] wird von der folgenden Methode im Controller [RdvMedecinsController] verarbeitet:
@RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post) {
// application status
if (messages != null) {
return new Reponse(-1, messages);
}
// retrieve posted values
long idRv = post.getIdRv();
// recovering the rv
Reponse réponse = getRv(idRv);
if (réponse.getStatus() != 0) {
return réponse;
}
// rv deletion
try {
application.supprimerRv(idRv);
} catch (Exception e1) {
return new Reponse(3, Static.getErreursForException(e1));
}
// ok
return new Reponse(0, null);
}
Die resultierenden „ s“ lauten wie folgt:
![]() |
oder diese, falls die Termin-ID nicht existiert:
![]() |
Wir sind mit dem Controller fertig. Schauen wir uns nun an, wie das Projekt konfiguriert wird.
2.12.18. Webdienst-Konfiguration
![]() |
Die Konfigurationsklasse [AppConfig] sieht wie folgt aus:
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class })
public class AppConfig {
}
- Zeile 9: Wir setzen den Modus auf [AutoConfiguration], damit Spring Boot das Projekt anhand der Dateien konfigurieren kann, die es im Klassenpfad des Projekts findet;
- Zeile 10: Wir geben an, 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;
2.12.19. Die ausführbare Klasse des Webdienstes
![]() |
Die [Boot]-Klasse 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 Konfigurationsklasse des Projekts [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 Protokolle während der Ausführung lauten wie folgt:
- Zeile 17: Der Tomcat-Server startet;
- Zeilen 23–31: Die Schichten [Geschäftslogik, DAO, JPA] werden initialisiert;
- Zeile 34: Die Methode, die die URL [/getRvMedecinJour/{idMedecin}/{jour}] verarbeitet, wurde gefunden. Dieser Prozess des Auffindens von Controller-Methoden wiederholt sich bis Zeile 44;
- Zeile 52: Das Spring-MVC-Servlet [DispatcherServlet] ist bereit, auf Anfragen von Web-Clients zu reagieren;
Wir verfügen nun über einen funktionierenden Webdienst, der von einem Web-Client 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.
2.13. Einführung in Spring Security
Wir werden erneut einen Spring-Leitfaden 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;
2.13.1. Maven-Konfiguration
Projekt [3] ist ein Maven-Projekt. Sehen wir uns die Datei [pom.xml] an, um die Abhängigkeiten zu überprüfen:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.1.1.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
- Zeilen 1–5: Das Projekt ist ein Spring-Boot-Projekt;
- Zeilen 8–11: Abhängigkeit vom [Thymeleaf]-Framework, das die Erstellung dynamischer HTML-Seiten ermöglicht. Dieses Framework kann JSP (Java Server Pages) ersetzen, das bis vor kurzem das Standard-View-Framework für Spring MVC war;
- Zeilen 12–15: Abhängigkeit vom Spring Security-Framework;
2.13.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>
- Die [th:xx]-Attribute sind Thymeleaf-Attribute. Sie werden von Thymeleaf interpretiert, bevor die HTML-Seite an den Client gesendet wird. Der Client sieht sie nicht;
- Zeile 12: Das Attribut [th:href="@{/hello}"] erzeugt das Attribut [href] des Tags <a>. Der Wert [@{/hello}] erzeugt den Pfad [<context>/hello], wobei [context] der Kontext der Webanwendung ist;
Der generierte HTML-Code lautet wie folgt:
- Zeile 10: Der Anwendungskontext ist das Stammverzeichnis /;
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:
- 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:
Beachten Sie in Zeile 21, dass Thymeleaf ein verstecktes Feld mit dem Namen [_csrf] hinzugefügt hat.
2.13.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: Mit der Methode [addViewControllers] können URLs mit HTML-Ansichten verknüpft werden. Dort werden folgende Verknüpfungen hergestellt:
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 [main] 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.
2.13.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: Mit der Methode [http.authorizeRequests()] können URLs mit Zugriffsrechten verknüpft werden. Dort werden folgende Zuordnungen vorgenommen:
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: Neudefinition der Methode [configure(AuthenticationManagerBuilder auth)], die Benutzer verwaltet;
- Zeile 20: Die Authentifizierung erfolgt über fest programmierte Benutzer [auth.inMemoryAuthentication()]. Ein Benutzer wird hier mit dem Login [user], dem Passwort [password] und der Rolle [USER] definiert. Benutzern mit derselben Rolle können dieselben Berechtigungen erteilt werden;
2.13.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 verwaltet wurden [/, /home, /login, /hello] und dass einige durch Zugriffsrechte geschützt waren.
2.13.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:
Wenn wir auf den Link klicken, wird die URL [/hello] aufgerufen. Diese ist geschützt:
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();
So erhalten wir sie [1]:
![]() |
Der Quellcode der abgerufenen 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 uns Spring Security zur URL [/hello] weiter, da dies die URL ist, die wir angefordert haben, 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 jedermann 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>
2.13.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 nur sehr 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.
2.14. Implementierung der Sicherheit für den Online-Terminvereinbarungsdienst
2.14.1. Die Datenbank
Die Datenbank [rdvmedecins] wird aktualisiert, um Benutzer, deren Passwörter und deren Rollen zu erfassen. Es wurden 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 für Benutzer und Rollen
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:
![]() |
2.14.2. Das neue Eclipse-Projekt für [Geschäftslogik, DAO, JPA]
Wir duplizieren das ursprüngliche Projekt [rdvmedecins-business-dao] in [rdvmedecins-business-dao-v2]:
![]() |
- in [1]: das neue Projekt;
- in [2]: Die durch die Implementierung der Sicherheit eingeführten Änderungen wurden in einem einzigen Paket [rdvmedecins.security] zusammengefasst. Diese neuen Elemente gehören zu den Schichten [JPA] und [DAO], aber der Einfachheit halber habe ich sie in einem einzigen Paket zusammengefasst.
2.14.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 des Fremdschlüssels von der Tabelle [USERS_ROLES] zur Tabelle [ROLES];
2.14.4. Änderungen an der [DAO]-Schicht
![]() |
Die [DAO]-Schicht wird um drei neue [Repository]s 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> {
// list of user roles identified by id
@Query("select ur.role from UserRole ur where ur.user.id=?1")
Iterable<Role> getRoles(long id);
// list of user roles identified by login and password
@Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
Iterable<Role> getRoles(String login, String password);
// search for a user via 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;
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;
2.14.5. Klassen für die Benutzer- und Rollenverwaltung
![]() |
Für Spring Security muss eine Klasse erstellt werden, 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 durch die folgende Klasse [AppUserDetails] 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);
2.14.6. Tests der [DAO]-Schicht
![]() |
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] aufgebaut. Diese Klasse war bereits im vorherigen 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: Wird die gesuchte Rolle nicht gefunden, wird eine Zeile in der Tabelle [USERS_ROLES] angelegt, 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 die folgenden Ergebnisse in der Datenbank erhalten:
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 wird es mit dem BCrypt-Algorithmus verschlüsselt. Die Methode [BCrypt.checkpw] überprüft, ob das verschlüsselte 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:
2.14.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 vorteilhafte 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 haben, 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.
2.14.8. Das Eclipse-Projekt für die [Web]-Schicht
![]() |
Das vorherige Projekt [rdvmedecins-webapi] wurde im Projekt [rdvmedecins-webapi-v2] dupliziert [1]:
![]() |
Die einzigen Änderungen müssen im Paket [rdvmedecins.web.config] vorgenommen werden, wo Spring Security konfiguriert werden muss. 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 werden genauso vorgehen:
- 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.boot.autoconfigure.EnableAutoConfiguration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import rdvmedecins.security.AppUserDetailsService;
@EnableAutoConfiguration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AppUserDetailsService appUserDetailsService;
@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();
// the password is transmitted by the header Authorization: Basic xxxx
http.httpBasic();
// only the ADMIN role can use the application
http.authorizeRequests() //
.antMatchers("/", "/**") // all URL
.hasRole("ADMIN");
}
}
- Zeilen 14–15: Wir haben die Annotationen aus dem Beispiel wiederverwendet;
- Zeilen 17–18: Die Klasse [AppUserDetails], die Zugriff auf die Benutzer der Anwendung bietet, wird injiziert;
- Zeilen 20–21: 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:
- einen Verweis auf den [appUserDetailsService] aus Zeile 18, 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 27–40: Die Methode [configure(HttpSecurity http)] definiert Zugriffsrechte für die URLs des Webdienstes;
- Zeile 30: 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;
- Zeile 32: Wir aktivieren die Authentifizierung über den 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 34–36: 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;
Die Klasse [AppConfig], die die gesamte Anwendung konfiguriert, wird wie folgt aktualisiert:
![]() |
package rdvmedecins.web.config;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import rdvmedecins.config.DomainAndPersistenceConfig;
@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class })
public class AppConfig {
}
- Die Änderung erfolgt in Zeile 11: Dort wird festgelegt, dass nun zwei Konfigurationsdateien verwendet werden sollen: [DomainAndPersistenceConfig] und [SecurityConfig].
2.14.9. Testen des Webdienstes
Wir werden den Webdienst mit dem Chrome-Client [Advanced Rest Client] testen. 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. 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] die Liste der Ärzte.
Versuchen wir nun eine HTTP-Anfrage mit einem falschen Authentifizierungsheader. Die Antwort lautet wie folgt:
![]() |
- in [1] und [3]: der HTTP-Authentifizierungsheader;
- in [2]: die Antwort des Webdienstes;
Probieren wir nun den Benutzer / user aus. 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;
2.15. Fazit
Lassen Sie uns die Gesamtarchitektur unserer Client/Server-Anwendung noch einmal betrachten:
![]() |
Ein sicherer Webdienst ist nun betriebsbereit. Wir werden sehen, dass er aufgrund von Problemen, die während der Entwicklung des Angular-JS-Clients auftreten werden, angepasst werden muss. Wir werden jedoch abwarten, bis das Problem auftritt, um es dann zu beheben. Wir werden nun den Angular-Client erstellen, der eine Weboberfläche zur Verwaltung von Arztterminen bereitstellt.

















































































































































