Gepuffertes Event-Dispatching mit SecurityContext für asynchrone UI-Updates mit Vaadin
Asynchrone UI-Updates mit Vaadin: Push-Events gepuffert, damit die Browser-Verbindung nicht überlastet, mit Zugriff auf den Spring SecurityContext.

Oft sollen Daten vom Server zum Client gepusht werden. Man denke an eine Chat-App, die eingehende Nachrichten im Browser anzeigt: Neue Nachrichten sollen automatisch erscheinen, ohne Polling und ohne dass der Benutzer etwas anklicken muss. Push-Updates in Vaadin sind grundsätzlich einfach umzusetzen, es gibt aber ein paar Sonderfälle.
In diesem Beitrag zeige ich einen Weg, asynchrone UI-Updates mit Vaadin umzusetzen, ohne den Client mit Daten zu fluten oder die Verbindung zwischen Server und Browser zu überlasten, wenn viele Push-Events gleichzeitig ankommen. Wer Spring Security mit Vaadin einsetzt, braucht außerdem Zugriff auf den Security-Kontext des Benutzers, um Push-Updates zu autorisieren, bevor sie an den Browser gehen.
Zuerst zur Umgebung eines meiner größeren Projekte, an dem ich mit Kollegen arbeite. Es ist ein gutes Beispiel für Server-Push in Produktion. Apache Kafka ist dort die zentrale Event-Streaming-Plattform, der gesamte Datenstand liegt in Kafka in Form von Domain-Events. Dazu kommen einige Self-contained Systems (SCS) mit Vaadin und Spring Boot. Konsumiert etwa ein Stream-Listener eines SCS ein Domain-Event, erfahren die Benutzer per Server-Push sofort von der Änderung.
Zum entscheidenden Punkt: Angenommen, jedes eingehende Event löst ein asynchrones UI-Update aus. Was passiert, wenn viele Events gleichzeitig konsumiert werden, sagen wir mehr als 1.000? Im besten Fall entsteht unnötig hohe Last auf Server und Browser, im schlimmsten Fall stürzt die Anwendung ab. In event-getriebenen Architekturen sind große Mengen an Nachrichten und Events der Normalfall.
Für Vaadin 14+ gibt es dafür meines Wissens keine fertige Lösung. Klassische Event-Busse wie der von Guava helfen hier nicht, weil sie weder Pufferung noch den Security-Kontext von Haus aus unterstützen.
Pufferung
Buffering: sammelt die von einem Observable ausgegebenen Elemente periodisch zu Bündeln und gibt diese Bündel aus, statt jedes Element einzeln auszugeben
Damit nicht jedes eingehende Domain-Event ein eigenes UI-Update auslöst, bietet sich Pufferung an. Dafür haben wir eine einfache Klasse namens UiAwareBufferingEventDispatcher eingeführt. Der Event-Dispatcher sammelt alle eingehenden Events innerhalb eines festgelegten Zeitfensters. Dann gibt er ein einziges Event mit der Liste aller gesammelten Events an die konsumierenden Vaadin-Komponenten weiter:
Die konsumierende Komponente entscheidet selbst, wie sie mit den gepufferten Events umgeht. Zeigt sie zum Beispiel nur einen Hinweis wie „Neue Daten verfügbar“ an, reicht meist das letzte Event der Liste. Ob keines, eines, mehrere oder alle Events für eine Komponente oder View relevant sind, hängt vom Anwendungsfall ab.
Für die Pufferung nutzen wir intern RxKotlin, eine Bibliothek für reaktive Programmierung:
class UiAwareBufferingEventDispatcher(/* omitted code */) {
// omitted code...
companion object {
private const val BUFFER_TIMESPAN_IN_MILLIS: Long = 500L
}
private val subject = PublishSubject.create<Any>()
private val scheduler = Schedulers.from(Executors.newSingleThreadExecutor())
private var subscriber: Disposable? = null
@PostConstruct
fun postConstruct() {
subscribe()
}
/** dispatch event (Note: runs in caller thread) */
fun dispatch(event: Any) {
subject.onNext(event)
}
/** start internal subscription to subject (events, which will be dispatched) */
private fun subscribe() {
if (subscriber == null || subscriber!!.isDisposed) {
subscriber = subject.observeOn(scheduler)
.buffer(BUFFER_TIMESPAN_IN_MILLIS, TimeUnit.MILLISECONDS)
.subscribe {
// dispatches the buffered events to Vaadin components
this.dispatchToHandlers(it)
}
}
}
// omitted code...
}Ein Demo-Projekt mit dem vollständigen Code liegt auf GitHub:
Zugriff auf Springs SecurityContext
Das folgende Beispiel zeigt eine konsumierende View bzw. Komponente. Kommt ein Event an, steht im asynchronen Handler der SecurityContext der Session zur Verfügung.
@Push
@Route("")
class MainView(
private val dispatcher: UiAwareBufferingEventDispatcher
) : VerticalLayout() {
// omitted code...
override fun onAttach(event: AttachEvent) {
dispatcher.register(this, MessagePostedEvent::class) { bufferedEvents ->
// following code doesn't run in component's thread,
// but the SecurityContext is available!
val username = SecurityUtils.user?.username ?: "unknown"
val lastEvent = bufferedEvents.last()
add(Span("ID: ${lastEvent.id}, Username: $username"))
}
}
override fun onDetach(event: DetachEvent) {
// do not forget to unregister the consumer!
dispatcher.unregister(this)
}
// omitted code...
}Der Event-Dispatcher greift auf die zugrunde liegende HTTP-Session der Komponente zu und setzt den threadgebundenen SecurityContext, bevor der Handler ausgeführt wird:
@Service
class UiAwareBufferingEventDispatcher(
@Qualifier("uiTaskExecutor") val taskExecutor: AsyncTaskExecutor
) {
// omitted code...
/**
* Sends a list of buffered events to registered handlers and synchronizes call to
* view state with session bound security context. Runs within TaskExecutor Thread.
* In case the view isn't bound to a UI or session this call ends
* without any exception. (handlers should only update UI and must not trigger any
* business logic)
*/
private fun dispatchWithinUIContext(
view: Component, handler: (List<*>) -> Unit,
events: List<*>
) {
val ui = view.ui.orElse(null) ?: return
val vaadinSession = ui.session ?: return
val httpSession = vaadinSession.session ?: return
val sessionSecurityContext = httpSession.getAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY
)
val securityContextToUse = if (sessionSecurityContext is SecurityContext) {
sessionSecurityContext
} else {
SecurityContextHolder.createEmptyContext()
}
ui.access {
val origCtx = SecurityContextHolder.getContext()
try {
SecurityContextHolder.setContext(securityContextToUse)
handler(events)
} catch (e: UIDetachedException) {
// ignore exceptions (just UI updates)
} catch (e: Exception) {
logger().error(
"unexpected exception while handling events {} bound to view {}",
events,
view,
e
)
} finally {
SecurityContextHolder.setContext(origCtx)
}
}
}
// omitted code...
}Der Code steht unter MIT-Lizenz und darf frei in eigenen Projekten verwendet werden.
Ein ähnliches Projekt im Kopf?
In einem unverbindlichen Erstgespräch schauen wir uns deine Ausgangslage an und sagen dir, was realistisch ist und wie der nächste Schritt aussieht.

// über den autor
Alexander J. Gassner, MSc
Gründer und Geschäftsführer von agsolutions, seit über 15 Jahren in der Software-Entwicklung (MSc Software Engineering, FH Hagenberg). Entwickelt und betreibt geschäftskritische Software von der Anforderung bis zum Betrieb: Kotlin, Spring Boot und React im Code, Kubernetes, Pulumi und GitOps im Betrieb, als Exoscale Certified Solution Architect.


