[JAVA] Die erste WEB-Anwendung mit Spring Boot-Making a Pomodoro Timer-

Überblick

Dieser Artikel ist eine Aufzeichnung, als ich zum ersten Mal versuchte, eine WEB-Anwendung von Grund auf neu zu erstellen. Ich konnte die technischen Details nicht erklären, da es einige Teile gibt, die ich nicht verstehe, aber da ich den Ablauf vom Design zur Implementierung und den Quellcode veröffentlicht habe, hoffe ich, dass er als Referenz für andere Anfänger dient.

Abgeschlossen

Eine WEB-Anwendung, die die Startzeit der Arbeit aufzeichnet, nach 50 Minuten eine Warnung anzeigt und die Endzeit der Arbeit aufzeichnet.

image.png

Umgebung

Ergebnisse

Projektbaum


.
├── src
│   ├── main
│   │   ├── java
│   │   │   └── jp
│   │   │       └── co
│   │   │           └── anyplus
│   │   │               ├── Entity
│   │   │               │   └── TasksEntity.java
│   │   │               ├── Repository
│   │   │               │   └── TasksRepository.java
│   │   │               ├── ServletInitializer.java
│   │   │               ├── TaskTrackerApplication.java
│   │   │               ├── controller
│   │   │               │   └── IndexController.java
│   │   │               ├── form
│   │   │               │   └── TaskForm.java
│   │   │               └── service
│   │   │                   ├── TaskCancelService.java
│   │   │                   └── TaskRegisterService.java
│   │   ├── resources
│   │   │   ├── application.properties
│   │   │   ├── static
│   │   │   │   ├── css
│   │   │   │   │   ├── bootstrap-grid.css
│   │   │   │   │   ├── bootstrap-grid.css.map
│   │   │   │   │   ├── bootstrap-grid.min.css
│   │   │   │   │   ├── bootstrap-grid.min.css.map
│   │   │   │   │   ├── bootstrap-reboot.css
│   │   │   │   │   ├── bootstrap-reboot.css.map
│   │   │   │   │   ├── bootstrap-reboot.min.css
│   │   │   │   │   ├── bootstrap-reboot.min.css.map
│   │   │   │   │   ├── bootstrap.css
│   │   │   │   │   ├── bootstrap.css.map
│   │   │   │   │   ├── bootstrap.min.css
│   │   │   │   │   ├── bootstrap.min.css.map
│   │   │   │   │   └── jQuery.countdownTimer.css
│   │   │   │   ├── images
│   │   │   │   └── js
│   │   │   │       ├── bootstrap.bundle.js
│   │   │   │       ├── bootstrap.bundle.js.map
│   │   │   │       ├── bootstrap.bundle.min.js
│   │   │   │       ├── bootstrap.bundle.min.js.map
│   │   │   │       ├── bootstrap.js
│   │   │   │       ├── bootstrap.js.map
│   │   │   │       ├── bootstrap.min.js
│   │   │   │       ├── bootstrap.min.js.map
│   │   │   │       ├── import.js
│   │   │   │       ├── jQuery.countdownTimer.js
│   │   │   │       ├── jQuery.countdownTimer.min.js
│   │   │   │       ├── jQuery.countdownTimer.min.js.map
│   │   │   │       ├── jquery-3.4.1.min.js
│   │   │   │       ├── localisation
│   │   │   │       └── view-index.js
│   │   │   └── templates
│   │   │       └── index.html

Design

Ich habe die Funktion so einfach wie möglich gemacht und davon ausgegangen, dass sie vorerst an einem Tag erstellt werden kann.

Funktionelles Design

// Die Ergebnisanzeige wurde diesmal verschoben. Ich möchte es in Zukunft implementieren.

Das Sequenzdiagramm ist wie folgt.

image.png

Tischgestaltung

Erstellen Sie nur eine Tabelle, da Sie nur die von Ihnen ausgeführten Aufgaben speichern müssen.

** Aufgabentabelle **

Säule Erläuterung
task_id Sequenzwert
task_name Aufgabennname
task_category Arbeitskategorie.
task_starttime Arbeitsbeginnzeit
task_endtime Arbeitsendzeit
userid BENUTZERIDENTIFIKATION. Weil es keine Benutzerfunktion gibtsystemVorausgesetzt, dass nur eingegeben werden kann

DB Wir haben eine Datenbank für diese WEB-Anwendung vorbereitet und die Tabelle wie folgt erstellt.

DDL


CREATE TABLE public.tasks (
	task_id bigserial NOT NULL,
	task_name varchar(200) NULL,
	task_category varchar(200) NULL,
	task_starttime timestamp NULL,
	task_endtime timestamp NULL,
	userid varchar(20) NULL
);

Ein Projekt erstellen

Erstellen Sie ein Projekt. Dieses Mal habe ich Folgendes aus dem Spring Starter Project gemacht.

image.png

image.png

Das oben festgelegte Maven-Repository lautet wie folgt.

--SpringBootDevTool: Ein praktisches Entwicklungstool, das automatisch kompiliert und neu gestartet wird, wenn eine Änderung der Java-Quelle ausgelöst wird. --SpringDataJpa: Eine praktische Bibliothek für DB-Operationen. Es führt eine automatische Typkonvertierung zwischen Java <-> DB durch. --PostgreSQL-Treiber: Treiber für den Zugriff auf PostgreSQL

Klicken Sie nach dem Erstellen des Projekts mit der rechten Maustaste auf Projekt> Ausführen> Maven-Installation, um die in pom.xml definierte Repository-Datei zu installieren.

In diesem Verfahren wird pom.xml automatisch im Spring Starter-Projekt generiert. Wenn Sie die Version ändern oder die Bibliothek manuell hinzufügen, ist es besser, pom.xml direkt zu bearbeiten und Maven Install oder Maven Clean and Install durchzuführen. Das Maven-Repository finden Sie auf dieser Site (https://mvnrepository.com).

Öffnen Sie als Nächstes die Datei application.properties und legen Sie die Einstellungen für die DB-Verbindung fest.

application.properties


spring.datasource.url=jdbc:postgresql://localhost:5432/[Name der Datenbank]
spring.datasource.username=[Datenbankbenutzer]
spring.datasource.password=[Datenbankkennwort]
spring.datasource.driver-class-name=org.postgresql.Driver

Führen Sie es aus, um festzustellen, ob das Projekt ohne Probleme bereit ist. (Klicken Sie mit der rechten Maustaste auf das Projekt> Ausführen> Spring Boot-Anwendung.)

Stellen Sie sicher, dass die folgende Meldung im Konsolenprotokoll ausgegeben wird, und fahren Sie mit dem nächsten fort.

o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s):- -(Abkürzung)- -
jp.co.anyplus.TaskTrackerApplication     : Started [Anwendungsname] in [Numerischer Wert] seconds (JVM running for [Numerischer Wert])

Bildschirmimplementierung

Bibliothek

Geben Sie zunächst die erforderlichen Bibliotheken ein. Es gab jQuery als Erweiterung von JavaScript, Bootstrap für das Layout und Countdown-Timer für den Timer, also habe ich dies verwendet.

CountdownTimer (https://github.com/harshen/jQuery-countdownTimer/) Eine jQuery-Bibliothek mit einem vollständigen Satz von Countdown- und Countdown-Funktionen für Datum und Uhrzeit. Es war perfekt für diese App, da es alle Funktionen der Layoutspezifikation und Start / Stopp bietet.

html Diesmal habe ich nur einen Bildschirm gemacht.

Das Layout wird von Bootstrap festgelegt. Der Anzeigebereich des Timers wird durch "" bestimmt, und der Anzeigeort wird für die Teile des Coundown-Timers angegeben. Das Standard-CSS von CoundownTimer ist etwas unbefriedigend, daher bearbeite ich es, aber ich werde es hier weglassen.

index.html


<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

<link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css">
<link rel="stylesheet" type="text/css" href="/css/jQuery.countdownTimer.css" />

<title>TaskTracker</title>
</head>
<body>
    <main class="container">
        <h1>Task Tracker</h1>
        <p>Notieren Sie die Aufgabe.</p>
        <div class="container text-center">
            <div id="taskMainContainer" class="jumbotron col-md-8 mx-auto">
                <form id="TaskForm" th:object="${taskForm}">
                    <div class="row">
                        <div class="col display-4"><span id="timerView">50:00</span></div>
                    </div>
                    <div class="row mt-3 justify-content-md-center">
                        <div class="col-md-4 "><button id="startButton" type="button" class="btn btn-block btn-success" disabled>Start</button></div>
                        <div class="col-md-4 "><button id="stopButton" type="button" class="btn btn-block btn-danger" disabled>Stop</button></div>
                    </div>
                    <div class="row mt-3 justify-content-md-center">
                        <div class="col-md-4 p-1"><input th:field="*{category}" type="text" class="form-control" placeholder="Kategorie"></div>
                        <div class="col-md-8 p-1"><input th:field="*{taskName}" type="text" class="form-control" placeholder="Aufgabe"></div>
                        <div><input th:field="*{taskId}" type="text" class="form-control" hidden></div>
                    </div>
                </form>
            </div>
        </div>
    </main>
  
    <script src="/js/import.js"></script>
    <script src="/js/view-index.js"></script>
  </body>
</html>

JavaScript Ich habe import.js zum Importieren der Bibliothek und view-index.js zum Beschreiben der Bildschirmverarbeitung erstellt.

import.js


/**
 * import
 **/
document.write('<script type="text/javascript" src="/js/jquery-3.4.1.min.js"></script>');
document.write('<script type="text/javascript" src="/js/bootstrap.bundle.min.js"></script>');

view-index.js


/*--------------------------------
 * index.JS für HTML
 --------------------------------*/
document.write('<script type="text/javascript" src="/js/jQuery.countdownTimer.js"></script>');

$(function(){
    $("#timerView").countdowntimer({
        minutes :50,
        seconds :00,
        displayFormat : "MS",
        size : "xl",
        timeUp : taskFinish
    });
    $("#timerView").countdowntimer("stop", "stop");
    stopbtnOn();
});

/*--------------------------------
 *Aufgabe starten
 --------------------------------*/
 $('#startButton').on('click', taskStart);
 function taskStart(){
    startbtnOn();
    $("#timerView").countdowntimer("stop", "start");

    var form = $('#TaskForm').serializeArray();
    var formdata = {};
    jQuery.each(form, function(i, e) {
    	formdata[e.name] = e.value;
    });
    
    $.ajax({
        url:'/',
        type:'POST',
        contentType : 'application/json; charset=utf-8',
        data: JSON.stringify(formdata)
    }).done( (data) => {
        console.log("success");
        console.log(data);
        $('#taskId').val(data);
    }).fail( (data) => {
        console.log("fail");
        console.log(data);
    });
}

/*--------------------------------
 *Aufgabe stoppen
 --------------------------------*/
$('#stopButton').on('click', taskStop);
function taskStop(){
    stopbtnOn();
    $("#timerView").countdowntimer("stop", "stop");

    var form = $('#TaskForm').serializeArray();
    var formdata = {};
    jQuery.each(form, function(i, e) {
    	formdata[e.name] = e.value;
    });
    
    $.ajax({
        url:'/stop',
        type:'POST',
        contentType : 'application/json; charset=utf-8',
        data: JSON.stringify(formdata)
    }).done( (data) => {
        console.log("success");
        console.log(data);
    }).fail( (data) => {
        console.log("fail");
        console.log(data);
    });
}

/*--------------------------------
 *Aufgabe erledigt
 --------------------------------*/
function taskFinish(){
    setTimeout(() => {
        taskStop();
        alert("Finish!!");
        $("#timerView").countdowntimer("stop", "stop");
        stopbtnOn();
    }, 1000);
}

/*--------------------------------
 *Tastensteuerung: Wenn die Starttaste eingeschaltet ist
 --------------------------------*/
function startbtnOn(){
    $('#startButton').prop("disabled", true);
    $('#stopButton').prop("disabled", false);
}

/*--------------------------------
 *Tastensteuerung: Wenn die Stopptaste eingeschaltet ist
 --------------------------------*/
function stopbtnOn(){
    $('#startButton').prop("disabled", false);
    $('#stopButton').prop("disabled", true);
}

Serverseitige Implementierung

Modellschichtklasse

Erstellen Sie zunächst eine TasksEntity-Klasse zum Speichern von Tabellendaten und eine TasksRepository-Klasse zum Bearbeiten der in der Datenbank erstellten Tabelle.

TasksEntity.java


package jp.co.anyplus.Entity;

import java.sql.Timestamp;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="tasks")
public class TasksEntity {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	@Column(name="task_id")
	private Long taskId;
	@Column(name="task_name")
	private String taskName;
	@Column(name="task_category")
	private String taskCategory;
	@Column(name="task_starttime")
	private Timestamp taskStartTime;
	@Column(name="task_endtime")
	private Timestamp taskEndTime;
	@Column(name="userid")
	private String userid;
	
	public Long getTaskId() {
		return taskId;
	}
	public void setTaskId(Long taskId) {
		this.taskId = taskId;
	}
	public String getTaskName() {
		return taskName;
	}
	public void setTaskName(String taskName) {
		this.taskName = taskName;
	}
	public String getTaskCategory() {
		return taskCategory;
	}
	public void setTaskCategory(String taskCategory) {
		this.taskCategory = taskCategory;
	}
	public Timestamp getTaskStartTime() {
		return taskStartTime;
	}
	public void setTaskStartTime(Timestamp taskStartTime) {
		this.taskStartTime = taskStartTime;
	}
	public Timestamp getTaskEndTime() {
		return taskEndTime;
	}
	public void setTaskEndTime(Timestamp taskEndTime) {
		this.taskEndTime = taskEndTime;
	}
	public String getUserid() {
		return userid;
	}
	public void setUserid(String userid) {
		this.userid = userid;
	}
}

TasksRepository.java


package jp.co.anyplus.Repository;

import java.sql.Timestamp;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import jp.co.anyplus.Entity.TasksEntity;

@Repository
public interface TasksRepository extends JpaRepository<TasksEntity, Long> {

	@Query(value="select current_timestamp", nativeQuery = true)
	public Timestamp getCurrentTime();
	
	@Query(value="select * from tasks where task_id = :taskId"
			, nativeQuery = true)
	public TasksEntity getByTaskId(@Param("taskId")Long taskId);
}

Da das von TasksRepository.java geerbte JpaRepository die Funktion von Spring Data JPA verwenden kann (Referenzartikel) Es scheint, dass der Inhalt dieser Abfrage automatisch aus dem Methodennamen generiert werden kann, ohne SQL zu schreiben.

Controller-Schicht und geschäftsbezogene Klassen

Erstellen Sie um die Controller-Ebene herum, um von der Ansicht zum Modell zu gelangen. IndexController.java der Cotroller-Klasse, TaskRegisterService.java und TaskCancelService.java der Service-Klasse, Bildschirm <-> Wir haben insgesamt 4 Klassen von TaskForm.java für die Weitergabe von Aufgabeninformationen zwischen Servern vorbereitet.

IndexController.java


//Paket- und Importanweisungen wurden weggelassen

@Controller
public class IndexController {

	@Autowired
	TaskRegisterService registerService;
	@Autowired
	TaskCancelService cancelService;

	/**
	 *Erste Anzeigeverarbeitung
	 * 
	 * @param mav ModelAndView
	 * @Rückgabeinformationen zurückgeben
	 */
	@GetMapping("/")
	public ModelAndView init(ModelAndView mav) {
		TaskForm taskForm = new TaskForm();
		
		mav.addObject("taskForm", taskForm);
		mav.setViewName("index.html");
		return mav;
	}

	/**
	 *Prozess der Aufgabenregistrierung
	 * 
	 * @param model Model
	 * @param taskForm Aufgabeninformationsformular
	 * @return Neu zugewiesene Task-ID
	 */
	@PostMapping("/")
	@ResponseBody
	public String taskRegister(Model model, @RequestBody TaskForm taskForm) {

		try {

			TasksEntity regTask = registerService.registerTask(taskForm);
			
			return regTask.getTaskId().toString();

		} catch (Exception e) {
			e.printStackTrace();
			return "";
		}
	}
	
	/**
	 *Task-End-Verarbeitung
	 * 
	 * @param model Model
	 * @param taskForm Aufgabeninformationsformular
	 * @Verarbeitungsnachricht zurückgeben
	 */
	@PostMapping("/stop")
	@ResponseBody
	public String taskEnd(Model model, @RequestBody TaskForm taskForm) {
		
		try {
			cancelService.endTask(taskForm);
		} catch(RuntimeException e) {
			e.printStackTrace();
			return "error";
		}
		
		return "task canceling success";
	}

}

Ich habe die Klassen @ResponseBody, @ PostMapping / @ GetMapping und Model, die für die Kommunikation zwischen dem Bildschirm und dem Server verwendet werden, nicht verstanden. Wenn Sie verstehen, möchte ich die Quelle überprüfen und einen Qiita-Artikel verfassen.

TaskRegisterService.java


//Paket- und Importanweisungen wurden weggelassen

@Service
public class TaskRegisterService {

	@Autowired
	TasksRepository tasksRep;

	@Transactional(rollbackOn = Exception.class)
	public TasksEntity registerTask(TaskForm taskForm) throws RuntimeException {
		Timestamp currenttime = tasksRep.getCurrentTime();

		TasksEntity taskEntity = new TasksEntity();
		taskEntity.setTaskName(taskForm.getTaskName());
		taskEntity.setTaskCategory(taskForm.getCategory());
		taskEntity.setTaskStartTime(currenttime);
		taskEntity.setUserid("system");

		try {
			TasksEntity regTask = tasksRep.saveAndFlush(taskEntity);
			return regTask;
		} catch (RuntimeException e) {
			//Protokoll: Registrierung fehlgeschlagen.
			throw e;
		}
	}

}

TaskCancelService.java


//Paket- und Importanweisungen wurden weggelassen

@Service
public class TaskCancelService {

	@Autowired
	TasksRepository tasksRep;

	@Transactional(rollbackOn = Exception.class)
	public void endTask(TaskForm taskForm) throws RuntimeException {
		Timestamp currenttime = tasksRep.getCurrentTime();

		TasksEntity taskEntity = tasksRep.getByTaskId(Long.parseLong(taskForm.getTaskId()));
		
		taskEntity.setTaskId(Long.parseLong(taskForm.getTaskId()));
		taskEntity.setTaskEndTime(currenttime);

		try {
			tasksRep.saveAndFlush(taskEntity);
		} catch (RuntimeException e) {
			//Protokoll: Aktualisierung fehlgeschlagen.
			throw e;
		}
	}

}

TaskForm.java


//Paket- und Importanweisungen wurden weggelassen

@SuppressWarnings("serial")
public class TaskForm implements java.io.Serializable {

	private String taskId;
	private String category;
	private String taskName;
	
	public String getTaskId() {
		return taskId;
	}
	public void setTaskId(String taskId) {
		this.taskId = taskId;
	}
	public String getCategory() {
		return category;
	}
	public void setCategory(String category) {
		this.category = category;
	}
	public String getTaskName() {
		return taskName;
	}
	public void setTaskName(String taskName) {
		this.taskName = taskName;
	}
	
}

Recommended Posts

Die erste WEB-Anwendung mit Spring Boot-Making a Pomodoro Timer-
Erstellen Sie eine Webanwendung mit Javalin
Erstellen wir eine Buchverwaltungs-Webanwendung mit Spring Boot part1
Lassen Sie uns mit Spring Boot part3 eine Webanwendung für die Buchverwaltung erstellen
Lassen Sie uns mit Spring Boot part2 eine Webanwendung für die Buchverwaltung erstellen
Erstellen Sie mit Dropwizard eine einfache Webanwendung
Starten Sie die Entwicklung von Webanwendungen mit Spring Boot
Führen Sie die WEB-Anwendung mit Spring Boot + Thymeleaf aus
Erstellen Sie einen Web-API-Server mit Spring Boot
Erstellen Sie ein WEB-System mit Spring + Doma + H2DB
Melden Sie sich bei Spring Boot-Webanwendungen auf der Microsoft ID-Plattform an
Ich habe versucht, eine Webanwendung voller Fehler mit Spring Boot zu klonen
Erstellen Sie ein WEB-System mit Spring + Doma + H2DB + Thymeleaf
Etwa der Ablauf der Entwicklung von Webanwendungen mit Rails.
Erstellen Sie ein WEB-System mit Spring + Doma + H2DB Part 2
Eine Geschichte voller Grundlagen von Spring Boot (gelöst)
[Spring Boot] Erstellung von Webanwendungen
Überprüfen Sie die Funktion von zwei Rollen mit einer Chat-Anwendung
Erstellen Sie eine Hello World-Webanwendung mit Spring Framework + Jetty
Bis Sie eine Webanwendung mit Servlet / JSP erstellen (Teil 1)
Mit Docker erstellte Webanwendung (1)
Erstmaliges Modellieren von Digimon mit DDD Teil 1
AWS Elastic Beanstalk # 1 mit Java von Grund auf neu Erstellen einer Java-Webanwendungsumgebung mit EB CLI-
Implementieren Sie einen einfachen Web-REST-API-Server mit Spring Boot + MySQL
Erstellen Sie einen einfachen Webserver mit der Java-Standardbibliothek com.sun.net.httpserver
Die Geschichte des Refactorings mit einem selbstgemachten Helfer zum ersten Mal in einer Rails-App
Lesen Sie die Datei unter dem Klassenpfad als Zeichenfolge mit spring
[Wahrscheinlich die einfachste] WEB-Anwendungsentwicklung mit Apache Tomcat + Java Servlet
Was ist der Unterschied zwischen einem Webserver und einem Anwendungsserver?
Eine Geschichte, die mit der Einführung von Web Apple Pay zu kämpfen hatte
Erstellen Sie eine Anfrage-App mit Spring Boot
Erstellen einer Timer-App mit Schlamm
Memo nach dem ersten Frühjahrsprojekt-MVC-
Spring AOP zum ersten Mal
Memo nach dem ersten Frühjahrsprojekt-Datenbank-
[Spring Boot] Vorsichtsmaßnahmen beim Entwickeln einer Webanwendung mit Spring Boot und beim Ausführen eines Krieges auf einem unabhängigen Tomcat-Server
Erstellen Sie mit dem Befehl cURL + tar ein Spring Boot-App-Entwicklungsprojekt
So überprüfen Sie, bevor Sie mit Spring Integration eine Nachricht an den Server senden
Spring Boot2-Webanwendungsentwicklung mit Visual Studio Code SQL Server-Verbindung
Spring5 MVC-Webanwendungsentwicklung mit Visual Studio Code SQL Server-Verbindung
Erstellen einer Entwicklungsumgebung für Java-Webanwendungen mit Docker für Mac Teil1
Java-Anfänger haben versucht, mit Spring Boot eine einfache Webanwendung zu erstellen
Spring Boot2-Webanwendungsentwicklung mit Visual Studio Code Hello World-Erstellung
Automatische Bereitstellung von in Java entwickelten Webanwendungen mit Jenkins [Spring Boot App Edition]
WAR die WEB-Anwendung per Spring Boot und stellen Sie sie auf dem Tomcat-Server bereit
Entwicklung von Spring5 MVC-Webanwendungen mit Visual Studio Code Maven-Vorlagenerstellung
[Java] Stellen Sie eine mit Eclipse + Maven + Ontology mit Heroku erstellte Webanwendung bereit
Erstellen Sie mit Docker für Mac Teil2 eine Entwicklungsumgebung für Java-Webanwendungen
Senden Sie nach eindeutiger Authentifizierung mit Spring Cloud Gateway eine Anfrage an das Backend
Verarbeitung beim Starten einer Anwendung mit Spring Boot
Erstellen Sie mit dem Befehl eine JAR-Datei
Memo nach dem ersten Frühlingsprojekt-Was ist Frühling-
Starten Sie die Nginx + Spring Boot-Anwendung mit Docker-Compose
Führen Sie DMN mit der Camunda DMN Engine aus
CI / CD Spring-Anwendung mit CircleCI (Heroku)
Erstellen wir eine Taschenrechner-App mit Java ~ Erstellen Sie einen Anzeigebereich im Fenster
Spring 5 MVC-Webanwendungsentwicklung mit Visual Studio Code Spring Security-Verwendung 2/3 [Seitenerstellung 1/2]