Artikelreihe
Dies ist der zweite Teil einer kleinen Artikelserie darüber, wie man Hilla-Anwendungen für Apache Kafka erstellt.
- Part 1: Nachrichten konsumieren
- Part 2: Nachrichten produzieren
- Part 3: Kafka Streams
In diesem Blog-Post wird aufgezeigt, wie man Kafka-Nachrichten aus einer Benutzereingabe im Hilla-Frontend und mit Hilfe von Spring Kafka im Hilla-Backend erzeugt.
Kafka einrichten und Hilla-Projekt erstellen
Bitte lesen Sie den ersten Blog-Beitrag dieser Artikelserie, um herauszufinden, wie man einen lokalen Kafka-Broker einrichtet und wie man Nachrichten aus dem Topic reservations
in der entsprechenden Hilla-Anwendungen konsumiert.
KafkaTemplate im Hilla-Backend
Um eine neue Nachricht in dem Topic reservations
zu erzeugen, wird ein automatisch konfiguriertes KafkaTemplate verwendet. Dies erfordert ein wenig zusätzliche Konfiguration in der application.properties
im Hilla-Backend:
# Configure the producer
spring.kafka.producer.client-id=reservation-producer-client
spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer
spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer
In diesem Beispiel kommt der von Spring Kafka bereitgestellte JsonSerializer zum Einsatz, um ein Reservation
-Objekt in eine Nachricht für das Topic reservations
zu serialisieren.
Das automatisch konfigurierte KafkaTemplate
kann nun in einem KafkaProducerService
verwendet werden.
package de.rwi.hillakafkaexample.kafka;
import org.springframework.kafka.core.KafkaTemplate;
import de.rwi.hillakafkaexample.Reservation;
import com.vaadin.flow.server.auth.AnonymousAllowed;
import com.vaadin.hilla.BrowserCallable;
@BrowserCallable
@AnonymousAllowed
public class KafkaProducerService {
private final KafkaTemplate<String, Reservation> kafkaTemplate;
public KafkaProducerService(KafkaTemplate<String, Reservation> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void produce(Reservation reservation) {
kafkaTemplate.send("reservations", reservation);
}
}
Die Methode produce
erhält eine neue Reservierung vom Frontend und übergibt diese an das KafkaTemplate
.
Formular im Hilla-Frontend
Als Nächstes wird die bestehende ReservationsView
im Hilla-Frontend erweitert.
import { ActionOnLostSubscription } from '@vaadin/hilla-frontend';
import { useSignal } from '@vaadin/hilla-react-signals';
import {
Button,
DatePicker,
Dialog,
Grid,
GridColumn,
HorizontalLayout,
Icon,
Notification,
TextField,
VerticalLayout,
} from '@vaadin/react-components';
import { ViewToolbar } from 'Frontend/components/ViewToolbar';
import Reservation from 'Frontend/generated/de/rwi/hillakafkaexample/reservation/Reservation';
import { KafkaConsumerService, KafkaProducerService } from 'Frontend/generated/endpoints';
import { useEffect } from 'react';
import { useForm } from '@vaadin/hilla-react-form';
import ReservationModel from 'Frontend/generated/de/rwi/hillakafkaexample/reservation/ReservationModel';
export default function ReservationsView() {
const reservations = useSignal<Reservation[]>();
const subscriptionNotificationOpened = useSignal<boolean>(false);
const subscriptionNotificationMessage = useSignal<string | undefined>(undefined);
const newReservationDialogOpened = useSignal<boolean>(false);
const { model, field, clear, submit, invalid } = useForm(ReservationModel, {
onSubmit: async (reservation: Reservation) => {
await KafkaProducerService.produce(reservation);
closeNewReservationDialog();
},
});
useEffect(() => {
const reservationSubscription = KafkaConsumerService.getLatestReservation()
.onNext((reservation: Reservation) => {
reservations.value = [...(reservations.value ?? []), reservation];
})
.onError((message: string) => {
subscriptionNotificationOpened.value = true;
subscriptionNotificationMessage.value = message;
})
.onSubscriptionLost(() => ActionOnLostSubscription.RESUBSCRIBE);
return () => reservationSubscription.cancel();
}, []);
const closeSubscriptionNotification = () => {
subscriptionNotificationOpened.value = false;
subscriptionNotificationMessage.value = undefined;
};
const closeNewReservationDialog = () => {
newReservationDialogOpened.value = false;
clear();
};
const retrySubscription = () => {
window.location.reload();
};
return (
<VerticalLayout theme="margin">
<ViewToolbar title="Reservations" />
<Grid items={reservations.value}>
<GridColumn path={'id'} />
<GridColumn path={'date'} />
<GridColumn path={'customer'} />
</Grid>
<HorizontalLayout theme="margin" className="self-end">
<Button theme="primary" onClick={() => (newReservationDialogOpened.value = true)}>
New reservation
</Button>
</HorizontalLayout>
<Dialog
headerTitle={'New reservation'}
opened={newReservationDialogOpened.value}
onOpenedChanged={({ detail }) => {
newReservationDialogOpened.value = detail.value;
}}
footer={
<>
<Button onClick={closeNewReservationDialog}>Cancel</Button>
<Button theme="primary" disabled={invalid} onClick={submit}>
Add
</Button>
</>
}>
<VerticalLayout style={{ alignItems: 'stretch', width: '18rem', maxWidth: '100%' }}>
<TextField label="ID" {...field(model.id)} />
<DatePicker label="Date" {...field(model.date)} />
<TextField label="Customer" {...field(model.customer)} />
</VerticalLayout>
</Dialog>
<Notification
theme="warning"
duration={0}
position="middle"
opened={subscriptionNotificationOpened.value}
onOpenedChanged={(event) => {
subscriptionNotificationOpened.value = event.detail.value;
}}>
<HorizontalLayout theme="spacing" style={{ alignItems: 'center' }}>
<div>{subscriptionNotificationMessage.value ?? 'Failed to subscribe to reactive reservation endpoint'}</div>
<Button theme="tertiary-inline" style={{ marginLeft: 'var(--lumo-space-xl)' }} onClick={retrySubscription}>
Retry
</Button>
<Button theme="tertiary-inline icon" onClick={closeSubscriptionNotification} aria-label="Close">
<Icon icon="lumo:cross" />
</Button>
</HorizontalLayout>
</Notification>
</VerticalLayout>
);
}
Hauptbestandteil der Erweiterung ist eine Dialog-Komponente, die ein Formular zum Erstellen einer neuen Reservierung enthält. Das Formular wird mit Hilfe des useForm
-Hook von Hilla erstellt. Der Hook bietet alle notwendigen Properties und Funktionen für wesentliche Aufgaben wie Validierung, Absenden und Löschen des Formulars (siehe dazu auch den entsprechenden Guide in der Dokumentation). Der onSubmit
-Callback ruft die produce
-Methode des KafkaProducerService
auf und verwendet die validierte Reservierung, die in das Formular eingegeben wurde.
Manueller Test
Nun ist es an der Zeit, die Erweiterungen in Aktion zu sehen. Der Kafka-Broker muss ausgeführt werden (docker compose -f docker/docker-compose.yaml up
), die Hilla-Anwendung muss gestartet werden (zum Beispiel mit ./mvnw spring-boot:run
) und die URL http://localhost:8080 muss im Browser geöffnet werden. Anschließend kann über einen Klick auf die Schaltfläche New reservation
unterhalb der Grid-Komponente eine neue Reservierung im Dialog-Formular hinzugefügt werden.
Die eingegebene Reservierung sollte sofort in der Grid-Komponente erscheinen, sobald der Dialog geschlossen wurde.
Fazit
Dank Spring Kafka und den Autokonfigurationsfähigkeiten von Spring Boot ist es einfach, eine neue Nachricht zu einem Kafka-Topic hinzuzufügen. In Verbindung mit Hillas typsicheren Browser Callable Services und der erstklassigen Formularunterstützung sind die notwendigen Ergänzungen im Frontend ebenfalls leicht zu implementieren.
Der nächste Artikel dieser Serie wird zeigen, wie man Kafka-Nachrichten konsumiert und produziert, wenn man Kafka-Streams verwendet.
Den Quellcode der gezeigten Beispiel-Hilla-App findet man auf GitHub.
Bildnachweis:
- Cover-Bild Quelle: https://www.flickr.com/photos/mgaylard/54107413960/in/album-72177720321628498
- Cover-Bild Lizenz: https://creativecommons.org/licenses/by/2.0/deed.en