Functional style CRUD für Minio (S3) in einer Vaadin-App
Heute werden wir uns ansehen, wie mit funktionalen Aspekten das Minio-API für den Zugriff auf einen S3-kompatiblen Speicher in einer Vaadin-App konsumiert werden kann. Ziel ist es, aus dem S3-Speicher Blobs zu laden und in einer Vaadin-App anzuzeigen. Bei den Blobs handelt es sich um Bilder im jpg-Format.
Um mit der Anwendung zu beginnen, muss man in das Verzeichnis _data wechseln und dort das Kommando docker-compose up ausführen. Hiermit wird in dem definierten Docker-Host ein cluster bestehend aus 4 Minio-Nodes erzeugt und mit einem Proxy, der als rudimentärer Load Balancer fungiert, ausgestattet.
Nachdem alles erfolgreich gestartet worden ist – was bei dem ersten Start ein wenig dauern kann – da die Images erst aus dem Internet gezogen werden müssen, ist über die URL [ht tp://localhost:9999] ein erster Anmeldevorgang möglich.
In dem Cluster wurde die Kombination minio/minio123 als Zuganskombination zugewiesen. Willkommen bei einem frischen S3-Storage!
Die dazu gehörige Vaadin-App kann klassisch mit einem mvn clean install im Projekt-Hauptverzeichnis erzeugt werden. Der Start der Anwendung erfolgt mittels Aufruf der main-Methode in der Klasse JumpstartUI.
S3-Storage – Minio
Das hier verwendete Open Source-Projekt ist auf github zu finden [1]. Mittels dieses Projekts kann man beginnen, einen Dokumenten-Storage für unsere Vaadin-App aufzubauen. Auf der Projektseite selbst kann man folgendes nachlesen: "It is best suited for storing unstructured data such as photos, videos, log files, backups and container / VM images. Size of an object can range from a few KBs to a maximum of 5TB."
Beginnen wir jedoch zuerst mit einem einzigen Knoten und probierenden S3-Storage ein wenig aus. In diesem Beispiel wird Docker und Docker-Compose verwendet. Die Installation ist hier allerdings nicht beschrieben. Da verweise ich auf die gängige, online verfügbare Literatur.
Der erste Schritt besteht darin, das Image minio in den verwendeten Docker-Host zu holen. Das kann man wie gewohnt mit dem Docker-Kommando docker pull minio/minio erledigen.
Sobald das Image vorhanden ist, kann der erste Container damit erzeugt werden. Es ist meist empfehlenswert, bei den ersten Versuchen rein transient zu arbeiten. Demnach werden keine Verzeichnisse in den Docker-Container eingebunden. Das hat zur Folge, dass die in dem Minio-Knoten gespeicherten Daten verloren sind, sobald der Container gelöscht wird.
Der Befehl zum starten lautet wie folgt.
docker run -p 9000:9000 --name minio minio/minio server /data
Hier wird der Port 9000 nach außen freigegeben, der Container unter dem Namen minio erzeugt und lediglich das intern verfügbare Verzeichnis /data zur Ablage der Daten verwendet. Werden mehrere Volumes verwendet, zum Beispiel weil mehrere Platten in dem System vorhanden sind, kann man wie folgt vorgehen. Pro Knoten können bis zu 16 Volumes angegeben werden die aktiv verwendet werden.
docker run -p 9000:9000 --name minio \ -v /mnt/data1:/data1 \ -v /mnt/data2:/data2 \ -v /mnt/data3:/data3 \ -v /mnt/data4:/data4 \ -v /mnt/data5:/data5 \ -v /mnt/data6:/data6 \ -v /mnt/data7:/data7 \ -v /mnt/data8:/data8 \ minio/minio server /data1 /data2 /data3 /data4 /data5 /data6 /data7 /data8
Im folgenden wurde die erste einfache Version verwendet, um den Server-Knoten zu starten. In den Log-Dateien sind dann die neu erzeugten Zugangsdaten zu finden. Das kann dann wie folgt aussehen.
Drive Capacity: 50 GiB Free, 60 GiB Total Endpoint: ht tp://172.17.0.2:9000 ht tp://127.0.0.1:9000 AccessKey: 1TEQLU3S6N19ID4A32QJ SecretKey: KQamn/OWyGZPnuGq+1ZNYgRZqJLeiAJ06bJwNmJ9 Browser Access: ht tp://172.17.0.2:9000 ht tp://127.0.0.1:9000 Command-line Access: ht tps://docs.minio.io/docs/minio-client-quickstart-guide $ mc config host add myminio ht tp://172.17.0.2:9000 1TEQLU3S6N19ID4A32QJ KQamn/OWyGZPnuGq+1ZNYgRZqJLeiAJ06bJwNmJ9 Object API (Amazon S3 compatible): Go: ht tps://docs.minio.io/docs/golang-client-quickstart-guide Java: ht tps://docs.minio.io/docs/java-client-quickstart-guide Python: ht tps://docs.minio.io/docs/python-client-quickstart-guide JavaScript: ht tps://docs.minio.io/docs/javascript-client-quickstart-guide .NET: ht tps://docs.minio.io/docs/dotnet-client-quickstart-guide
Wichtig hierbei sind die folgend nochmals extra aufgeführten Daten.
* Endpoint: ht tp://172.17.0.2:9000 ht tp://127.0.0.1:9000 * AccessKey: 1TEQLU3S6N19ID4A32QJ * SecretKey: KQamn/OWyGZPnuGq+1ZNYgRZqJLeiAJ06bJwNmJ9
Mit diesen Zugangsdaten kann man nun den ersten Anmeldeversuch unternehmen. Hierzu in einem Browser die URL ht tp://127.0.0.1:9000 eingeben. Als Anmeldedaten nun die Werte für AccessKey und SecretKey eingeben. Nach der ersten Anmeldung sieht man dann als Belohnung einen fast leeren Bildschirm. Da nun alle notwendigen Vorbereitungen erledigt sind, kann mit der Exploration des JAVA-API begonnen werden.
Da nun alle notwendigen Vorbereitungen erledigt sind, kann mit der Exploration des JAVA-API begonnen werden.
S3-Storage – Minio – Java-SDK
Das zu Minio gehörende JAVA-SDK ist in Maven Central vorgehalten. Die dazu notwendigen Koordinaten sind (wobei sich die Versionsnummer natürlich mittlerweile schon erhöht haben kann):
<dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>3.0.12</version> </dependency>
In dem SDK ist eine Klasse mit den Namen **MinioClient**. Mittels dieser Klasse werden alle Kommandos zu dem S3 Storage abgesetzt. Um eine initialisierte Instanz dieser Klasse zu erhalten, werden ebenfalls die Anmeldedaten wie bei dem Anmeldeversuch mittels Browser benötigt.
final MinioClient minioClient = new MinioClient( "https://localhost:9000", "1TEQLU3S6N19ID4A32QJ", "KQamn/OWyGZPnuGq+1ZNYgRZqJLeiAJ06bJwNmJ9");
Nun ist die Verbindung hergestellt und es kann damit begonnen werden, die Funktionalitäten auszuprobieren. Einige grundlegenden Beispiele sind in der Klasse MinioBasicTest in den Test-Sourcen dieses Projekts zu finden.
S3-Storage – Minio – mit Docker Compose
Minio kann auch in einer Variante gestartet werden, in der ein Verbund von Knoten zu einem Cluster zusammengeschaltet wird. Um das zu zeigen und die ersten Schritte zu vereinfachen, wurde dem Projekt ein docker-compose-File hinzugefügt. Diese Datei ist in dem Verzeichnis _data/ zu finden. Mit dem Kommandozeilenbefehl docker-compose up wird ein Minio-Cluster erzeugt, bei dem jeder Knoten ein Datenverzeichnis transient vorhält. Dem Cluster wird eine nginx-Instanz als Proxy vorgeschaltet. Der Proxy fungiert auch als ein sehr einfacher LoadBalancer. Der Cluster, bzw der nginx ist über localhost Port 9999 zu erreichen. Dort wird man dann wieder zur Eingabe von Access- und Sec-Key aufgefordert. In diesem Setup wurden diese beiden Werte allerdings fest auf minio/minio123 gesetzt. Diese Information ist auch im docker-compose-File verfügbar. Allerdings muss einem bewusst sein, dass diese Einstellung selbstverständlich in einem regulären System durch andere Were ersetzt werden muss.
Die Vaadin-App
Da nun alle Infrastrukturelemente vorhanden sind, kann mit der Entwicklung der Vaadin-App begonnen werden. In diesem Beispiel findet die Vaadin-Version 8 Verwendung – eine Portierung auf Vaadin10 beta sollte allerdings keinen großen Aufwand bedeuten. Wir werden uns in einem späteren Artikel mit dem Thema Vaadin 10 noch ausführlich auseinandersetzen.
Die Funktionalität selbst ist sehr einfach und im Umfang sehr überschaubar. Der Benutzer soll die Kontaktinformationen zu dem S3-Storage eingeben können inklusive des Bucket-namen, in dem sich die Bilder, die angezeigt werden sollen, befinden. In diesem Beispiel gehen wir erst einmal davon aus, dass die Bilder, die angezeigt werden können, bekannt sind.
Nachdem die Verbindung hergestellt worden ist, soll alle paar Sekunden ein Bild aus der Menge von dem Server zufällig ausgewählt werden. Dieses Bild wird dann zum Browser übermittelt, um dort angezeigt zu werden.
Vaadin Ramp Up
Kommen wir nun zu dem Punkt, an dem wir die Vaadin-Applikation starten. Wir benötigen einen Servletcontainer. Hier wird ein Undertow verwendet. Der Start des Containers erfolgt in der Main-Methode der Klasse CoreUIService. Wer mehr zu den Details lesen möchte, dem empfehle ich das sehr kleine Open Source-Projekt nano-vaadin, das auf github zu finden ist [2].
In wenigen Worten erklärt passiert hier folgendes. Es wird in einer Main-Methode der Undertow als Servletcontainer hochgefahren. Darin wird das Vaadin-Servlet initialisiert und mit einer UI-Klasse mittels Annotation verbunden. Da die Verwendung einer Annotation nur in der statischen Semantik Auswirkungen hat, wird dort ein Supplier erzeugt. Dieser lädt – basierend auf einem Property, das global gesetzt worden ist – die angegebene Klasse. Nachfolgend instantiiert und verwendet er die Klasse, um die Root-Component zu erzeugen und zu setzen.
@FunctionalInterface public static interface ComponentSupplier extends Supplier<Component> { } @PreserveOnRefresh @Push public static class MyUI extends UI implements HasLogger { public static final String COMPONENT_SUPPLIER_TO_USE = "COMPONENT_SUPPLIER_TO_USE"; @Override protected void init(VaadinRequest request) { final String className = System.getProperty(COMPONENT_SUPPLIER_TO_USE); logger().info("class to load : " + className); ((CheckedSupplier<Class<?>>) () -> forName(className)) .get() //TODO make it fault tolerant .flatMap((CheckedFunction<Class<?>, Object>) Class::newInstance) .flatMap((CheckedFunction<Object, ComponentSupplier>) ComponentSupplier.class::cast) .flatMap((CheckedFunction<ComponentSupplier, Component>) Supplier::get) .ifPresentOrElse(this::setContent, failed -> logger().warning(failed) ); } }
Als Entwickler leitet man von der Klasse CoreUIService ab und implementiert das Interface ComponentSupplier. Die Implementierung besteht hier darin, auf die Komponente DashboardComponent zu delegieren.
public class JumpstartUI extends CoreUIService implements HasLogger { static { setProperty(COMPONENT_SUPPLIER_TO_USE, MySupplier.class.getName()); } public static class MySupplier implements CoreUIService.ComponentSupplier { @Override public Component get() { return new DashboardComponent().postConstruct(); } } }
In der Klasse DashboardComponent ist in diesem Beispiel die gesamte UI implementiert. Der statische Teil besteht lediglich aus den benötigten Eingabefeldern.
private final TextField accessPoint = new TextField("Access point"); private final TextField accessKey = new TextField("accessKey"); private final TextField secKey = new TextField("secKey"); private final TextField bucketName = new TextField("bucketName"); private final Button connect = new Button("connect"); private final FormLayout layout = new FormLayout(accessPoint, accessKey, secKey, bucketName, connect ); private Image image = new Image(); private Layout mainLayout = new VerticalLayout(layout, image); public DashboardComponent() { setCompositionRoot(mainLayout); }
Um nun den dort verwendeten Button mit der benötigten Logik zu versehen, sehen wir uns nun ein wenig das API an, das uns die Klasse MinioClient bietet. Was hier auffallend ist, ist die sehr ausgeprägte Verwendung von Exceptions. Das führt in der Verwendung leider zu einer Menge an try-catch Blöcken.
Nun kann man die gesamte Logik in ein einziges Try-catch einbetten oder beginnen, mit geschachtelten Blöcken zu arbeiten. Wie man sich auch entscheiden möge, es wird sehr schnell unübersichtlich. Jedoch können die repetetiven Elemente extrahiert werden. Das wurde dann auch gemacht und ein wenig funktional ausgestaltet. Hierbei verwende ich das kleine Open Source-Projekt "functional-reactive" [3].
Nachfolgend einfach ein paar Beispiele, die wir für ein simples CRUD benötigen werden.
creating an MinioClient instance
static CheckedFunction<Coordinates, MinioClient> client() { return (coord) -> new MinioClient(coord.endpoint(), coord.accessKey(), coord.secretKey() ); }
create a bucket if not already exists
static CheckedBiFunction<MinioClient, String, MinioClient> bucket() { return (minioClient, bucketName) -> { if (!minioClient.bucketExists(bucketName)) { minioClient.makeBucket(bucketName); } return minioClient; }; }
put Object to bucket
static CheckedBiFunction<MinioClient, Blob, MinioClient> putObj() { return (minioClient, obj) -> { minioClient.putObject(obj.getT1(), obj.getT2(), obj.getT3(), obj.getT4() ); return minioClient; }; }
get Object from bucket
static CheckedBiFunction<MinioClient, BlobCoordinates, InputStream> getObj() { return (minioClient, obj) -> minioClient .getObject( obj.bucketName(), obj.objectName() ); }
Von S3 zu Vaadin
Mit den gerade gezeigten Funktionen kann nun sehr übersichtlich die Anbindung an den S3-Storage erfolgen. Das Verpacken der Verbindungsdaten, die in den Eingabefeldern vorgehalten werden, in eine Instanz der Klasse Coordinates wird in einem Supplier definiert, der dann zu gegebener Zeit innerhalb des UI-Thread ausgeführt werden wird.
private Supplier<Coordinates> access() { return () -> new Coordinates(accessPoint.getValue(), accessKey.getValue(), secKey.getValue() ); }
Die Verbindung wird dann erzeugt, nachdem der Button gedrückt worden ist. In diesem Moment wird ebenfalls eine Registration erzeugt, die es dem TimerService ermöglicht, die Information, welches Bild als nächstes angezeigt werden soll, an alle Abonnenten zu verteilen.
Sobald eine neue imageID empfangen wird, wird der dazu passende Blob aus dem S3-Storage geladen. Allerdings muss diese Datei nachfolgend in eine StreamResouce verpackt werden, die dann als Content der Instanz der Klasse Image gesetzt wird. Vaadin selbst wird dann mittels Push den Inhalt zum Browser senden.
client() .apply(access().get()) .ifPresentOrElse( minioClient -> imageStream() .apply(minioClient, new BlobCoordinates(DEFAULT_BUCKET_NAME, imageID)) .ifFailed(failed -> logger().warning(failed)) .map(bytes -> new StreamResource( (StreamSource) () -> bytes, imageID + "." + nanoTime() )) .ifPresentOrElse( ok -> image.getUI() .access(() -> image.setSource(ok)), failed -> { logger().warning(failed); image.getUI() .access(() -> image.setSource( asStreamSource() .apply(failedImageAsInputStream().apply(imageID), imageID ))); } ), failed -> logger().warning(failed))
Zu beachten ist hier der jeweilige Teil in dem ein Runnable dem UI-Thread übergeben wird.
image.getUI().access(()->{..}
Alle UI-unabhängigen Teile können ausserhalb des UI-Threads erledigt werden. Der Teil, der dann den Inhalt des Bildes setzt, muss nun allerdings in dem UI-Thread erfolgen. Vaadin erkennt das Setzen der neuen Inhalte und sorgt dann dafür, dass die Änderungen im Browser sichtbar werden.
Fazit
Wir haben nun alles zusammen, was wir für eine einfache CRUD-Anwendung auf Basis eines S3-Storage benötigen. Die Möglichkeiten sind vielfältig und eigenen Experimenten steht nichts mehr im Wege.
Bei Fragen oder Problemen können Sie gerne Kontakt zu mir aufnehmen. Happy Coding!