Service Event Bus
- Registering Listeners
- Request Events
- Session Lock Events
- RPC Invocation Events
- Message Synchronization Events
- Data Provider Query Events
- Navigation Events
- Browser Compatibility Events
- Other Framework Events
- Firing Your Own Events
- Deprecated Listener Interfaces
Every VaadinService has an event bus through which the framework reports what it’s doing, and through which an application can fire service-wide events of its own.
The events the framework fires cover the machinery that a request passes through:
-
the request itself, from start to finish,
-
the session lock that serializes all server-side work for a session,
-
the individual client-to-server RPC invocations handled while that lock is held,
-
the client-server message synchronization those invocations depend on,
-
the data provider queries the invocations trigger,
-
the server-side navigations that a router link click, a
UI.navigate()call, or a page load can start, and -
the redirect to the page shown when a browser is too old to run the application.
Listening to them is useful for performance monitoring, distributed tracing, diagnosing lock contention, and reporting slow or failing requests — without modifying application logic.
The bus is reached with VaadinService.getEventBus(), and a listener is added for one event type:
Source code
Java
Registration registration = service.getEventBus()
.addListener(SessionLockAcquiredEvent.class,
event -> handleLockAcquired(event));The returned Registration removes the listener again with remove().
A few properties of the bus are worth knowing:
-
Events are dispatched by their exact runtime type. A listener registered for a supertype — for instance
AbstractDataFetchEvent— is never notified; register for each concrete event type you want. -
The bus is thread-safe. Listeners can be added and removed while events are being fired from other request threads. Requests belonging to different sessions are handled concurrently, so a listener must expect events from several sessions on several threads at once.
-
An exception thrown by a listener is logged, and the remaining listeners are notified regardless, so that one misbehaving listener can neither disrupt the framework nor hide the event from other listeners.
-
Listeners run on the request or access thread, directly around the operation they report. Implementations must be fast and non-blocking.
Registering Listeners
Listeners are typically added from a VaadinServiceInitListener, which receives the service before it starts handling requests.
How that init listener itself is discovered depends on the project type:
-
In Spring and CDI projects, annotate the class with
@Component(Spring) or make it a managed bean (CDI); it’s then registered automatically. -
In plain Java projects, register it through the Java Service Provider Interface by listing its fully qualified class name in
META-INF/services/com.vaadin.flow.server.VaadinServiceInitListener.
See Service Init Listener for the full details of each approach.
Request Events
The framework reports the outermost span of a request: from just after the request interceptors' requestStart callback runs to just before their requestEnd callback runs.
| Event | When It’s Fired |
|---|---|
| After the request interceptors' |
| Once the response has been written, before the request interceptors' |
Every started event is followed by a matching ended event for the same request, on the same thread. Requests are handled concurrently, so a listener must expect several requests to be in flight on several threads at once.
Both events carry the VaadinService handling the request and the request itself, through getService() and getRequest().
getResponse() gives an Optional with the response.
It’s empty for a push message, which is reported as a request of its own, without a response.
A Hilla endpoint call is also reported without a response, session, or handler in RequestEndedEvent, although its RequestStartedEvent has a response.
So a missing response doesn’t by itself mean a push message.
RequestEndedEvent reports more, including how long handling the request took:
| Method | Description |
|---|---|
| An |
| An |
| An |
| An |
| How long handling the request took.
It’s measured from when the framework starts handling the request, before the request interceptors' |
The following example logs slow or failed requests, along with the handler that handled them:
Source code
RequestMetricsInitListener.java
Session Lock Events
Because all of a session’s server-side work is serialized behind a single lock, the time a thread spends blocked acquiring that lock (the wait time) and the time it then holds it (the hold time) are key performance signals. Three events expose both:
| Event | When It’s Fired |
|---|---|
| Immediately before the thread attempts to acquire the lock. |
| Immediately after the lock has been acquired. |
| Immediately after the outermost hold has been released (the hold count reached zero). |
All three carry the VaadinService the lock belongs to, through getService(), and the VaadinSession it protects, through getSession().
The session is an empty Optional when it isn ’t known.
This happens when the lock is taken for a new HTTP session before the Vaadin session has been created in it.
It can also happen after the Vaadin session has been removed from the HTTP session, since the lock keeps the session only through a weak reference.
SessionLockAcquiredEvent.getWaitTime() gives how long the thread waited for the lock, from the moment it was requested until it was acquired, including time other threads spent holding it.
SessionLockReleasedEvent.getHoldTime() gives how long the thread then held the lock, from acquisition until release.
Flow measures both itself, so no listener has to time anything on its own; it only measures them while a listener is registered to receive the value, so an application that listens to neither pays nothing for the timing.
The same lock instance protects a session whether it’s acquired by the framework while handling a request, or through VaadinSession.lock() — for example from UI.access().
Events are fired for the outermost acquisition only; reentrant re-locks aren’t reported.
For a single lock-hold, all three are fired on the same thread, in the order requested → acquired → released.
The following example logs the wait time and the hold time of each lock-hold:
Source code
SessionLockMetricsInitListener.java
When several listeners are registered, the requested and acquired events are delivered in registration order, while the released event is delivered in reverse registration order.
Listeners therefore nest like try/finally blocks: a listener registered later sees the release before one registered earlier does.
Listeners registered after a session’s lock has already been created are still honored, but only a listener already registered for SessionLockReleasedEvent at the moment the lock was acquired is notified of its release.
A listener added while a thread is already holding the lock therefore misses the release event for that hold; it’s notified of the next one.
RPC Invocation Events
A single client request typically carries several RPC invocations — a DOM event, a synchronized property update, a @ClientCallable or template event handler, a server-side navigation, a return channel message, and so on.
One event of each type is fired per invocation, which is useful for emitting a tracing span that shows exactly which invocation consumes the time spent holding the session lock during a request.
| Event | When It’s Fired |
|---|---|
| Immediately before an invocation is handled. |
| When handling an invocation threw, before the ended event.
The throwable is available from |
| Once an invocation has been handled, whether it completed normally or threw.
|
All three expose the same details about the invocation:
| Method | Description |
|---|---|
| The |
| The protocol-level invocation type, such as |
| The id of the targeted |
| A human-readable identifier — the DOM event name, the invoked method name, the navigation location, and so on — or |
| An |
For one invocation, the started event, the optional failed event, and the ended event are fired on the same thread, in that order.
The ended event is always fired after the started one, regardless of outcome, so a listener may keep state in a ThreadLocal.
The ended event carries what such state is most often kept for — the duration and the error of the invocation — so a listener that only needs those can listen for the ended event alone.
Within one request the events don’t nest: those of one invocation are all fired before those of the next.
Being reported doesn’t mean the invocation had an effect: an RPC targeting a node that’s detached, disabled, or inert is reported and only then discarded unhandled.
Message Synchronization Events
Every client-to-server message carries an incrementing id, which the server uses to detect a resent message or one it didn’t expect.
The client can also ask the server to resynchronize a UI whose client-side state has fallen behind.
See Sync Server & Client UI States for how the ids and resynchronization work.
Three events report these cases, on the request thread while the session lock is held:
| Event | When It’s Fired |
|---|---|
| When the client resends the message it didn’t get a response to, before the server responds with the response it sent the first time instead of processing the message again. |
| When the client sends a message with an id the server didn’t expect, before the message is discarded and the client is shown the session synchronization error.
This typically happens when the server that handled the |
| When the client asks the server to resynchronize a |
All three expose the UI involved through getUI().
MessageIdSyncErrorEvent additionally exposes getExpectedId() and getReceivedId(), the message id the server expected and the one the client actually sent.
Data Provider Query Events
Every fetch and count query a data-bound component issues is reported on the bus, so that slow backend queries can be measured where they happen instead of being inferred from overall request timings.
| Event | When It’s Fired |
|---|---|
| Before a page of items is requested from a data provider. |
| When the fetch threw, before the ended event.
The throwable is available from |
| Once the items have been loaded and consumed, or the fetch threw.
|
| Before a count query is issued. |
| When the count query threw, before the ended event. |
| Once the count query has returned or thrown.
|
The events describe the query and where it came from:
| Method | Description |
|---|---|
| The |
| An |
| The index of the first item requested, and how many were requested. Fetch events only. |
| Whether the query carried a filter, which distinguishes, for example, a Combo Box search from its initial page load. |
Because a data provider may return a lazily evaluated Stream, the ended event is fired only after the returned items have been consumed, so the measured duration covers the backend round-trip rather than only the call that started it.
The started and ended events of a query are fired on the same thread, and the ended event is fired in reverse registration order, so listeners nest around the started one.
Queries made for a component that isn’t attached to a live UI aren’t reported.
|
Note
|
Data fetches triggered by push updates run on the executor given to DataCommunicator.enablePushUpdates(), so these events aren’t always fired on a request thread.
|
Navigation Events
Each server-side navigation the router handles fires a matching pair of events, useful for timing navigations and tagging them with their outcome — something that pairing BeforeEnterEvent and AfterNavigationEvent listeners gets wrong for forwards, reroutes, redirects, postponed navigations, and error views.
| Event | When It’s Fired |
|---|---|
| Before the router starts handling a navigation, before any |
| Once the navigation has finished, whatever the outcome. |
Only the outermost navigation is reported.
A reroute, a forward, the redirect that adds or removes a trailing slash, and the rendering of an error view are part of the navigation that caused them and fire no events of their own; their effect shows up in the ended event’s outcome instead.
Resuming a postponed navigation with ContinueNavigationAction.proceed() is reported as a navigation of its own, with an outcome of Completed, Failed, NotShown, or Postponed again.
Showing a PreserveOnRefresh view once the browser has sent its window name fires no events, since it continues a navigation that has already been reported.
Both events carry the details of the navigation that was requested:
| Method | Description |
|---|---|
| The |
| The requested location, before any forward or reroute.
Never |
| The action that triggered the navigation, such as a page load, a router link click, or a call to |
NavigationEndedEvent additionally exposes how the navigation ended:
| Method | Description |
|---|---|
| One of the records of the sealed
|
| The HTTP status code of the navigation, such as 200 or 404, or |
For one navigation, the started event and the ended event are fired on the same thread, in that order, even when the navigation throws, so timing state can again be kept in a ThreadLocal.
The ended event is fired in reverse registration order, so listeners nest around the started one.
Example: Finding broken links in the application.
A navigation that ends with status 404 after the user opened a URL directly usually means a mistyped address or an outdated bookmark. When the user followed a router link instead, the application itself links to a route that doesn’t exist:
Source code
Java
service.getEventBus().addListener(NavigationEndedEvent.class, event -> {
if (event.getStatusCode() == 404
&& event.getTrigger() == NavigationTrigger.ROUTER_LINK) {
LoggerFactory.getLogger(getClass()).warn(
"Router link points to a missing route: {}",
event.getLocation().getPath());
}
});Browser Compatibility Events
UnsupportedBrowserEvent is fired when a browser is redirected to the page telling it that it’s too old to run the application, just before that page is written to the response.
It carries the VaadinService, through getService(), the VaadinSession of the browser, through getSession(), and the VaadinRequest for the page, through getRequest() — the request’s User-Agent header identifies the browser.
Source code
Java
service.getEventBus().addListener(UnsupportedBrowserEvent.class,
event -> log(event.getSession(),
event.getRequest().getHeader("User-Agent")));A browser too old to run the inline script that detects the need for the redirect never makes the redirected request, so no event is fired for it.
Other Framework Events
The service lifecycle events are fired through the same bus, and the dedicated addSessionInitListener(), addSessionDestroyListener(), addServiceDestroyListener(), and addUIInitListener() methods on VaadinService are thin wrappers that register on it.
Either style works: use the listener interface when it exists, or listen for SessionInitEvent, SessionDestroyEvent, ServiceDestroyEvent, and UIInitEvent directly on the bus.
Firing Your Own Events
Any component of an application that needs to notify service-wide listeners can define an event type of its own and fire it on the bus, instead of maintaining its own listener collection.
An event type only has to extend EventObject:
Source code
Java
public class MaintenanceModeEvent extends EventObject {
public MaintenanceModeEvent(VaadinService service) {
super(service);
}
}
// Elsewhere, to notify the listeners:
service.getEventBus().fireEvent(new MaintenanceModeEvent(service));Three firing methods are available:
| Method | Description |
|---|---|
| Notifies the listeners of the event type in registration order, logging an exception from a listener and continuing with the rest. |
| The same, but hands a listener that threw to the given error handler instead of logging it. |
| Notifies the listeners in reverse registration order. Use it for the closing half of a pair of events, so that listeners nest around the opening one. |
On a hot code path, hasListener(MaintenanceModeEvent.class) tells whether building the event is worth it at all.
Deprecated Listener Interfaces
SessionLockListener and RpcInvocationListener, together with the VaadinService.addSessionLockListener() and VaadinService.addRpcInvocationListener() methods that register them, are deprecated for removal.
They still work and are still delivered from the bus, but new code should listen for the events directly:
| Deprecated Callback | Event |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
The SessionLockEvent and RpcInvocationEvent classes the callbacks receive are deprecated with them; the new events carry the same information.
970a4b45-bc87-4381-9ab9-7c3e34e97b26