Anwendungsentwicklung mit Spring Boot und Thymeleaf
Das Spring-Framework wurde im Jahr 2003 als quelloffenes Projekt mit dem Ziel veröffentlicht, die Anwendungsentwicklung mit Java zu vereinfachen und gute Programmierpraktiken zu fördern. Am 1. April 2014 folgte die Freigabe von Spring Boot 1.0. Es handelt sich dabei nicht um ein neues Framework, sondern vereinfacht die Verwaltung von Abhängigkeiten und die Konfiguration des Spring-Frameworks. Mit Spring Boot lassen sich auf einfache Weise und nach dem Prinzip "Convention over Configuration" produktive Spring-Anwendungen erstellen. Die anhaltende Popularität von Spring Boot auch nach nunmehr bald acht Jahren zeigt sich unter anderem an nachstehender Google-Trend-Grafik:

Im nachstehenden Blog-Beitrag werde ich Schritt für Schritt eine einfache Web-Anwendung zur Verwaltung von Studierenden entwickeln und hierfür einige Komponenten des Spring Universums nutzen. Es geht dabei nicht um die Erstellung einer produktionsreifen Web-basierten Geschäftsanwendung, sondern um einen Show-Case für Einsteiger. So verzichte ich unter anderem auf weitreichenden Fehlerprüfungen und nutze der Einfachheit halber lediglich eine H2 in-memory Datenbank.
Das Endprodukt sieht wie folgt aus:
Architektur der Beispielanwendung

Der Betrachter des obigen Klassndiagramms mag sich fragen, weshalb der Controller nicht direkt auf das Repository zugreift, sondern dies indirekt via Service tut. Für einfache Anwendungen wäre eine solche direkte Koppelung durchaus denkbar, jedoch kommt dieser Ansatz bei grösseren Anwendungen schnell an seine Grenzen. Durch eine direkte Abhängigkeit zwischen Controller und Repository lässt sich zum Beispiel die Datenquelle nicht ohne weiteres austauschen. Falls beispielsweise die Daten statt aus einer Datenbank aus einem Web Service geladen werden sollen, impliziert dies diverse Anpassungen. Aus architektonischer Sicht macht es deshalb Sinn, einen weiteren Layer zwischen Presentation und Persistence zu einzuführen, der sogenannte Business Layer.
Der Business Layer enthält dabei die Geschäftslogik in Form von Services, einer Komponentenart in Spring. Diese Vorgehensweise hat unter anderem den Vorteil der einfachen Wiederverwendung. Der Business Layer kann von verschiedenen Controllern genutzt werden. Grundsätzlich verfolgt man somit also das Grundprinzip der Vermeidung von festen Abhängigkeiten bzw. die Realisierung von loser Kopplung.

Implementierung der Beispielanwendung
Der vollständige Source-Code steht auf meinem Gitlab-Repository zum Herunterladen bereit (siehe Hinweise am Ende dieses Blog-Beitrags).
Initialisierung der Projektumgebung
Mit Hilfe des Spring Initializers werden alle nötigen Konfigurationen (Build Tool z.B. Maven, verwendete Programmiersprache z.B. Java und Spring Boot Version) definiert sowie alle Abhängigkeiten wie Web-Frameworks, Persistenzlösung etc. ausgewählt.

Im vorliegenden Anwendungsbeispiel wählen wir nachstehende Abhängigkeiten:
- Spring Web: Unterstützt Spring MVC Funktionen und stellt einen eingebetteten Apache Tomcat Container.
- Spring Data JPA: Stellt Persistenz-Funktionen zur Verfügung.
- Spring Boot DevTools: Enthält nützliche Funktionen für den Entwickler / die Entwicklerin.
Das im Spring Initializer generierte Projekt kann in die genutzte Entwicklungsumgebung (IDE) importiert werden. Ich verwende IntelliJ als IDE und das Ergebnis des Imports sieht bei mir wie folgt aus:

Da ich - wie in der Einleitung erwähnt - für mein Projekt eine in-memory H2 Datenbank nutze, füge ich der generierten pom.xlm Datei noch nachstehende Abhängigkeit hinzu:
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
Dank dieser Abhängigkeit weiss Spring Boot, dass wir in unserem Projekt eine eingebettete H2 Datenbank-Engine verwenden wollen. Darüber hinaus wird automatisch eine Datenbank mit einem zufälligen Namen und Schemata erstellt.
Um mehr Kontrolle zu haben, empfiehlt es sich, einige Anwendungseigenschaften in Bezug auf die Datenquelle selber zu bestimmen. Hierzu fügen wir folgenden Inhalt zur Datei '/src/main/resources/application.yaml'.
spring:
datasource:
url: jdbc:h2:mem:student
username: sa
password: password
driverClassName: org.h2.Driver
jpa:
show-sql: true
properties.hibernate.format_sql: true
database-platform: org.hibernate.dialect.H2Dialect
h2:
console:
path: /h2
enabled: true
Persitenz Layer
Der Persistenz Layer wird im der vorliegenden Beispielanwendung mit Spring Data JPA implementiert. Es handelt sich hier um ein weiteres Projekt aus dem Spring Ecosystem mit dem Ziel, JPA-basierte Repositories zu implementieren. Spring Data JPA erleichtert die Erstellung von Spring-basierten Anwendungen, welche Datenzugriffstechnologien verwenden.
Als Entwickler/in schreibt man dabei eine Repository-Schnittstelle, einschliesslich allfälligen benutzerdefinierten Finder-Methoden. Spring Data JPA stellt daraufhin automatisch die Implementierung der Schnittstelle zur Verfügung.
Unser Repository soll Studierende verwalten und deshalb ist zuerst einmal die entsprechende Klasse zu erstellen:
@Entity
public class Student {
@Id
@SequenceGenerator(name = "student_sequence", sequenceName = "student_sequence", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "student_sequence")
private Long id;
private String firstName;
private String lastName;
@Column(unique=true)
private String email;
private LocalDate dateOfBirth;
@Transient
private int age;
private String address;
@Length(min = 4, max = 10)
private String zip;
private String city;
//getters and setters...
}
Das Student POJO kann mit Hilfe der Annotation @Entity als Entität definiert werden, wodurch JPA diese als solches erkennt und folglich die Instanzen im Repository speichern bzw. verwalten kann. Mit den weiteren Annotationen legen wir wichtige Eigenschaften der Entität fest. Dies sind im vorliegenden Fall der Primärschlüssel (@Id), die automatische Erzeugung von Werten für den Primärschlüssel (@GeneratedValue), die generierte Sequenz für den Primärschlüssel (@SequenceGenerator), einmalige Werte (@Column (unique = true)) für ein gegebenes Attribut, keine Speicherung eines berechneten Attributs in der Datenbank (@Transient) und die Länge eines Attributes (@Length).
Nun kann das StudentRespository entwickelt werden. Wie bereits erwähnt, müssen wir hierzu lediglich ein Interface schreiben, welches JpaRepository erweitert.
public interface StudentRepository extends JpaRepository<Student, Long> {
Optional<Student> findByEmail(String email);
}
Bei JpaReposirory handelt es sich seinerseits ebenfalls um ein Interface, welche eine Reihe von nützlichen Methoden anbietet:
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort sort);
List<T> findAllById(Iterable<ID> ids);
...
}
Die Eleganz besteht dabei darin, dass wir diese Methoden nicht implementieren müssen; Spring erstellt eine geeignete Proxy-Instanz für die von uns definierte Repository-Schnittstelle. Somit müssen also in StudentRepository nur Methoden definiert werden, welche nicht von JpaRepository standardmässig angeboten werden. In unserem Falle ist dies also die Methode findByEmail(). Die Signatur sorgt dabei dafür, dass auch für diese Methode automatisch eine Proxy-Instanz erzeugt wird.
Um dieses Repository zu testen, stellen wir eine Testdatenbank zur Verfügung. Dies können wir zum Beispiel erreichen, indem wir einerseits die Abhängigkeit "flywaydb" zu unserer pom.xml Datei hinzufügen und andererseits in der Datei '/src/main/resources/db/migration/V001_Create_DB.sql' sowohl die Tabelle "Student" erzeugen als auch mit einigen Datenbankzeilen ergänzen.
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>8.2.1</version>
</dependency>
DROP TABLE IF EXISTS student CASCADE;
CREATE SEQUENCE student_sequence INCREMENT BY 1 ;
create table student
(
id bigint default student_sequence.nextval primary key,
address varchar(255),
city varchar(255),
date_of_birth date,
email varchar(255),
first_name varchar(255),
last_name varchar(255),
zip varchar(255),
UNIQUE (email)
);
insert into student (first_name, last_name, email, date_of_birth, address, zip, city )
values ('Hans', 'Muster', 'hans.muster@demo.ch', '1996-06-12', 'Musterstrasse 21', '3000', 'Bern');
insert into student (first_name, last_name, email, date_of_birth, address, zip, city)
values ('Rosa', 'Meier', 'rosa.meier@demo.ch', '1999-04-25', 'Rosenweg 456', '2501', 'Biel');
insert into student (first_name, last_name, email, date_of_birth, address, zip, city)
values ('Ida', 'Schneider', 'ida.schneider@demo.ch', '1995-07-23', 'Bachstrasse 2', '8000', 'Zürich');
Nun können wir eine Testklasse für unser StudentRepository schreiben:
@DataJpaTest
@DisplayName("StudentRepository Test")
public class StudentRepositoryTest {
@Autowired
private StudentRepository studentRepository;
@Test
@DisplayName("Find Student by Id")
public void findStudentById() {
Optional<Student> optionalStudent = studentRepository.findById(1L);
assertTrue(optionalStudent.isPresent());
}
// more test methods...
}
Business Layer
Der Business Layer wird in Spring Boot mit Hilfe von Service Klassen implementiert. Diese enthalten die Annotation @Service. Der Nutzen dieser zusätzlichen Schicht besteht in der Trennung der Geschäftslogik von den anderen Schichten. So sollte sich beispielsweise im Presentation Layer (in Spring Boot durch Controller umgesetzt) keine Geschäftslogik befinden. Der Presentation Layer (sprich der Controller) ist zuständig für die Entgegennahme des Requests und das Mappen auf eine passende Handler-Methode. Aus architektonischer Sicht macht es deshalb Sinn, eine weitere Schicht zwischen Presentation und Persistence einzufügen: die Businessschicht.
In unserer Beispielanwendung stellt die Service Klasse StudentService alle nötigen Service-Methoden zum Erzeugen, Finden und Verwalten von Studierenden zur Verfügung. Hierzu wird das im obigen Kapitel beschriebenen Repository genutzt. Dieses Repository wird im Konstruktor injected.
@Service
public class StudentService {
private StudentRepository studentRepository;
private Validator validator;
public StudentService(StudentRepository studentRepository, Validator validator) {
this.studentRepository = studentRepository;
this.validator = validator;
}
@Transactional(rollbackFor = StudentAlreadyExistsException.class)
public void addStudent(Student student) throws StudentAlreadyExistsException {
if (studentRepository.findByEmail(student.getEmail()).isPresent()) {
throw new StudentAlreadyExistsException();
}
studentRepository.save(student);
}
public Student findStudent(Long id) throws StudentNotFoundException {
Optional<Student> student = studentRepository.findById(id);
return student.orElseThrow(StudentNotFoundException::new);
}
public List<Student> findStudents() {
List<Student> students = studentRepository.findAll();
return students;
}
@Transactional(rollbackFor = StudentNotFoundException.class)
public Student updateStudent(Student student) throws StudentNotFoundException {
studentRepository.findById(student.getId()).orElseThrow(StudentNotFoundException::new);
return studentRepository.save(student);
}
private boolean isValid(Student student) {
Set<ConstraintViolation<Student>> constraintViolations = validator.validate(student);
return constraintViolations.isEmpty();
}
public void deleteStudent(Long id) throws StudentNotFoundException {
Student book = studentRepository.findById(id).orElseThrow(StudentNotFoundException::new);
studentRepository.delete(book);
}
public Long count() {
return studentRepository.count();
}
}
Die beiden geworfenen Exception erweitern die Standard Exception und müssen deshalb nicht weiter erklärt werden.
Auch diese Service Klasse soll nun wieder mit Hilfe von geeigneten Tests validiert werden:
@SpringBootTest
@DisplayName("StudentService Test")
public class StudentServiceTest {
@Autowired
private StudentService studentService;
@Test
@DisplayName("Add Student")
public void addStudent() throws StudentAlreadyExistsException {
Student student = new Student();
student.setFirstName("Max");
student.setLastName("Häberli");
student.setEmail("max.haeberli@demo.ch");
studentService.addStudent(student);
}
@Test
@DisplayName("Find Student by Id 1")
public void findStudent() throws StudentNotFoundException {
Student student = studentService.findStudent(1L);
assertNotNull(student);
assertEquals("Hans", student.getFirstName());
assertEquals("Muster", student.getLastName());
}
@Test
@DisplayName("Find Student with unknown Id. Should throw StudentNotFoundException")
public void findBookThrowsBookNotFoundException() {
assertThrows(StudentNotFoundException.class, () -> studentService.findStudent(4711L));
}
@Test
@DisplayName("Find all Students")
public void findStudents() {
List<Student> students = studentService.findStudents();
assertFalse(students.isEmpty());
assertEquals(studentService.count(), students.size());
}
}
Presentation Layer
Der Presentation Layer wird in Spring Boot durch einen oder mehreren Controllern umgesetzt. Diese werden mit @Controller annotiert.
An dieser Stelle muss erwähnt werden, dass es in Spring unterschiedliche Controller Typen gibt. Einerseits werden oft sogenannte RestController verwendet (annotiert mit @RestController), welche RESTFul-Dienste bereitstellen. D.h. ein Client sendet z.B. eine JSON-Anfrage an diesen RestController und erwartet sodann als Resultat ebenfall ein JSON-Objekt. Andererseits werden im Umfeld von Spring MVC die in diesem Anwendungsbeispiel benutzen Controller eingesetzt. Damit wird eine Klasse als Web-Request-Handler markieren.
Ich werde in einem nächsten Blog-Beitrag etwas detaillierter auf die Unterschiede zwischen diesen beiden Controller-Typen eingehen.
Unser Controller ist wie folgt implementiert:
@Controller
public class StudentController {
private StudentService studentService;
public StudentController(StudentService studentService) {
this.studentService = studentService;
}
@GetMapping("/")
String getCustomers(Model model) {
List<Student> students = studentService.findStudents();
model.addAttribute("students", students);
return "studentlist";
}
@RequestMapping(value = "/delete/{id}")
String deleteStudent(@PathVariable(name = "id") Long id) throws StudentNotFoundException {
studentService.deleteStudent(id);
return "redirect:/";
}
@RequestMapping(value = "/add")
public String showAddStudentForm(Model model) {
Student student = new Student();
model.addAttribute("student", student);
return "addstudent";
}
@RequestMapping(value = "/edit/{id}")
public ModelAndView showEditStudentForm(@PathVariable(name = "id") Long id) throws StudentNotFoundException {
ModelAndView modelAndView = new ModelAndView("editstudent");
Student student = studentService.findStudent(id);
modelAndView.addObject("student", student);
return modelAndView;
}
@RequestMapping(value = "/save", method = RequestMethod.POST)
public String SaveStudent(@ModelAttribute("student") Student student, @RequestParam(value="action", required=true) String action) throws StudentAlreadyExistsException {
if (action.equals("save")) {
studentService.addStudent(student);
}
return "redirect:/";
}
@RequestMapping(value = "/update", method = RequestMethod.POST)
public String updateStudent(@ModelAttribute("student") Student student, @RequestParam(value="action", required=true) String action) throws StudentAlreadyExistsException, StudentNotFoundException {
if (action.equals("update")) {
studentService.updateStudent(student);
}
return "redirect:/";
}
}
Man beachte, dass die Methoden dieser Klasse entweder einen Namen einer View, ein ModelAndView Objekt (inkl. Angabe der anzuzeigenden View) oder aber den Wert redirect:/ (also eine Umleitung auf die Root URL der Anwendung zurückgeben). Im Falle der Rückgabe eines View-Namens wird in unserem Anwendungsbeispiel eine Thymeleaf HTML-Seite aufgerufen, welche die im Model eingefügten Objekte rendert. Weitere Details hierzu folgen im nächsten Kapitel dieses Blog-Beitrages.
Das Testen von Controllers ist naturgemäss etwas kompliziert. Ich beschränke mich deshalb auf einen minimalen "WebMVCTest":
@WebMvcTest(StudentController.class)
public class StudentControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private StudentService studentService;
@Test
public void shouldReturnViewWithPrefilledData() throws Exception {
this.mockMvc
.perform(get("/"))
.andExpect(status().isOk())
.andExpect(view().name("studentlist"))
.andExpect(model().attributeExists("students"));
}
}
View Layer
Die vorliegende Beispielanwendung ist für den Endanwender ohne geeignete Darstellung im Browser (View) nutzlos, weshalb wir diese mit Hilfe von Thymeleaf implementieren. Thymeleaf ist eine Template-Engine, d.h. eine in JAVA geschriebene Bibliothek. Sie ermöglicht es dem Entwickler bzw. der Entwicklerin, eine HTML-, XHTML- oder HTML5-Seitenvorlage zu definieren und diese später mit Daten zu füllen, um die finale Seite zu generieren. In diesem Sinne realisiert Thymeleaf den Model-View-Teil des Model-View-Controller-Musters.
Die Einstiegsseite unserer Beispielanwendung soll eine Liste aller Studierenden darstellen. Hierzu erstellen wir im Verzeichnis '/src/main/resources/template' die Datei studentlist.html mit nachstehendem Inhalt:
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body>
<header th:insert="fragments/header.html :: header"> </header>
<h1>List of Students</h1>
<p><a th:href="@{/add}" class="btn btn-primary">Add Student</a></p>
<!--<table class="table table-dark"> -->
<table class="table">
<thead class="thead-light">
<tr>
<th scope="col">Id</th>
<th scope="col">FirstName</th>
<th scope="col">LastName</th>
<th scope="col">Email</th>
<th scope="col">Date of Birth</th>
<th scope="col">Age</th>
<th scope="col">Address</th>
<th scope="col">Zip</th>
<th scope="col">City</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<tr th:each="students: ${students}">
<td th:text="${students.id}" />
<td th:text="${students.firstName}" />
<td th:text="${students.lastName}" />
<td th:text="${students.email}" />
<td th:text="${students.dateOfBirth}" />
<td th:text="${students.age}" />
<td th:text="${students.address}" />
<td th:text="${students.zip}" />
<td th:text="${students.city}" />
<td><a th:href="@{/delete/{id}(id=${students.id})}" class="btn btn-danger">Delete</a>
<a th:href="@{/edit/{id}(id=${students.id})}" class="btn btn-info">Edit</a></td>
</tr>
</tbody>
</table>
</body>
</html>
In der oben dargestellten HTML Seite verwenden wir neben Thymeleaf spezifischen Elementen auch Bootstrap. Bei letzterem handelt es sich um quelloffenes Frontend-Web-Framework zur Erstellung von Webseiten. Es enthält HTML- und CSS-basierte Designvorlagen für Typografie, Formulare, Schaltflächen und Navigation sowie optionale JavaScript-Erweiterungen.
Bei Thymeleaf handelt es sich um eine serverseitige Rendering-Technologie. Anders als bei clientseitigen Rendering-Frameworks wie Angular oder Vue.js müssen die Anweisungen vor dem Ausliefern an den Browser vom Server ausgewertet und ersetzt werden. In unserer Beispielanwendung wird eine Liste mit Studierendendaten dargestellt. Hierzu hinterlegen wir ein List-Objekt mit mehreren Studierenden im Model. Thymeleaf iteriert nun mit „th:each“ über diese Liste, wie nachstehender Auszug zeigt:
<tr th:each="students: ${students}">
<td th:text="${students.id}" />
<td th:text="${students.firstName}" />
...
</tr>
Damit die obige HTML-Seite gerendert wird, müssen wir jedoch nochmals unsere pom.xml Datei mit nachstehender Abhängigkeit ergänzen:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Die Liste der Studierenden kann nun im Browser unter der Adresse http://localhost:8080 aufgerufen werden.

Wie anhand der oben dargestellten Buttons entnommen werden kann, erlaubt die Anwendung auch das Hinzufügen von neuen Studierendendaten, sowie das Löschen oder Aktualisierend von vorhandenen Studierendendaten.
Für die Erfassung von neuen Studierendendaten dient nachstehende HTML-Seite, welche mit dem Namen addstudent.html im Verzeichnis '/src/main/resources/template' angelegt wird.
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body>
<h1>Add Student</h1>
<form action="#" th:action="@{/save}" th:object="${student}" method="post">
<table>
<tr>
<td><label class="col-auto col-form-label control-label">Firstname <span class=sub>*</span></label></td>
<td><input type="text" th:field="*{firstName}" class="form-control" aria-describedby="firstnameHelp" placeholder="Firstname" required></td>
</tr>
<tr>
<td><label class="col-auto col-form-label">Lastname <span class=sub>*</span></label></td>
<td><input type="text" th:field="*{lastName}" class="form-control" placeholder="Lastname" required></td>
</tr>
<tr>
<td><label class="col-auto col-form-label">E-Mail <span class=sub>*</span></label></td>
<td><input type="email" th:field="*{email}" class="form-control" placeholder="E-Mail" required></td>
</tr>
<tr>
<td><label class="col-auto col-form-label">Date of birth <span class=sub>*</span></label></td>
<td><input type="date" th:field="*{dateOfBirth}" class="form-control" data-date-format="yyyy-mm-dd" placeholder="Date of birth" required></td>
</tr>
<tr>
<td><label class="col-auto col-form-label">Address</label></td>
<td><input type="text" th:field="*{address}" class="form-control" placeholder="Address"></td>
</tr>
<tr>
<td><label class="col-auto col-form-label">Zip <span class=sub>*</span></label></td>
<td><input type="text" th:field="*{zip}" class="form-control" placeholder="Zip" minlength="4" maxlength="10" required></td>
</tr>
<tr>
<td><label class="col-auto col-form-label">City <span class=sub>*</span></label></td>
<td><input type="text" th:field="*{city}" class="form-control" placeholder="City"required></td>
</table>
<p>
<button type="submit" name="action" value="save" class="btn btn-primary">Save</button>
<button type="submit" name="action" value="cancel" class="btn btn-info" onclick="this.form.setAttribute('novalidate', 'novalidate');">Cancel</button>
</p>
</form>
</body>
</html>
Wie unschwer erkannt werden kann, enthält obige HTML-Seite ein Formular für die Erfassung von neuen Studierendendaten. Wiederum kommt hier Thymeleaf zum Einsatz. Zum Beispiel wird das Eingabefeld für den Vornamen an das Thymeleaf Attribut 'th:field="*{firstName}"' gebunden.

Als Herausforderung erweist sich die Erfassung des Geburtstags. Ohne weitere Massnahmen wird beim Abspeichern ein typeMismatch Fehler geworfen, welche vom Datumsformat verursacht wird. Dies kann durch nachstehende Ergänzung in der Datei application.yaml behoben werden:
mvc:
pathmatch:
matching-strategy: ant_path_matcher
format:
date: yyyy-MM-dd
Nahezu identisch zur oben dargestellten Erfassungsseite präsentiert sich die HTML-Seite für das Editieren von Studierendendaten, welches unter dem Namen editstudent.html wiederum im Verzeichnis '/src/main/resources/template' abgelegt wird:
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body>
<h1>Edit Student</h1>
<form action="#" th:action="@{/update}" th:object="${student}" method="post">
<table>
<tr>
<td><label class="col-auto col-form-label control-label">Id</label></td>
<td><input type="text" th:field="*{id}" class="form-control" placeholder="id" readonly></td>
</tr>
<tr>
<td><label class="col-auto col-form-label control-label">Firstname <span class=sub>*</span></label></td>
<td><input type="text" th:field="*{firstName}" class="form-control" placeholder="Firstname" required></td>
</tr>
<tr>
<td><label class="col-auto col-form-label">Lastname <span class=sub>*</span></label></td>
<td><input type="text" th:field="*{lastName}" class="form-control" placeholder="Lastname" required></td>
</tr>
<tr>
<td><label class="col-auto col-form-label">E-Mail <span class=sub>*</span></label></td>
<td><input type="email" th:field="*{email}" class="form-control" placeholder="E-Mail" required></td>
</tr>
<tr>
<td><label class="col-auto col-form-label">Date of birth <span class=sub>*</span></label></td>
<td><input type="date" th:field="*{dateOfBirth}" class="form-control" data-date-format="yyyy-mm-dd" placeholder="Date of birth" required></td>
</tr>
<tr>
<td><label class="col-auto col-form-label">Address</label></td>
<td><input type="text" th:field="*{address}" class="form-control" placeholder="Address"></td>
</tr>
<tr>
<td><label class="col-auto col-form-label">Zip <span class=sub>*</span></label></td>
<td><input type="text" th:field="*{zip}" class="form-control" placeholder="Zip" minlength="4" maxlength="10" required></td>
</tr>
<tr>
<td><label class="col-auto col-form-label">City <span class=sub>*</span></label></td>
<td><input type="text" th:field="*{city}" class="form-control" placeholder="City"required></td>
</table>
<p>
<button type="submit" name="action" value="update" class="btn btn-primary">Update</button>
<button type="submit" name="action" value="cancel" class="btn btn-info">Cancel</button>
</p>
</form>
</body>
</html>
Zusammenfassung
In diesem Blog-Beitrag habe ich gezeigt, wie unter Verwendung von Spring Boot, Spring MVC, Spring Data JPA und Thymeleaf in wenigen Schritten eine einfache Lösung für die Verwaltung von Studierenden entwickelt werden kann. Dabei habe ich bewusst auf eine umfassende Fehlerprüfungen, auf Sicherheitsmechanismen oder die nachhaltige Speicherung der Daten verzichtet.
Das vorliegende Ergebnis kann als Ausgangslage für weitere Ausbauschritte verwendet werden.
Ausblick
In weiteren Blog-Beiträgen werde ich auf der Basis des hier entwickelten Beispiels zeigen, welche weiteren Schritte für eine produktionsreife Anwendung notwendig sind. So werde ich näher auf Themen wie Security Mechanismen mit Spring Security, Entwicklung von RESTFul Web Services mit Spring Boot, Paging und Sorting von grossen Datenbeständen etc. eingehen.
Source Code
Den vollständigen Source Code zu dieser Beispielanwendung finden Sie auf meinem GitHub Repository unter https://gitlab.com/jfr1/student-mgmt.