Dans une taverne bien tenue, il ne suffit pas d’avoir une belle salle et une carte lisible. Il faut aussi contrôler l’accès au comptoir, prévenir le personnel quand la salle s’agite, et savoir accrocher au mur une carte ou un graphique que l’on n’a pas acheté tout fait.
Vaadin permet déjà de construire des interfaces riches en Java, et les articles autour de Spring Boot + Vaadin ont couvert les bases : créer une vue, ajouter des composants, manipuler une Grid, naviguer entre les écrans et personnaliser l’apparence.
Refaire le même tour de salle avec Quarkus n’aurait pas grand intérêt.
Nous allons donc entrer directement par les trois portes les plus intéressantes de The Falling Whale :
- sécuriser l’accès avec Quarkus Security et un formulaire Vaadin ;
- pousser des mises à jour temps réel vers l’interface ;
- construire un pont entre Java et JavaScript pour utiliser Chart.js et Leaflet.
Autrement dit, nous n’allons pas refaire la visite complète de la taverne. Nous allons regarder ce qui rend le comptoir vivant, protégé et extensible.
Poser la porte de la taverne
Avant de parler sécurité ou temps réel, il faut que Vaadin soit réellement servi par Quarkus.
Dans The Falling Whale, la porte d’entrée est volontairement discrète : une servlet dédiée, branchée à la racine de l’application.
@WebServlet(urlPatterns = "/*", name = "AdminServlet", asyncSupported = true, loadOnStartup = 1)
public class AdminServlet extends QuarkusVaadinServlet {
}
notre porte d'entrée
Cette classe ne contient presque rien, mais elle joue un rôle essentiel. Elle indique à Quarkus que l’application Vaadin doit répondre sur /*, c’est-à-dire à la racine de l’application.
Contrairement à une application Spring Boot où l’autoconfiguration masque souvent ce branchement, ici la servlet Vaadin est déclarée explicitement. Le comptoir n’est pas installé dans une arrière-salle obscure : il est immédiatement accessible lorsque l’on ouvre l’application.
Ce branchement est vérifié avec un test simple :
@QuarkusTest
class AdminServletTest {
@Test
void shouldServeVaadinBootstrapPageOnRootPath() {
given()
.when().get("/")
.then()
.statusCode(200);
}
}
Test minimaliste mais suffisant
Ce test ne cherche pas à valider toutes les interactions Vaadin.
Il vérifie simplement que la salle s’ouvre, que Quarkus démarre, et que Vaadin sert bien sa page de bootstrap.
C’est peu spectaculaire, mais c’est exactement le genre de garde-fou qui évite de chercher plus tard pourquoi aucune vue ne répond.
Le pom.xml contient ensuite les dépendances qui donnent à l’application son identité :
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-core</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-quarkus-extension</artifactId>
<version>${vaadin.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-properties-file</artifactId>
</dependency>
les dépendances de notre application
On retrouve Vaadin pour l’interface, l’extension Vaadin pour l’intégration Quarkus, et Quarkus Security pour gérer l’accès.
La taverne commence donc avec deux piliers : une UI côté serveur, et une sécurité portée par le framework applicatif.
Le formulaire à l’entrée
Dans The Falling Whale, l’entrée ne se fait pas par une simple fenêtre Basic Auth du navigateur.
Le choix retenu est une authentification par formulaire, plus naturelle pour une application Vaadin, parce qu’elle permet de garder une page de connexion cohérente avec le reste de l’interface.

La vue de login est très courte :
@Route("login")
@PageTitle("Login | The Falling Whale")
public class LoginView extends VerticalLayout {
private final LoginForm login = new LoginForm();
@Inject
public LoginView() {
addClassName("login-view");
setSizeFull();
setAlignItems(Alignment.CENTER);
setJustifyContentMode(JustifyContentMode.CENTER);
login.setAction("login");
add(new H1("The Falling Whale"), login);
getElement().getStyle().set("background", "var(--whale-parchment)");
getElement().getStyle().set("background-image", "var(--parchment-texture)");
}
}
la classe java générant cette vue
Le composant important est LoginForm. Vaadin fournit le formulaire, mais il ne décide pas seul de l’authentification. La ligne login.setAction("login") envoie les identifiants vers l’URL attendue par Quarkus.
La configuration Quarkus fait le lien entre ce formulaire et le mécanisme d’authentification :
quarkus.http.auth.form.enabled=true
quarkus.http.auth.form.login-page=/login
quarkus.http.auth.form.post-location=/login
quarkus.http.auth.form.error-page=/login?error
quarkus.http.auth.form.landing-page=/
quarkus.http.auth.form.username-parameter=username
quarkus.http.auth.form.password-parameter=password
Propriété de connexion
On retrouve ici une idée importante : Vaadin ne remplace pas Quarkus Security. Il lui fournit une porte d’entrée agréable côté UI, tandis que Quarkus reste responsable de la mécanique d’authentification.
L’application utilise un fichier de propriétés pour les utilisateurs :
quarkus.security.users.file.enabled=true
quarkus.security.users.file.plain-text=true
quarkus.security.users.file.users=users.properties
quarkus.security.users.file.roles=roles.properties
Les identifiants de démonstration sont simples :
tavernier=secret
servante=biere
Et les rôles associés séparent clairement les responsabilités :
tavernier=ADMIN,STAFF
servante=STAFF
Le tavernier a accès à toute la maison. La servante peut tenir la salle, mais ne peut pas consulter les analytiques.
Ce même registre d’utilisateurs pourrait servir à d’autres mécanismes Quarkus, comme Basic Auth, dans un scénario plus API ou plus technique. Mais ici, The Falling Whale met en scène l’authentification par formulaire, et c’est elle que nous gardons comme exemple concret.
Les clés derrière le comptoir
Une fois la porte franchie, il faut encore empêcher les clients anonymes de se glisser dans les salles protégées.
Le layout principal joue ce rôle de contrôle à l’entrée.
public class MainLayout extends AppLayout implements BeforeEnterObserver {
private final SecurityService securityService;
@Inject
public MainLayout(TavernService tavernService, SecurityService securityService) {
this.securityService = securityService;
addClassName("tavern-app-layout");
addToNavbar(createHeader());
addToDrawer(createDrawer(tavernService));
setPrimarySection(Section.DRAWER);
}
@Override
public void beforeEnter(BeforeEnterEvent event) {
if (securityService.getAuthenticatedUser().isAnonymous()) {
event.rerouteTo(LoginView.class);
}
}
}
Notre layout principal
BeforeEnterObserver intervient avant l’entrée dans la vue. Si l’utilisateur est anonyme, la navigation est détournée vers LoginView. C’est une manière très lisible de dire que la salle principale n’est pas ouverte aux inconnus.
Le service de sécurité injecte l’identité Quarkus :
@ApplicationScoped
public class SecurityService {
@Inject
SecurityIdentity securityIdentity;
public SecurityIdentity getAuthenticatedUser() {
return securityIdentity;
}
}
À ce stade, Vaadin manipule la navigation et les composants, mais la vérité sur l’utilisateur vient bien de Quarkus.
Les vues ajoutent ensuite leurs propres règles avec @RolesAllowed :
@RolesAllowed("STAFF")
@PageTitle("Salle commune")
@Route(value = "", layout = MainLayout.class)
public class DashboardView extends VerticalLayout {
Et pour la partie réservée au tavernier :
@RolesAllowed("ADMIN")
@PageTitle("Analytiques de la Taverne")
@Route(value = "analytics", layout = MainLayout.class)
public class AnalyticsView extends VerticalLayout {
La configuration HTTP protège aussi la route des analytiques :
quarkus.http.auth.policy.admin-policy.roles-allowed=ADMIN
quarkus.http.auth.permission.admin.paths=/analytics/*
quarkus.http.auth.permission.admin.policy=admin-policy
Il y a donc deux niveaux de verrou : la navigation Vaadin côté vue, et la politique Quarkus côté HTTP.
Dans une vraie taverne, cela revient à avoir à la fois un portier à l’entrée et une serrure sur la réserve.
La déconnexion mérite aussi un détour. Avec Vaadin, l’interface possède une session côté serveur.
Quitter proprement l’application demande donc de fermer la session HTTP et la session Vaadin.
public void logout() {
FormAuthenticationMechanism.logout(securityIdentity);
var request = VaadinServletRequest.getCurrent()
.getHttpServletRequest();
request.getSession().invalidate();
VaadinSession.getCurrent().close();
UI.getCurrent().getPage().setLocation("/login");
}
Politique de logout
Ce passage est typique d’une application Vaadin sécurisée avec Quarkus : il ne suffit pas d’effacer un bouton ou de changer de vue. Il faut réellement refermer les sessions qui maintiennent l’état de l’utilisateur.
Sans invalidation explicite de la session HTTP et fermeture de la VaadinSession, il est possible de conserver un état utilisateur actif malgré une redirection visuelle vers la page de login. Le code paraît donc un peu plus lourd qu’un simple setLocation("/login"), mais il ferme réellement la porte derrière l’utilisateur.
Une salle qui se met à jour toute seule
Dans une taverne vivante, les réservations arrivent pendant que le service continue.
Les stocks descendent pendant que la salle discute. Personne ne veut recharger la page toutes les dix secondes pour savoir si une futaille est presque vide.
The Falling Whale active Vaadin Push au niveau de l’application :
@Push
@PWA(name = "The Falling Whale", shortName = "Whale")
@Theme("falling-whale")
public class TavernAppShell implements AppShellConfigurator {
}
Activation de push
@Push permet au serveur d’envoyer des changements vers le navigateur.
C’est la base nécessaire pour que l’interface se mette à jour lorsque le changement ne vient pas directement d’un clic utilisateur sur la page courante.
L’application introduit ensuite un petit diffuseur d’événements :
@ApplicationScoped
public class TavernBroadcaster {
private final Executor executor = Executors.newSingleThreadExecutor();
private final LinkedList<Consumer<ReservationAddedEvent>> reservationListeners = new LinkedList<>();
private final LinkedList<Consumer<StockUpdatedEvent>> stockListeners = new LinkedList<>();
public synchronized void registerReservationListener(Consumer<ReservationAddedEvent> listener) {
LOG.debug("Nouveau listener enregistré pour les réservations.");
reservationListeners.add(listener);
}
public synchronized void unregisterReservationListener(Consumer<ReservationAddedEvent> listener) {
LOG.debug("Listener de réservations retiré.");
reservationListeners.remove(listener);
}
public synchronized void broadcast(ReservationAddedEvent event) {
LOG.info("Diffusion d'une nouvelle réservation : {}", event.entry().guestName());
for (Consumer<ReservationAddedEvent> listener : reservationListeners) {
executor.execute(() -> listener.accept(event));
}
}
}
Notre diffuseur d'event
Le principe est volontairement simple : les vues s’abonnent, le service diffuse, et chaque listener reçoit l’événement. Pour un tutoriel, cette simplicité est précieuse. On voit le mécanisme sans avoir besoin d’introduire tout de suite un broker de messages ou une architecture plus lourde.
Quand une réservation est ajoutée, le service modifie les données puis prévient les abonnés :
@ApplicationScoped
public class TavernService {
private final InMemoryTavernRepository repository;
private final TavernBroadcaster broadcaster;
@Inject
public TavernService(InMemoryTavernRepository repository, TavernBroadcaster broadcaster) {
this.repository = repository;
this.broadcaster = broadcaster;
}
public void addReservation(ReservationEntry entry) {
repository.addReservation(entry);
broadcaster.broadcast(new ReservationAddedEvent(entry));
}
}
La vue des réservations s’abonne lorsqu’elle est attachée à l’UI :
@Override
protected void onAttach(AttachEvent attachEvent) {
listener = event -> attachEvent.getUI().access(() -> {
grid.setItems(tavernService.getReservationBoard().reservations());
Notification notification = Notification.show("Mise à jour : Nouvelle réservation de " + event.entry().guestName());
notification.addThemeVariants(NotificationVariant.LUMO_SUCCESS);
});
broadcaster.registerReservationListener(listener);
}
Le détail crucial est attachEvent.getUI().access(...). Une mise à jour déclenchée depuis un autre thread ne peut pas modifier directement les composants Vaadin. Il faut repasser par l’UI, comme un serveur qui ne laisse entrer personne dans la salle sans passer par la bonne porte.
Et quand la vue disparaît, elle se désabonne :
@Override
protected void onDetach(DetachEvent detachEvent) {
if (listener != null) {
broadcaster.unregisterReservationListener(listener);
listener = null;
}
}
Sans cette étape, les anciennes vues continueraient d’écouter dans le vide.
Dans une application longue durée, ce genre d’oubli finit par laisser traîner des références inutiles, comme des commandes abandonnées au bout du comptoir.
La cave qui respire en temps réel
Les réservations ne sont pas les seuls événements vivants de la taverne. La cave aussi évolue.
Un service simule la consommation des stocks après le démarrage de Quarkus :
@ApplicationScoped
public class StockSimulationService {
void onStart(@Observes StartupEvent ev) {
LOG.info("Démarrage de la simulation des stocks de la cave...");
scheduler.scheduleAtFixedRate(this::simulateStockChanges, 5, 10, TimeUnit.SECONDS);
}
private void simulateStockChanges() {
LOG.debug("Simulation d'un cycle de consommation...");
for (CellarStock stock : repository.getCellarStocks()) {
if (random.nextDouble() < 0.7) {
int consumption = random.nextInt(3) + 1;
repository.updateStock(stock.productName(), -consumption);
LOG.info("Consommation : {} (-{} {})", stock.productName(), consumption, stock.unit());
}
}
broadcaster.broadcast(new StockUpdatedEvent(repository.getCellarStocks()));
}
}
Le service observe StartupEvent, démarre un scheduler, modifie les stocks, puis diffuse un StockUpdatedEvent. La cave devient un petit monde autonome : les niveaux changent, et les vues qui les affichent peuvent réagir.
2026-05-12 09:16:12,211 INFO []() Consommation : Cidre du nord (-1 service(s))
2026-05-12 09:16:22,210 INFO []() Consommation : Blonde des remparts (-1 pintes)
2026-05-12 09:16:22,211 INFO []() Consommation : Cidre du nord (-1 service(s))
2026-05-12 09:16:32,209 INFO []() Consommation : Blonde des remparts (-2 pintes)
2026-05-12 09:16:32,211 INFO []() Consommation : Hydromel réservé (-1 cruchons)
2026-05-12 09:16:32,212 INFO []() Consommation : Vin d'épices (-2 bouteilles)
2026-05-12 09:16:32,212 INFO []() Consommation : Cidre du nord (-1 service(s))
2026-05-12 09:16:42,209 INFO []() Consommation : Blonde des remparts (-3 pintes)
2026-05-12 09:16:42,210 INFO []() Consommation : Vin d'épices (-2 bouteilles)Extrait de logs montrant la consommation de la taverne
Le tableau de bord écoute ces événements :
@Override
protected void onAttach(AttachEvent attachEvent) {
listener = event -> attachEvent.getUI().access(() -> {
updateMetrics(tavernService.getDashboard());
updateDialogContent(event.stocks());
});
broadcaster.registerStockListener(listener);
}
La page des analytiques fait la même chose pour rafraîchir son graphique :
@Override
protected void onAttach(AttachEvent attachEvent) {
listener = event -> attachEvent.getUI().access(() -> {
updateLiveStockChart(event.stocks());
});
broadcaster.registerStockListener(listener);
}


Les stocks se mettant à jour en temps réel
Ce qui est intéressant ici, ce n’est pas seulement que l’écran se met à jour. C’est la combinaison de Quarkus et Vaadin : Quarkus déclenche une tâche de fond au démarrage, le service métier modifie les données, le broadcaster diffuse, et Vaadin Push transporte le changement jusqu’au navigateur.
La taverne n’est plus une page figée. Elle respire.
Quand Java tend la main à JavaScript
Vaadin permet d’écrire beaucoup d’interface en Java, mais cela ne veut pas dire qu’il faut ignorer tout l’écosystème JavaScript. Dans The Falling Whale, deux besoins sont confiés à des bibliothèques spécialisées :
- Chart.js pour les graphiques ;
- Leaflet pour la carte.
Le besoin initial était très concret : afficher des graphiques et une carte sans dépendre d’un composant commercial. Vaadin propose des composants avancés, certains disponibles via une licence payante, mais cela ne veut pas dire que l’on se retrouve bloqué dès que le besoin dépasse le catalogue gratuit.
La force de l’approche, ici, est ailleurs : on peut créer son propre composant, l’habiller avec une bibliothèque JavaScript libre, puis l’exposer au reste de l’application comme un composant Vaadin classique. Le tavernier ne trouve pas l’instrument exact dans l’armoire officielle ? Très bien. Il en fabrique un avec les matériaux disponibles, et il l’accroche au même mur que les autres.
L’idée n’est donc pas de quitter Vaadin, mais de créer un pont propre entre le monde Java et un composant web. On garde le confort de l’UI côté Java, tout en gardant une porte ouverte vers l’écosystème JavaScript lorsque c’est plus économique, plus souple, ou simplement plus adapté.
Le composant Java du graphique est déclaré ainsi :
@Tag("tavern-chart")
@JsModule("./tavern-chart.ts")
public class CustomChart extends Component implements HasSize {
public void setData(AnalyticsData data) {
JsonNodeFactory factory = JsonNodeFactory.instance;
ObjectNode json = factory.objectNode();
json.put("title", data.title());
json.put("chartType", data.chartType());
ArrayNode labels = factory.arrayNode();
data.labels().forEach(labels::add);
json.set("labels", labels);
ArrayNode values = factory.arrayNode();
data.values().forEach(values::add);
json.set("values", values);
getElement().setPropertyJson("data", json);
}
}
@Tag("tavern-chart") indique le nom du composant web. @JsModule("./tavern-chart.ts") indique le fichier TypeScript à charger. Et setPropertyJson("data", json) pousse les données Java vers le composant côté navigateur.
Le composant Java devient une façade typée et intégrée à l’écosystème Vaadin, tandis que le rendu reste entièrement libre côté navigateur.
Côté TypeScript, le composant reçoit cette propriété :
@customElement('tavern-chart')
export class TavernChart extends LitElement {
@property({ type: Object })
data: any = null;
render() {
return html`<canvas></canvas>`;
}
updated(changedProperties: Map<string, any>) {
if (changedProperties.has('data') && this.data) {
this.updateChart();
}
}
}
Puis il charge Chart.js si nécessaire :
private initChartLib() {
if (typeof (window as any).Chart === 'undefined') {
const script = document.createElement('script');
script.src = "https://cdn.jsdelivr.net/npm/chart.js";
script.onload = () => this.updateChart();
document.head.appendChild(script);
} else {
this.updateChart();
}
}
La vue Java, elle, n’a pas besoin de connaître les détails de Chart.js. Elle manipule simplement un composant Vaadin :
private void updateLiveStockChart(List<CellarStock> stocks) {
AnalyticsData data = new AnalyticsData(
"Quantités disponibles",
stocks.stream().map(CellarStock::productName).toList(),
stocks.stream().map(s -> (double) s.currentLevel()).toList(),
"bar"
);
liveStockChart.setData(data);
}
C’est exactement le bon niveau d’abstraction : la vue métier parle en CellarStock et AnalyticsData, le composant Java fait la conversion, le composant web se charge du rendu.
La carte des alentours
Le même modèle sert à intégrer Leaflet.
Côté Java :
@Tag("tavern-map")
@JsModule("./tavern-map.ts")
public class CustomMap extends Component implements HasSize {
public void setLocations(List<MapLocation> locations) {
JsonNodeFactory factory = JsonNodeFactory.instance;
ArrayNode array = factory.arrayNode();
for (MapLocation loc : locations) {
ObjectNode node = factory.objectNode();
node.put("name", loc.name());
node.put("latitude", loc.latitude());
node.put("longitude", loc.longitude());
node.put("description", loc.description());
node.put("type", loc.type());
array.add(node);
}
getElement().setPropertyJson("locations", array);
}
}
Côté TypeScript, le composant désactive le Shadow DOM pour faciliter l’intégration Leaflet :
@customElement('tavern-map')
export class TavernMap extends LitElement {
@property({ type: Array })
locations: any[] = [];
createRenderRoot() {
return this;
}
}
Puis il charge Leaflet et crée la carte :
private initMap() {
const loadLeaflet = () => {
if (typeof (window as any).L === 'undefined') {
if (!document.getElementById('leaflet-js')) {
const script = document.createElement('script');
script.id = 'leaflet-js';
script.src = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js";
script.onload = () => this.createMap();
document.head.appendChild(script);
}
} else {
this.createMap();
}
};
loadLeaflet();
}
Et les marqueurs sont créés à partir des données envoyées par Java :
this.locations.forEach((loc: any) => {
L.marker([loc.latitude, loc.longitude])
.addTo(this.map)
.bindPopup(`<b>${loc.name}</b><br>${loc.description}`);
});
Ce pont Java-JavaScript est peut-être la partie la plus intéressante de l’application. Il rappelle que Vaadin n’est pas une cage dorée.
On peut rester dans une architecture Java côté application, tout en appelant une bibliothèque JavaScript spécialisée lorsque le besoin le justifie.

C’est aussi le message pratique de cette intégration : avec Vaadin, on n’est jamais vraiment coincé. Si le composant existe dans l’offre gratuite, on l’utilise.
S’il existe seulement dans une offre payante et que le projet ne veut pas porter ce coût, on peut bâtir une alternative ciblée.
Et si une bibliothèque JavaScript fait déjà très bien le travail, le pont Java-JavaScript permet de l’intégrer sans transformer toute l’application en front séparé.
Conclusion
Cette intégration Vaadin + Quarkus n’a pas besoin de répéter les bases déjà vues avec Spring Boot. Ce qui mérite l’attention ici, c’est la répartition des responsabilités : Quarkus garde la main sur la sécurité, le cycle de vie applicatif et les tâches de fond ; Vaadin porte l’interface métier ; JavaScript intervient lorsque le navigateur dispose déjà du bon outil.
Cette approche permet de garder une application majoritairement Java, cohérente et centralisée, tout en laissant la porte ouverte aux outils spécialisés du web moderne lorsque cela devient pertinent.
Au final, Vaadin tient le comptoir, Quarkus garde les clés, et JavaScript vient accrocher au mur les instruments que la taverne ne veut pas acheter tout faits. L’ensemble reste confortable, extensible, et jamais enfermé.
Tout le code relatif à cet article et même plus est consultable juste ici :