Calling Swing Methods from Vaadin
- Exposing a Method on the Swing Side
- The Four Invocation Shapes
- How the Bridge Finds the Target Instance
- Calling the Bridge from Vaadin
- Build Setup
- Errors You May See
A Vaadin view calls a Swing method by going through a generated bridge interface. The Swing side declares which methods are callable with @ExposedMethod; the build’s Maven plugin generates a typed *Bridge interface plus a proxy that handles EDT dispatch; the Vaadin side obtains a handle through SwingBridge.interop().of(MyBridge.class).
Exposing a Method on the Swing Side
Annotate any public method on a Swing class with @ExposedMethod:
Source code
Java
package com.example.swingapp;
import com.vaadin.swingbridge.interop.ExposedMethod;
import javax.swing.JFrame;
public class MainWindow extends JFrame {
@ExposedMethod
public void hideNavigation() {
navigationPanel.setVisible(false);
revalidate();
}
@ExposedMethod
public int countSelectedItems() {
return itemList.getSelectedIndices().length;
}
}At build time, the codegen plugin emits a matching bridge interface:
Source code
Java
// Generated into target/generated-sources/swing-bridge/
package com.example.swingapp;
public interface MainWindowBridge {
void hideNavigation();
int countSelectedItems();
}The Swing source remains a normal Swing class. It doesn’t import any Vaadin types.
The Four Invocation Shapes
@ExposedMethod takes one optional attribute, invocation(), with values Invocation.SYNC (default) and Invocation.ASYNC. Combined with the method’s return type, four useful shapes emerge:
| Return type | invocation() |
Bridge method shape | When to use |
|---|---|---|---|
|
|
| Stateless commands. The Vaadin thread returns immediately; the EDT runs the work later. |
|
|
| Quick state queries that won’t show a dialog or take long on the EDT. |
|
|
| Anything that might show a dialog, run slowly on the EDT, or otherwise risk blocking the Vaadin UI thread. |
|
|
| The Swing developer wants to compose multiple async stages themselves. |
Codegen rejects two combinations:
-
@ExposedMethod(invocation = ASYNC)on a method whose return type is alreadyCompletableFuture<T>(redundant). -
@ExposedMethod(invocation = ASYNC)on a method whose return type isRunnable(incompatible with the adder/remover-style listener pattern — see Listening to Swing Events).
Examples for Each Shape
Void, fire-and-forget — typical for UI commands:
Source code
Java
@ExposedMethod
public void refreshSidebar() {
sidebar.reload();
}Synchronous return — for cheap queries that complete on the EDT immediately:
Source code
Java
@ExposedMethod
public String getCurrentLocale() {
return localeService.getCurrentTag();
}Async return (codegen wraps the future) — for anything that might block on the EDT:
Source code
Java
@ExposedMethod(invocation = Invocation.ASYNC)
public Boolean confirmLeaveCurrentEditor() {
// Shows a modal Swing dialog. Don't block the Vaadin thread for this.
return JOptionPane.showConfirmDialog(this,
"Discard unsaved changes?",
"Confirm",
JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION;
}The generated bridge method has signature CompletableFuture<Boolean> confirmLeaveCurrentEditor(). The Swing source itself doesn’t import CompletableFuture — the codegen does the wrapping.
Explicit CompletableFuture return — when the Swing dev wants to compose async stages:
Source code
Java
@ExposedMethod
public CompletableFuture<String> fetchUserGreeting(String userId) {
return CompletableFuture
.supplyAsync(() -> userService.findGreeting(userId))
.thenApplyAsync(g -> g + ", " + LocalDate.now());
}How the Bridge Finds the Target Instance
Before a bridge method can run, the framework needs an instance to invoke it on. That instance is resolved per call using one of three discovery strategies, inferred from the annotated class — there is no @Discovery annotation to declare on user code.
| Strategy | Inferred when | Resolution |
|---|---|---|
| The annotated class extends | The framework uses the most recently visible window of that exact class on the Swing AppContext. |
| The annotated class has a | The factory is invoked on every call to obtain a fresh-or-cached instance. |
| All | No instance needed; static dispatch only. |
Mixing static and instance @ExposedMethod methods on the same class is a build error.
WINDOW — Main Frame and Dialogs
For methods on the application’s main JFrame or any other Window subclass, nothing extra is required:
Source code
Java
public class MainWindow extends JFrame {
@ExposedMethod
public void hideNavigation() { ... }
}The bridge resolves to whichever instance of MainWindow is currently visible in the Swing AppContext.
SINGLETON — @InstanceProvider for Long-Lived Panels
For a panel held in a customer-side registry — a common pattern in Swing applications that route between "editors" — declare a static factory and annotate it with @InstanceProvider:
Source code
Java
package com.example.swingapp.editors;
import com.vaadin.swingbridge.interop.ExposedMethod;
import com.vaadin.swingbridge.interop.InstanceProvider;
import javax.swing.JPanel;
public class CustomerPanel extends JPanel {
@InstanceProvider
public static CustomerPanel currentInstance() {
return EditorsRegistry.getInstance().get(CustomerPanel.class);
}
@ExposedMethod
public CustomerRecord getSelectedCustomer() {
return (CustomerRecord) customerList.getSelectedValue();
}
}Rules for the factory method:
-
Must be
public static. -
Must take no arguments.
-
Must return the annotated type (or a subtype).
-
Is invoked on every bridge call (not cached), so the registry can return different instances over time.
-
Runs on the EDT — it’s safe to touch Swing state inside it.
STATIC_ONLY — Utility Classes
For stateless helpers (settings, logging, lookups), make every annotated method static:
Source code
Java
public final class Settings {
private Settings() {}
@ExposedMethod
public static String getDefaultLocale() {
return Locale.getDefault().toLanguageTag();
}
@ExposedMethod
public static void setDefaultLocale(String tag) {
Locale.setDefault(Locale.forLanguageTag(tag));
}
}No instance is resolved; the bridge proxy calls the static methods directly.
Calling the Bridge from Vaadin
Inside a Vaadin view, get a BridgeHandle and use it. There are two entry points on the handle, matched to the two shapes of work.
For void or SYNC-with-result methods, register a one-shot callback with onReady:
Source code
Java
import com.example.swingapp.MainWindowBridge;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.notification.Notification;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import com.vaadin.swingbridge.SwingBridge;
@Route("main")
public class MainView extends VerticalLayout {
public MainView() {
add(new SwingBridge("com.example.swingapp.SwingAppMain"));
SwingBridge.interop()
.of(MainWindowBridge.class)
.onReady(main -> main.hideNavigation());
}
}onReady fires once, on the Vaadin UI thread, as soon as the bridge is resolvable. If the bridge is already ready when onReady is called (a re-attach, or a different view registering after the Swing app has booted), the callback fires immediately, synchronously, on the calling thread.
For ASYNC-shaped methods, use requestAsync. It awaits readiness, invokes the method, and relays the result off the Swing EDT so a whenComplete continuation can safely call ui.access(…):
Source code
Java
SwingBridge.interop().of(MainWindowBridge.class)
.requestAsync(MainWindowBridge::confirmLeaveCurrentEditor)
.whenComplete((approved, err) -> getUI().ifPresent(ui -> ui.access(() -> {
if (err != null) {
Notification.show("Could not confirm: " + err.getMessage());
} else if (Boolean.TRUE.equals(approved)) {
ui.navigate("home");
}
})));|
Note
|
The off-EDT relay is load-bearing. The Swing-side future is typically completed on the EDT (for example when a |
To check readiness without registering a callback — useful in a BeforeLeaveEvent handler, for example, where you may want to skip postponing navigation altogether if the Swing app hasn’t booted yet:
Source code
Java
if (SwingBridge.interop().of(MainWindowBridge.class).isReady()) {
// ...
}When more than one Swing application is hosted in the same Vaadin session (rare), disambiguate by passing the main class FQN:
Source code
Java
SwingBridge.interop("com.example.swingapp.SwingAppMain")
.of(MainWindowBridge.class)
.onReady(...);Build Setup
Swing-Side pom.xml
The Swing application’s build depends on swing-bridge-annotations so that source files can reference @ExposedMethod, @VaadinCallback, and @InstanceProvider:
Source code
XML
<dependencies>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>swing-bridge-annotations</artifactId>
<version>${swing-bridge.version}</version>
</dependency>
</dependencies>The annotations are RUNTIME-retention; no further changes are needed on the Swing side. The compiled JAR carries the annotation descriptors, which the codegen plugin reads at build time on the Vaadin side.
Vaadin-Side pom.xml
The Vaadin application’s build configures the codegen plugin so the generated *Bridge, *BridgeProxy, and *BridgeMetadata classes land on the compile classpath:
Source code
XML
<build>
<plugins>
<plugin>
<groupId>com.vaadin</groupId>
<artifactId>swing-bridge-codegen-maven-plugin</artifactId>
<version>${swing-bridge.version}</version>
<executions>
<execution>
<goals>
<goal>generate-bridge</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>The goal binds to generate-sources by default and:
-
Scans every
.jarin theapplibsdirectory (or wherever<applibsDir>points). -
Walks the bytecode with ASM, finding classes with
@ExposedMethod,@VaadinCallback, or@InstanceProvider. -
Emits the bridge interface, proxy class, and metadata class per annotated class into
target/generated-sources/swing-bridge/. -
Registers the output directory as an additional Maven compile source root so the generated classes are visible to Vaadin code with no extra wiring.
Errors You May See
| Symptom | What it means |
|---|---|
| The bridge call could not be dispatched: the target instance couldn’t be resolved, the Swing method threw, or the EDT exceeded the timeout. The original throwable is preserved as |
| Subtype of |
| Called outside a Vaadin UI thread, or called when no Swing AppContext is associated with the bridge yet. Make sure the call happens inside a Vaadin route or |
Build error referencing | Codegen could not infer a discovery strategy for the annotated class. Either make all |
Continue with Listening to Swing Events to wire the other direction — events emitted by Swing into Vaadin handlers.