Ergänzungen zum Datenbank-Programm mit vertx

== Ergänzungen zum Datenbank-Programm mit vertx

In diesem Abschnitt werden einige Techniken beleuchtet, die im Datenbankprogramm Eingang finden.

Diese sind sinnvoll, um für die weiteren Programmieraufgaben eine solide Basis zu haben. Einiges mag im ersten Moment aufwändig erscheinen, bewährt sich aber mit steigender Komplexität der Web-Anwendung.

Der Code der Anwendung befindet sich hier.

In diesem Abschnitt werden einige Techniken beleuchtet, die im Datenbankprogramm Eingang finden. Diese sind sinnvoll, um für die weiteren Programmieraufgaben eine solide Basis zu haben. Einiges mag im ersten Moment aufwändig erscheinen, bewährt sich aber mit steigender Komplexität der Web-Anwendung. Der Code der Anwendung befindet sich https://github.com/menzelths/VertxWeb/tree/master/src/main/java/de/qreator/vertx/VertxDatabase[icon:external-link[]hier].

Logger

=== Logger

Insbesondere während der Entwicklung ist es praktisch, wenn ein Programm an bestimmten Stellen Informationen ausgibt. Dies ist in Java natürlich über System.out.println() möglich, allerdings gibt es für dieses Szenario auch eine häufig verwendete Bibliothek, die speziell für die Ausgabe von Logging-Informationen konzipiert wurde. Der Vorteil liegt darin, dass diese flexibel zu konfigurieren und einfach einzusetzen ist.

Im Folgenden wird die Bibliothek Logback vorgestellt, die in vielen Projekten Verwendung findet.

Per Maven kann die Bibliothek über

Insbesondere während der Entwicklung ist es praktisch, wenn ein Programm an bestimmten Stellen Informationen ausgibt. Dies ist in Java natürlich über `System.out.println()` möglich, allerdings gibt es für dieses Szenario auch eine häufig verwendete Bibliothek, die speziell für die Ausgabe von Logging-Informationen konzipiert wurde. Der Vorteil liegt darin, dass diese flexibel zu konfigurieren und einfach einzusetzen ist. Im Folgenden wird die Bibliothek https://logback.qos.ch/[icon:external-link[]Logback] vorgestellt, die in vielen Projekten Verwendung findet. Per Maven kann die Bibliothek über
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>
XML
[source, xml, indent=0] ---- <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> ----

eingebunden werden.

In jedem Programmteil, der Logging-Informationen ausgeben soll, wird eine Instanz des Loggers erstellt, z. B.

eingebunden werden. In jedem Programmteil, der Logging-Informationen ausgeben soll, wird eine Instanz des Loggers erstellt, z. B.
Logger LOGGER = LoggerFactory.getLogger("meinLogger");
Java
[source, Java,indent=0] ---- Logger LOGGER = LoggerFactory.getLogger("meinLogger"); ----

In diesem Fall heißt der Logger meinLogger, was dazu genutzt werden kann, um über den Namen eine Konfiguration für diesen Logger vorzunehmen. Dazu gleich mehr, zunächst aber: wie wird der Logger verwendet?

In diesem Fall heißt der Logger `meinLogger`, was dazu genutzt werden kann, um über den Namen eine Konfiguration für diesen Logger vorzunehmen. Dazu gleich mehr, zunächst aber: wie wird der Logger verwendet?
LOGGER.info("Programm wurde erfolgreich gestartet");
Java
[source, java,indent=0] ---- LOGGER.info("Programm wurde erfolgreich gestartet"); ----

Das info() legt fest, dass der Text nur dann ausgegeben wird, wenn das Debug-Level auch die Ausgabe von Informationen ausgibt.

Wenn ein Fehler ausgegeben werden soll, so würde man das folgendermaßen machen:

Das `info()` legt fest, dass der Text nur dann ausgegeben wird, wenn das Debug-Level auch die Ausgabe von Informationen ausgibt. Wenn ein Fehler ausgegeben werden soll, so würde man das folgendermaßen machen:
LOGGER.error("Es ist ein Fehler aufgetreten!");
Java
[source,java,indent=0] ---- LOGGER.error("Es ist ein Fehler aufgetreten!"); ----

Neben info und error gibt es noch weitere Debug-Level:

Neben `info` und `error` gibt es noch weitere Debug-Level:

TRACE < DEBUG < INFO < WARN < ERROR

`TRACE < DEBUG < INFO < WARN < ERROR`

Aus vertx heraus kann man in der Konfigurationsdatei logback.xml direkt in source/main/resources festlegen, ab welchem Debug-Level unser Logger meinLogger etwas ausgibt. So könnte dort unter anderem beispielsweise die folgende Zeile stehen:

Aus vertx heraus kann man in der Konfigurationsdatei `logback.xml` direkt in `source/main/resources` festlegen, ab welchem Debug-Level unser Logger _meinLogger_ etwas ausgibt. So könnte dort unter anderem beispielsweise die folgende Zeile stehen:
<logger name="meinLogger" level="info"/>
XML
[source,xml,indent=0] ---- <logger name="meinLogger" level="info"/> ----

Das bedeutet, dass unser Logger immer dann etwas ausgibt, wenn es sich vom Level her mindestens um eine info handelt. Ausgaben auf trace-Level würden ignoriert werden.

Das bedeutet, dass unser Logger immer dann etwas ausgibt, wenn es sich vom Level her mindestens um eine `info` handelt. Ausgaben auf `trace`-Level würden ignoriert werden.

Asynchrone Programmierung

=== Asynchrone Programmierung

vertx zeichnet sich dadurch aus, dass sehr viele Anfragen von Clients an den Server gestellt werden können, ohne dass dieser Probleme hätte, diese Anfragen zu beantworten. Dabei ist es besonders wichtig, dass die Abarbeitung einer Anfrage nicht die Anfragen anderer Clients verlangsamt.

Ein gutes Beispiel dafür ist der Zugriff auf eine Datenbank: bis eine komplexe Suchanfrage abgearbeitet wurde, können schon mal mehrere Sekunden vergehen. Wenn in dieser Zeit keine anderen Anfragen bearbeitet werden könnten, wäre das für eine Web-Seite von Nachteil.

Aus diesem Grund ist es üblich, dass beispielsweise eine Datenbankabfrage zwar aufgerufen wird, das Hauptprogramm danach aber direkt fortfährt. Sobald die Daten der Datenbank vorliegen, werden die nächsten passenden Schritte durchgeführt.

Man wartet also nicht ab, bis das Ergebnis vorliegt und blockiert dabei alles andere (das wäre synchron), sondern lässt die Datenbankabfrage parallel zum Hauptprogramm laufen und wenn die Ergebnisse vorliegen, werden diese erst dann weiterverarbeitet. Es lässt sich nicht vorhersagen, wann die Rückmeldung der Datenbank eintrifft (asynchron).

Manchmal ist es notwendig, dass man einen asynchronen Aufruf abwartet, ehe man einen weiteren asynchronen Aufruf startet. Ein Beispiel wäre, dass man eine Datenbank zunächst anlegt (asynchroner Prozess) und wenn dies geschehen ist, könnten erste Benutzer darin angelegt werden (wieder ein asynchroner Prozess).

Für solche Zwecke sind Futures in vertx ein brauchbares Mittel:

vertx zeichnet sich dadurch aus, dass sehr viele Anfragen von Clients an den Server gestellt werden können, ohne dass dieser Probleme hätte, diese Anfragen zu beantworten. Dabei ist es besonders wichtig, dass die Abarbeitung einer Anfrage nicht die Anfragen anderer Clients verlangsamt. Ein gutes Beispiel dafür ist der Zugriff auf eine Datenbank: bis eine komplexe Suchanfrage abgearbeitet wurde, können schon mal mehrere Sekunden vergehen. Wenn in dieser Zeit keine anderen Anfragen bearbeitet werden könnten, wäre das für eine Web-Seite von Nachteil. Aus diesem Grund ist es üblich, dass beispielsweise eine Datenbankabfrage zwar aufgerufen wird, das Hauptprogramm danach aber direkt fortfährt. Sobald die Daten der Datenbank vorliegen, werden die nächsten passenden Schritte durchgeführt. Man wartet also nicht ab, bis das Ergebnis vorliegt und blockiert dabei alles andere (das wäre _synchron_), sondern lässt die Datenbankabfrage parallel zum Hauptprogramm laufen und wenn die Ergebnisse vorliegen, werden diese erst dann weiterverarbeitet. Es lässt sich nicht vorhersagen, wann die Rückmeldung der Datenbank eintrifft (_asynchron_). Manchmal ist es notwendig, dass man einen asynchronen Aufruf abwartet, ehe man einen weiteren asynchronen Aufruf startet. Ein Beispiel wäre, dass man eine Datenbank zunächst anlegt (asynchroner Prozess) und wenn dies geschehen ist, könnten erste Benutzer darin angelegt werden (wieder ein asynchroner Prozess). Für solche Zwecke sind `Futures` in vertx ein brauchbares Mittel:
public void start(Future<Void> startFuture) {
	Future<Void> datenbankFuture = erstelleDatenbank()
	.compose(db -> erstelleUser("user", "geheim"));

	datenbankFuture.setHandler(db -> {
		if (db.succeeded()) {
			LOGGER.info("Datenbank initialisiert");
			startFuture.complete();
		} else {
			LOGGER.info("Probleme beim Initialisieren der Datenbank");
			startFuture.fail(db.cause());
		}
	});
}
Java
[source,java,indent=0] ---- public void start(Future<Void> startFuture) { Future<Void> datenbankFuture = erstelleDatenbank() .compose(db -> erstelleUser("user", "geheim")); datenbankFuture.setHandler(db -> { if (db.succeeded()) { LOGGER.info("Datenbank initialisiert"); startFuture.complete(); } else { LOGGER.info("Probleme beim Initialisieren der Datenbank"); startFuture.fail(db.cause()); } }); } ----
1 startFuture wird erstellt, um Informationen aufzunehmen, ob der gesamte Prozess funktioniert hat oder nicht
2 Zunächst wird die Datenbank erstellt (asynchron) und …​
3 …​ mittels compose im Anschluss auch ein neuer Benutzer erzeugt (auch asynchron).
4 datenbankFuture enthält Informationen, ob die Operationen erfolgreich waren:
5 succeeded nur dann, wenn beide Aufrufe (erstelleDatenbank() und erstelleUser()) erfolgreich waren
6 Man kann auch von Hand festlegen, dass eine asynchrone Operation erfolgreich war mittels complete
7 Im Falle eines Fehlschlags kann man mittels fail eine Fehlermeldung an die aufrufende Instanz weitergeben. Dort wird sie mittels cause ausgelesen.
<1> `startFuture` wird erstellt, um Informationen aufzunehmen, ob der gesamte Prozess funktioniert hat oder nicht <2> Zunächst wird die Datenbank erstellt (asynchron) und ... <3> ... mittels `compose` im Anschluss auch ein neuer Benutzer erzeugt (auch asynchron). <4> `datenbankFuture` enthält Informationen, ob die Operationen erfolgreich waren: <5> `succeeded` nur dann, wenn beide Aufrufe (`erstelleDatenbank()` und `erstelleUser()`) erfolgreich waren <6> Man kann auch von Hand festlegen, dass eine asynchrone Operation erfolgreich war mittels `complete` <7> Im Falle eines Fehlschlags kann man mittels `fail` eine Fehlermeldung an die aufrufende Instanz weitergeben. Dort wird sie mittels `cause` ausgelesen.

Die obige Methode kann von außen beispielsweise aufgerufen werden über:

Die obige Methode kann von außen beispielsweise aufgerufen werden über:
Future<String> starteDatenbankVerticle = Future.future();

vertx.deployVerticle(new DatenbankVerticle()); (1)

starteDatenbankVerticle.setHandler(db->{ (2)
	if (db.succeeded()) {
		LOGGER.info("War erfolgreich!");
	else {
		LOGGER.info("Fehlgeschlagen: "+db.cause());
	}
});
Java
[source,java,indent=0] ---- Future<String> starteDatenbankVerticle = Future.future(); vertx.deployVerticle(new DatenbankVerticle()); //<1> starteDatenbankVerticle.setHandler(db->{ //<2> if (db.succeeded()) { LOGGER.info("War erfolgreich!"); else { LOGGER.info("Fehlgeschlagen: "+db.cause()); } }); ----
1 Ruft automatisch die start-Methode im Datenbank-Verticle auf.
2 Dieser Abschnitt wird aufgerufen, wenn der Aufruf der Datenbank zurückkehrt.
<1> Ruft automatisch die `start`-Methode im Datenbank-Verticle auf. <2> Dieser Abschnitt wird aufgerufen, wenn der Aufruf der Datenbank zurückkehrt.

Der Eventbus

=== Der Eventbus

Unter dem Eventbus kann man sich die Kommunikationszentrale von vertx vorstellen. Darüber können unterschiedliche Programmteile von vertx miteinander kommunizieren, auch über mehrere Rechner hinweg.

Die Grundidee ist denkbar einfach: Für jedes Thema wird im einfachsten Fall eine Adresse festgelegt.

An diese Adresse kann man etwas versenden und die Gegenstelle kann bei Erhalt einer Nachricht darauf reagieren und gegebenenfalls eine Antwort zurückschicken.

Die Adresse könnte lauten:

Unter dem _Eventbus_ kann man sich die Kommunikationszentrale von vertx vorstellen. Darüber können unterschiedliche Programmteile von vertx miteinander kommunizieren, auch über mehrere Rechner hinweg. Die Grundidee ist denkbar einfach: Für jedes Thema wird im einfachsten Fall eine Adresse festgelegt. An diese Adresse kann man etwas versenden und die Gegenstelle kann bei Erhalt einer Nachricht darauf reagieren und gegebenenfalls eine Antwort zurückschicken. Die Adresse könnte lauten:
String EB_ADRESSE = "vertxdatabase.eventbus";
Java
[source,java,indent=0] ---- String EB_ADRESSE = "vertxdatabase.eventbus"; ----

Hier wird die Anfrage vom Http-Server bearbeitet und die Passwortkontrolle an den Datenbank-Teil übergeben per send:

Hier wird die Anfrage vom Http-Server bearbeitet und die Passwortkontrolle an den Datenbank-Teil übergeben per `send`:
JsonObject request = new JsonObject().put("name", name)
.put("passwort", passwort);

DeliveryOptions options = new DeliveryOptions()
.addHeader("action", "ueberpruefe-passwort");
vertx.eventBus().send(EB_ADRESSE, request, options, reply -> {
	if (reply.succeeded()) {
		JsonObject body = (JsonObject) reply.result().body();
		if (body.getBoolean("passwortStimmt") == true) {
			session.put("angemeldet", "ja");
			jo.put("typ", "überprüfung").put("text", "ok");
		} else {
			jo.put("typ", "überprüfung").put("text", "nein");
		}
		response.end(Json.encodePrettily(jo));
	} else {
		jo.put("typ", "überprüfung").put("text", "nein");
		response.end(Json.encodePrettily(jo));
	}
});
Java
[source,java,indent=0] ---- JsonObject request = new JsonObject().put("name", name) .put("passwort", passwort); DeliveryOptions options = new DeliveryOptions() .addHeader("action", "ueberpruefe-passwort"); vertx.eventBus().send(EB_ADRESSE, request, options, reply -> { if (reply.succeeded()) { JsonObject body = (JsonObject) reply.result().body(); if (body.getBoolean("passwortStimmt") == true) { session.put("angemeldet", "ja"); jo.put("typ", "überprüfung").put("text", "ok"); } else { jo.put("typ", "überprüfung").put("text", "nein"); } response.end(Json.encodePrettily(jo)); } else { jo.put("typ", "überprüfung").put("text", "nein"); response.end(Json.encodePrettily(jo)); } }); ----

Beim Empfänger muss festgelegt werden, dass dort Nachrichten an die gewünschte Adresse angenommen werden:

Beim Empfänger muss festgelegt werden, dass dort Nachrichten an die gewünschte Adresse angenommen werden:
vertx.eventBus().consumer(EB_ADRESSE, this::onMessage);
Java
[source,java,indent=0] ---- vertx.eventBus().consumer(EB_ADRESSE, this::onMessage); ----

In der entsprechenden Methode onMessage werden die Nachrichten verarbeitet und beantwortet:

In der entsprechenden Methode `onMessage` werden die Nachrichten verarbeitet und beantwortet:
private void onMessage(Message<JsonObject> message) {

	String action = message.headers().get("action");
	if (action.equals("ueberpruefe-passwort")){
		String name = message.body().getString("name");
		String passwort = message.body().getString("passwort");

		if (passwort.equals("geheim")&&name.equals("user")){
			message.reply(new JsonObject().put("passwortStimmt", Boolean.TRUE));
		} else {
			message.reply(new JsonObject().put("passwortStimmt", Boolean.FALSE));
		}
	}
}
Java
[source,java,indent=0] ---- private void onMessage(Message<JsonObject> message) { String action = message.headers().get("action"); if (action.equals("ueberpruefe-passwort")){ String name = message.body().getString("name"); String passwort = message.body().getString("passwort"); if (passwort.equals("geheim")&&name.equals("user")){ message.reply(new JsonObject().put("passwortStimmt", Boolean.TRUE)); } else { message.reply(new JsonObject().put("passwortStimmt", Boolean.FALSE)); } } } ----