JsInterop
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against GWT 2.13.1 (2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, jsinterop-annotations 2.0.0 (the version the GWT BOM pins), jsinterop-base 1.2.0 and Elemental2 1.3.2, and the Jakarta Servlet 6 server of the Bookshelf scenario, checked on 2026-10-08.
JsInterop is client knowledge: the annotations are read by the GWT compiler while it translates client and shared Java to JavaScript, and they decide what the generated JavaScript looks like at the boundary with the browser, with third-party scripts and with the host page.
The build side is one compiler flag and one module inheritance, both covered below; there is nothing to do on the server.
Every example on this page was compiled with the 2.13.1 compiler (from the command line and through mvn package with the gwt-app packaging), loaded in headless Chrome and exercised from JavaScript; the outputs and compiler messages are quoted as they were printed.
JsInterop is the modern way for Java and JavaScript to talk to each other in GWT.
It replaced hand-written JavaScript inside Java comments (JSNI, covered in JSNI & Overlay Types) with annotations on ordinary Java declarations: a Java type that stands for something JavaScript already provides is a native type, and a Java type that JavaScript is meant to call is an exported type.
The Elemental2 browser bindings (see DOM & Elemental2), jsinterop-base and parts of GWT’s emulated java.util classes are written with these annotations, so the rules on this page are the rules of the whole ecosystem.
The two directions
There are exactly two directions, and each has its own kind of Java type.
and widgets"] native["Native types
JsType(isNative = true)
NumberFormatJs, ChartJs, Shelf"] exp["Exported types
JsType
bookshelf.Api, bookshelf.BookRef"] cb["JsFunction interfaces
Listener, ClickHandler"] end subgraph js["JavaScript in the page"] browser["Browser globals
Intl, JSON, fetch, Promise"] lib["Third-party scripts
chart library, legacy Shelf"] host["Host page and other scripts
bookshelf.Api.open(7)"] end app -- "calls" --> native native -- "plain JS call or property access" --> browser native -- "plain JS call or property access" --> lib host -- "needs -generateJsInteropExports" --> exp exp -- "ordinary Java method call" --> app lib -- "invokes a JS function" --> cb cb -- "lambda body runs as Java" --> app app -- "passes a lambda" --> cb
Source: GWT — JsInterop
-
Java calls JavaScript. You write a Java declaration with the shape of the JavaScript API, mark it
@JsType(isNative = true), and the compiler emits a direct access to the JavaScript name. There is no wrapper object and no glue code:new NumberFormatJs("de-DE")compiles tonew $wnd.Intl.NumberFormat("de-DE"). -
JavaScript calls Java. You mark a Java type or member as exported and (in GWT) compile with
-generateJsInteropExports; the compiler then makes the member reachable under a stable, non-obfuscated JavaScript name such asbookshelf.Api.formatCopies. -
Callbacks cross both ways through
@JsFunctioninterfaces: a Java lambda becomes a real JavaScript function, and a JavaScript function becomes a Java interface instance.
The two kinds of type are easy to confuse because they use the same annotation, so keep this table in mind.
Native type: @JsType(isNative = true) |
Exported type: @JsType |
|
|---|---|---|
Who implements it |
JavaScript. The Java class is only a description (a facade). |
Java. JavaScript only calls it. |
Method bodies |
|
Ordinary Java. |
Names in the output |
The JavaScript name given by |
|
|
Allowed against native classes, rejected against native interfaces. |
Ordinary Java |
Typical size cost |
Zero: nothing is generated for the facade. |
Every exported member is kept alive, so it cannot be pruned. |
Setting up
JsInterop needs three things in a Maven build: the annotations jar (already on the classpath), the compiler flag for exports (only if JavaScript calls Java), and optionally the helper libraries.
bookshelf-interop-lab/pom.xml)<dependencies>
<dependency>
<groupId>org.gwtproject</groupId>
<artifactId>gwt-user</artifactId>
</dependency>
<dependency>
<groupId>org.gwtproject</groupId>
<artifactId>gwt-dev</artifactId>
</dependency>
<dependency>
<groupId>com.google.jsinterop</groupId>
<artifactId>base</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>com.google.elemental2</groupId>
<artifactId>elemental2-dom</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.google.elemental2</groupId>
<artifactId>elemental2-promise</artifactId>
<version>1.3.2</version>
</dependency>
</dependencies>
The org.gwtproject:gwt BOM manages jsinterop-annotations (2.0.0 in the 2.13.1 BOM), and gwt-user brings it in transitively, so the annotations need no dependency of their own.
mvn dependency:tree on the module above printed com.google.jsinterop:jsinterop-annotations:jar:2.0.0:compile under gwt-user, com.google.jsinterop:base:jar:1.2.0:compile with org.jspecify:jspecify:jar:0.3.0:compile, and the three Elemental2 jars (elemental2-dom, elemental2-core and elemental2-promise, all 1.3.2).
Maven Central also lists jsinterop-annotations 2.1.0 (2023-07-18); jsinterop-base 1.2.0 itself declares 2.0.2, and the BOM-managed 2.0.0 won the resolution without any problem.
Each library that has GWT-specific source needs a <inherits> line in the module XML, because GWT compiles from source, not from bytecode.
src/main/module.gwt.xml)<?xml version="1.0" encoding="UTF-8"?>
<module>
<inherits name="com.google.gwt.user.User"/>
<inherits name="elemental2.dom.Dom"/>
<inherits name="elemental2.promise.Promise"/>
<inherits name="jsinterop.base.Base"/>
<source path="client"/>
<entry-point class="com.example.lab.client.Lab"/>
</module>
Source: jsinterop-base — README
The annotations themselves need no <inherits>; jsinterop.base.Base is the module named in the jsinterop-base README, and the two Elemental2 modules above come from the .gwt.xml files in the 1.3.2 jars (elemental2/dom/Dom.gwt.xml, elemental2/promise/Promise.gwt.xml, which itself inherits jsinterop.base.Base).
The exports flag goes to the production compiler and to Super Dev Mode, and the Maven plugin has a separate argument list for each of them (see Build Tools: Maven & Gradle for the plugin). The flag is described among the compiler flags in Compiler & Compilation, and Super Dev Mode in Development Mode & Debugging.
<plugin>
<groupId>net.ltgt.gwt.maven</groupId>
<artifactId>gwt-maven-plugin</artifactId>
<configuration>
<moduleName>com.example.lab.Lab</moduleName>
<moduleShortName>lab</moduleShortName>
<compilerArgs>
<arg>-generateJsInteropExports</arg>
</compilerArgs>
<codeserverArgs>
<arg>-generateJsInteropExports</arg>
</codeserverArgs>
</configuration>
</plugin>
The Bookshelf scratch application sets only compilerArgs; that is enough for mvn package, but gwt:codeserver reads codeserverArgs, so a developer who tests bookshelf.Api from the host page in Super Dev Mode must add the second list.
The CodeServer class accepts the flag itself: started on the command line with -generateJsInteropExports, its compiled output contained 'bookshelf.Api' and 'bookshelf.BookRef' registrations, and without the flag the same compile contained none.
The Developer Guide’s own link for the annotation Javadoc, /javadoc/latest/jsinterop/annotations/package-summary.html on gwtproject.org, answered 404 on 2026-10-08, and the DevGuideJsInterop.html name some pages use is also a 404; the real page is DevGuideCodingBasicsJsInterop.html (linked from the Client-side menu as "JavaScript: JsInterop").
The annotation Javadoc is on javadoc.io.
The annotations
The table below was built from the jsinterop-annotations 2.0.0 sources (the @Target of each annotation) and from the 2.13.1 compiler sources (JsInteropUtil reads the annotations; JsInteropRestrictionChecker enforces the rules), and every row marked run was compiled and run in the lab.
| Annotation | Applies to (@Target) |
Meaning | GWT 2.13.1 |
|---|---|---|---|
|
a type |
Attributes |
Honoured (run). |
|
a method |
Attributes |
Honoured (run). |
|
a method or a field |
Attributes |
Honoured (run). |
|
a constructor |
The constructor that JavaScript calls with |
Honoured (run). |
|
a type |
Marks a functional interface (exactly one abstract method) as the Java view of a JavaScript function. |
Honoured (run). |
|
a method or a field |
Adds a Java-only member (a |
Honoured (run). |
|
a package |
One attribute, |
|
|
a constructor, a method or a field |
Removes one member from the exports of a |
Honoured (run). Rejected on members of a native type. |
|
a parameter |
The parameter may be omitted by JavaScript callers. It cannot be primitive and cannot precede a required parameter. Its Javadoc calls it informational for the GWT compiler. |
Accepted and validated (run): the three rules are compile errors (see the pitfalls table), and an omitted |
|
a type use |
Mark a type as nullable ( |
Accepted, no effect (run): nothing in |
|
a method |
Transpiles the method to an |
Accepted, no effect (run). The 2.9.0 release notes list |
|
a type |
Attributes |
Accepted, no effect (run): the enum stays a normal Java enum object. The GWT 2.14 milestone has an open item to explore |
The three "no effect" rows deserve a concrete picture.
A probe module compiled @JsNullable, @JsNonNull, @JsAsync and an @JsEnum enum without any error.
The only output was two warnings about the enum, [unusable-by-js] Type of parameter 'c' in 'String Probe.color(Probe.Color)' is not usable by but exposed to JavaScript. and the matching return-type warning, and in the browser the value returned by probe.Probe.green() was a plain Java enum object whose own keys were ["name_0","ordinal_0"].
Write these annotations only for code that must also compile with J2CL, and do not expect them to change what GWT generates.
The attribute defaults and the rules that the compiler checks are summarised here; each rule is quoted in the pitfalls table with the exact message.
-
Namespace and name of an exported type. The default
namespaceis the Java package (the compiler’s own message about two exports clashing names'com.example.bad2.client.TwoCtors'), and the defaultnameis the simple class name.JsPackage.GLOBALas the namespace of a native type means "a global". -
Members. A public member of a
@JsTypeis exported with its Java name unless@JsMethod,@JsPropertyor@JsIgnoresays otherwise. The Developer Guide says the annotation exposes the public non-static members; on 2.13.1 the public static members ofbookshelf.Api(VERSION,formatCopies,sum) were exported too. -
Not transitive.
@JsTypeon a class does not make the types in its signatures exportable; each needs its own annotation, or JavaScript receives opaque objects (the[unusable-by-js]warning below). -
Unknown shapes. A native type may use the name
"?"(unknown) or"*"(any):jsinterop-basenames itsAnytype"*"and Elemental2’s union helper types use"?". The Developer Guide describes"?"for a JavaScript API that has no well-defined type, and the 2.8.1 release notes added both names.
Calling JavaScript from Java: native types
A native type is a Java class or interface with the shape of a JavaScript API.
The compiler never generates its implementation; it generates calls to the JavaScript name.
The rules are those of the Developer Guide and the checker in the compiler: constructors are empty, methods are native (or @JsOverlay), fields have no initializer and are not final, and a native class extends only native classes.
A first binding: Intl.NumberFormat
The Bookshelf shows the number of copies per book and the price in the reader’s locale.
The browser already has a locale-aware formatter, Intl.NumberFormat, so there is nothing to ship: the binding is a facade.
This is the class that the Bookshelf scratch application contains.
bookshelf-client)package com.example.bookshelf.client.interop;
import jsinterop.annotations.JsType;
/** Binding for the browser's {@code Intl.NumberFormat}; native types need no JavaScript glue. */
@JsType(isNative = true, namespace = "Intl", name = "NumberFormat")
public class NumberFormatJs {
public NumberFormatJs(String locale) {}
public native String format(double value);
}
Source: MDN — Intl.NumberFormat
-
namespace = "Intl"andname = "NumberFormat"are the JavaScript name; the Java class is calledNumberFormatJsonly to avoid a clash with GWT’s owncom.google.gwt.i18n.client.NumberFormat. -
The constructor body is empty, as the compiler requires; the arguments are passed straight through.
-
format(double)isnative: there is no Java body, and the call compiles to.format(n)on the JavaScript object.
Compiled with -style PRETTY, a method that used the class produced this JavaScript (the whole facade disappears):
new NumberFormatJs(locale).format(n)return (new $wnd.Intl.NumberFormat(locale)).format(n);
$wnd is the window that hosts the page, not the iframe in which the compiled module runs.
Native types with JsPackage.GLOBAL (or a namespace such as Intl) are resolved through $wnd, which is why a library loaded by an ordinary <script> tag of the host page is visible to them.
Options objects: a native type that is a plain object
JavaScript APIs take "options bags".
A native type named Object in the global namespace is the idiom for them: it adds no behaviour, only typed property names.
@JsOverlay factory (FormatOptions.java)package com.example.lab.client.intl;
import jsinterop.annotations.JsOverlay;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;
/** Options bag for {@code Intl.NumberFormat}: a plain JavaScript object, never a Java object. */
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class FormatOptions {
public String style;
public String currency;
public int maximumFractionDigits;
@JsOverlay
public static FormatOptions currency(String code) {
FormatOptions o = new FormatOptions();
o.style = "currency";
o.currency = code;
return o;
}
}
Two details matter here.
First, the fields are public and not initialised, so only the properties that Java sets exist on the JavaScript object: the compiler printed (o = new $wnd.Object , o.style = 'currency' , o.currency = 'EUR' , o), and no maximumFractionDigits property was created.
Second, @JsOverlay lets the facade carry a final helper (the factory) that is plain Java and is inlined at each call site; JavaScript never sees it.
A second constructor taking the options object is a second declaration of the same native constructor.
NumberFormatJs eur = new NumberFormatJs("es-ES", FormatOptions.currency("EUR"));
String text = eur.format(1234.5); // "1234,50", a no-break space and the euro sign in headless Chrome
Source: MDN — Intl.NumberFormat
In headless Chrome the call printed 1234,50 € for es-ES (Spanish formatting does not group four-digit numbers) and 1.234,5 for de-DE through the exported bookshelf.Api.formatCopies.
The actual strings are the browser’s: they depend on its ICU data, so tests should not hard-code them.
Properties, methods and static members
JavaScript properties are not methods.
Java has no way to say "this getter reads a property", so JsInterop uses an annotation on the accessor.
The lab binds a small legacy script that defines a constructor Shelf with a read-only count accessor property, a label data property, a put method and a static kind property.
function Shelf(name) { this.name = name; this._items = []; this.label = name; }
Shelf.kind = "legacy";
Object.defineProperty(Shelf.prototype, "count", { get: function () { return this._items.length; } });
Shelf.prototype.put = function (t) { this._items.push(t); return this; };
Shelf.java)package com.example.lab.client.shelf;
import jsinterop.annotations.JsMethod;
import jsinterop.annotations.JsOverlay;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsProperty;
import jsinterop.annotations.JsType;
/** Binding for a hypothetical legacy script: function Shelf(name) { ... }. */
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Shelf")
public class Shelf {
@JsProperty(name = "kind")
public static native String getKind();
public Shelf(String name) {}
@JsProperty
public native int getCount();
@JsProperty(name = "label")
public native String getLabel();
@JsProperty(name = "label")
public native void setLabel(String label);
@JsMethod(name = "put")
public native Shelf add(String title);
@JsOverlay
public final Shelf addAll(String... titles) {
for (String t : titles) {
add(t);
}
return this;
}
}
Shelf shelf = new Shelf("Fiction");
shelf.addAll("Dune", "Emma").setLabel("Shelf A");
String summary = shelf.getLabel() + ":" + shelf.getCount() + ":" + Shelf.getKind();
// summary is "Shelf A:2:legacy"
-
@JsPropertyongetCount()makes the callshelf.getCount()readshelf.count; the name is derived from the JavaBean convention (getCountis propertycount), and@JsProperty(name = "label")gives the getter and setter an explicit name. -
A static property accessor is allowed only on a native type, which is why
getKind()readsShelf.kind; the compiler rejects it elsewhere (Static property accessor '%s' can only be native.). -
@JsMethod(name = "put")binds the Java methodaddto the JavaScriptput; without it Javaaddwould look forshelf.add. -
addAll(String...)is a@JsOverlaymethod, so it exists only in Java and expands intoputcalls; the generated JavaScript contained noaddAll. -
A native interface describes a structural type whose implementers are plain JavaScript objects; because there is no runtime marker to test, the compiler forbids
instanceofagainst it.
Globals, duck typing and JSON
A global function or object needs no class of its own to be wrapped.
The Developer Guide’s example binds JSON, whose members are static; the same class serves the lab’s JSON examples.
JsonNative.java)package com.example.lab.client;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "JSON")
public class JsonNative {
public static native Object parse(String text);
public static native String stringify(Object value);
}
For structures that have no constructor to name (an object returned by JSON.parse, a configuration literal), the options-bag idiom above works, and jsinterop-base (see [jsinterop-base]) offers JsPropertyMap for the dynamic case.
A data transfer object that mirrors a JSON document is therefore a native type named Object:
BookDto.java)package com.example.lab.client.exported;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;
/** A JSON-shaped DTO: native type means "a plain JavaScript object with these properties". */
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class BookDto {
public String title;
public int copies;
}
JSON.stringify of such an object printed {"title":"Dune","copies":3}.
JSON.stringify of an ordinary (non-@JsType) Java object holding the same values printed {}, because the fields of a GWT-compiled Java object are not JSON-visible properties; if JavaScript, or a library such as a chart or a store, must read the fields, the object must be a native DTO or an exported type.
Callbacks: @JsFunction
A JavaScript function that is passed as an argument is a Java functional interface annotated with @JsFunction.
Because the annotation implies "this is a function value", GWT turns a Java lambda that implements the interface into a real JavaScript function.
ChartOptions.java)package com.example.lab.client.chart;
import jsinterop.annotations.JsFunction;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class ChartOptions {
/** A JavaScript callback: the browser calls it, Java supplies it as a lambda. */
@JsFunction
public interface ClickHandler {
void onClick(Object event, ChartElement[] elements);
}
public ClickHandler onClick;
}
A @JsFunction interface cannot extend another interface, cannot declare members other than @JsOverlay ones, and cannot be implemented by a class that is also a @JsType; each rule has a compiler message in the pitfalls table. An instanceof or cast against a @JsFunction is, per its Javadoc, a JavaScript typeof test for a function.
The direction of the call decides which side owns the function: a lambda passed to a native API (options.onClick = ...) is a Java-implemented function called by JavaScript, and the parameter of an exported method typed with a @JsFunction interface (Api.subscribe(Listener)) is a JavaScript function that Java calls.
Worked example: a chart library for the Bookshelf
The Bookshelf’s admin area shows how many loans were made each month.
Chart.js draws a bar chart from a configuration object: new Chart(canvas, config), a data object with labels and datasets, a chart.update() after the data is changed, chart.destroy() when the canvas is thrown away, and an onClick option that is called with the event, the active elements and the chart.
That is the surface the lab binds; it is documented on the Chart.js site (links below).
The lab does not download or run Chart.js.
It runs a short stand-in script with the same constructor, update, destroy and onClick surface, which records its calls; what is exercised is the binding, and a real Chart.js build would be loaded by the same <script> tag.
The binding is five native types and one widget.
ChartJs.java)package com.example.lab.client.chart;
import com.google.gwt.dom.client.CanvasElement;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsType;
/** Binding for a Chart.js-style constructor: {@code new Chart(canvas, config)}. */
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Chart")
public class ChartJs {
public ChartData data;
public ChartJs(CanvasElement canvas, ChartConfig config) {}
public native void update();
public native void destroy();
}
ChartConfig.java, ChartData.java, Dataset.java, ChartElement.java)@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class ChartConfig {
public String type;
public ChartData data;
public ChartOptions options;
@JsOverlay
public static ChartConfig bar(String[] labels, String seriesName, double[] values) {
ChartConfig c = new ChartConfig();
c.type = "bar";
c.data = new ChartData();
c.data.labels = labels;
Dataset ds = new Dataset();
ds.label = seriesName;
ds.data = values;
c.data.datasets = new Dataset[] {ds};
return c;
}
}
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class ChartData {
public String[] labels;
public Dataset[] datasets;
}
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class Dataset {
public String label;
public double[] data;
}
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object")
public class ChartElement {
public int index;
public int datasetIndex;
}
Notes on the design:
-
double[]andString[]are Java arrays, which GWT represents as JavaScript arrays, so they are valid as thedataandlabelsproperties. The compiler printed no warning for them. -
CanvasElementis a GWT DOM class (aJavaScriptObjectoverlay), and passes through the boundary unchanged: a JSO is a plain JavaScript object, so it can be passed as an argument to a native constructor. -
ChartConfig.bar(...)is a@JsOverlayfactory, so the call site reads like a normal Java API while the output is an object literal built from$wnd.Object. -
ChartOptions.onClickuses the@JsFunctioncallback shown earlier, with the first parameter typedObject: Chart.js passes an event object that Java never inspects.
The widget wraps the lifetime: the chart exists only while the canvas is attached.
LoanChart.java)package com.example.lab.client.chart;
import com.google.gwt.dom.client.CanvasElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.user.client.ui.Widget;
import java.util.function.IntConsumer;
/** A GWT widget around the native chart: create on attach, destroy on detach. */
public class LoanChart extends Widget {
private final String[] months;
private double[] loans;
private final IntConsumer onMonth;
private ChartJs chart;
public LoanChart(String[] months, double[] loans, IntConsumer onMonth) {
this.months = months;
this.loans = loans;
this.onMonth = onMonth;
setElement(Document.get().createCanvasElement());
}
public void setLoans(double[] values) {
this.loans = values;
if (chart != null) {
chart.data.datasets[0].data = values;
chart.update();
}
}
@Override
protected void onLoad() {
ChartConfig config = ChartConfig.bar(months, "Loans", loans);
config.options = new ChartOptions();
config.options.onClick = (event, elements) -> {
if (elements.length > 0) {
onMonth.accept(elements[0].index);
}
};
chart = new ChartJs((CanvasElement) getElement().cast(), config);
}
@Override
protected void onUnload() {
if (chart != null) {
chart.destroy();
chart = null;
}
}
}
Run in headless Chrome, a page that attached the widget, called setLoans and detached it left this call log in the stand-in library: new bar,update,new bar,update,destroy (the first pair is a chart created directly by the entry point).
The destroy at the end is the important line: a native chart library keeps references to the canvas and its event listeners, and GWT’s widget life cycle (onLoad, onUnload) is the right place to release them.
Loading the library is a host-page decision. There are three ways, and the first is the simplest.
| Way | How | Trade-off |
|---|---|---|
Host page |
|
The library loads for every visitor, even those who never see the chart, but it is guaranteed to be there when |
|
|
With the default |
|
|
Loads on demand (for example when the admin area opens). The callback tells you when the global is ready; |
The lab loaded late.js (a one-line script assigning window.lateLib = 'v1') with ScriptInjector.fromUrl("late.js").setWindow(ScriptInjector.TOP_WINDOW), and the callback read the global back through Js.global().get("lateLib") and printed loaded:v1.
Source: GWT Javadoc — ScriptInjector
The Developer Guide’s own example does the same for the Leaflet map library: two native classes, L (global) and Map (namespace L), and a one-line call L.map("map").setView(positions, 13) in an EntryPoint. That example comes from a 2016 community repository (zak905/jsinterop-leaflet, last push 2016-05-31), so it shows the technique but its dependency versions are historical.
Exporting Java to JavaScript
Sometimes the caller is not Java: a legacy script on the Bookshelf host page, a Selenium test, a browser extension or a hand-written widget wants to open a book or format a number with the application’s own code. JsInterop makes a Java type reachable from JavaScript by exporting it under a name that the compiler does not obfuscate.
The Bookshelf’s exported API
This is ExportedApi, as it stands in the scratch application.
bookshelf-client)package com.example.bookshelf.client.interop;
import com.example.bookshelf.client.ClientFactory;
import com.example.bookshelf.client.place.BookPlace;
import jsinterop.annotations.JsMethod;
import jsinterop.annotations.JsType;
/** Java API exported to JavaScript as {@code bookshelf.Api} (needs -generateJsInteropExports). */
@JsType(namespace = "bookshelf", name = "Api")
public final class ExportedApi {
private static ClientFactory factory;
private ExportedApi() {}
public static void install(ClientFactory f) { factory = f; }
public static void open(double id) { factory.getPlaceController().goTo(new BookPlace((long) id)); }
@JsMethod
public static String formatCopies(String locale, double n) { return new NumberFormatJs(locale).format(n); }
}
Compiling the module printed a warning for it:
Warnings in com/example/bookshelf/client/interop/ExportedApi.java
[WARN] Line 15: [unusable-by-js] Type of parameter 'f' in 'void ExportedApi.install(ClientFactory)' is not usable by but exposed to JavaScript.
[WARN] Suppress "[unusable-by-js]" warnings by adding a `@SuppressWarnings("unusable-by-js")` annotation to the corresponding member.
The warning is accurate.
install(ClientFactory) is public in a @JsType, so it is exported, but ClientFactory is not a @JsType, so no JavaScript caller can supply a meaningful argument.
install is an internal hook that only App.onModuleLoad calls.
The fix is @JsIgnore, which removes the method from the exports, and it is what the lab’s Api does for its fire method.
A fuller example: the lab’s Api and BookRef
The lab has an exported static API and an exported class with instances.
Api.java)@JsType(namespace = "bookshelf", name = "Api")
public final class Api {
/** A JavaScript function that Java can call back. */
@JsFunction
public interface Listener {
void on(String event);
}
public static final String VERSION = "1.0";
private static Listener listener;
private Api() {}
public static String formatCopies(String locale, double n) {
return new NumberFormatJs(locale).format(n);
}
public static void subscribe(Listener l) {
listener = l;
}
@JsIgnore
public static void fire(String event) {
if (listener != null) {
listener.on(event);
}
}
@JsMethod(name = "sum")
public static double add(double a, double b) {
return a + b;
}
}
BookRef.java)package com.example.lab.client.exported;
import jsinterop.annotations.JsConstructor;
import jsinterop.annotations.JsIgnore;
import jsinterop.annotations.JsProperty;
import jsinterop.annotations.JsType;
/** A Java class used from JavaScript as {@code new bookshelf.BookRef("Dune", 7)}. */
@JsType(namespace = "bookshelf", name = "BookRef")
public class BookRef {
public String title;
private final double id;
@JsConstructor
public BookRef(String title, double id) {
this.title = title;
this.id = id;
}
@JsIgnore
public BookRef(long id) {
this("#" + id, id);
}
@JsProperty
public double getId() {
return id;
}
public String describe() {
return title + " (" + (long) id + ")";
}
@JsIgnore
public boolean isSameBook(BookRef other) {
return other != null && other.id == id;
}
}
bookshelf.Api.formatCopies('de-DE', 1234.5); // "1.234,5"
bookshelf.Api.sum(2, 3); // 5 (Java name add, JavaScript name sum)
bookshelf.Api.VERSION; // "1.0"
var b = new bookshelf.BookRef('Dune', 7);
b.describe(); // "Dune (7)"
b.id; // 7 (the @JsProperty getter getId)
b.title; // "Dune"
typeof b.isSameBook; // "undefined" (@JsIgnore)
typeof bookshelf.Api.fire; // "undefined" (@JsIgnore)
-
namespace = "bookshelf"plusname = "Api"produce the globalbookshelf.Api; the nested name is created by the generated code. -
@JsConstructoris the constructor JavaScript’snewcalls. The class has a second constructor taking along; it carries@JsIgnore, because a@JsTypeclass may have only one exported constructor (the pitfalls table shows the three errors the compiler reports without it). -
@JsPropertyongetId()exposesidas a property,b.id, with no parentheses. -
@JsMethod(name = "sum")renames the Java methodadd. -
static final String VERSIONis exported as a property of the namespace object. -
The
@JsFunctioninterfaceListeneris the type of the argument ofsubscribe: JavaScript passes a plain function, and Java calls it withlistener.on(event).
The compiler emits the exports as a block of assignments. In -style PRETTY output (trimmed to the relevant lines):
_ = provide('bookshelf.Api');
_.VERSION = VERSION;
_.formatCopies = formatCopies;
_.subscribe = subscribe;
_.sum = add_0;
_ = provide('bookshelf.BookRef', BookRef);
The Java method add is exported under the JavaScript name sum, and fire does not appear because of @JsIgnore.
The member names are exactly the exported JavaScript names even though everything else in the output (add_0, BookRef) is the compiler’s.
The export flags
Exports are generated only when asked for.
The flags are printed by Compiler -help and are the same for the code server.
| Flag | Meaning | Observed on 2.13.1 |
|---|---|---|
|
Generate the exports requested by the annotations. Default OFF. |
Without it |
|
Export only members whose qualified name matches. Repeatable. Only effective together with the flag above. |
With |
|
Remove matches from the included set. It must come after an include, or the compiler prints |
Including |
Details worth knowing:
-
The patterns are Java regular expressions, not the package wildcards that the 2.8.1 release notes describe. The class
RegexFiltercompiles each argument withjava.util.regex.Pattern; the.*suffix in the examples above is a regular expression, and the dots in a package name are best escaped. Quote the argument in the shell. A pattern may not start with+or-. -
Without a filter, everything is exported. In the PRETTY output of the lab, next to
bookshelf.Api, the compiler registeredjava.lang.String,java.util.Listand other JRE emulation types (for example_.copyOf = copyOf;underprovide('java.util.List')), because GWT’s own emulated classes carry JsInterop annotations. An include filter that names only your packages keeps the output smaller and keeps the global namespace yours. -
Exports cost bytes and defeat pruning. The measurement in Compiler & Compilation shows a growth from 244,639 to 263,127 bytes of the main file when the flag was on in the Bookshelf.
-
Code server. In Super Dev Mode the flag must reach
gwt:codeserver(codeserverArgs), as described under [setup]; theCodeServerclass accepts it and its compiled output then contained the exports. -
The flag is a build choice, not part of the code. The same annotated classes compiled without it simply have no JavaScript entry point; the Java callers are unaffected.
What crosses the boundary
Java and JavaScript do not share a type system, so the values that cross the line need care. The table lists each case that was tried; "in" means a JavaScript caller passing a value to an exported Java method, "out" means a Java value returned or thrown to JavaScript.
| Case | What happened | Advice |
|---|---|---|
|
Both arrived as Java |
Test with |
|
Passed unchanged ( |
The safe numeric type of the boundary. |
|
No conversion or range check: |
An |
|
The compiler warns |
Use |
Java array ( |
A JavaScript array works: |
Use arrays, not collections, in exported signatures. |
|
|
Take an array and wrap it in Java ( |
Object fields in JSON |
|
Export or use native DTOs for anything JavaScript must read. |
Private state of an exported class |
|
Do not rely on, or serialise, the own properties of an exported object; keep the contract in |
Java exception out |
|
Catch and convert at the boundary if the script needs a particular shape. |
JavaScript exception in |
A listener that threw |
Catch |
Missing global in a native binding |
With the |
Read the message, not just the class; load the script before the module. |
|
|
See [jsinterop-base]. |
jsinterop-base: Js, JsPropertyMap and Any
Annotations describe static shapes.
Some JavaScript code is not static: a JSON document whose properties are only known at run time, a value of unknown type, the global object, an array-like.
jsinterop-base (com.google.jsinterop:base) is a small library of native types and helpers for these cases.
It is written once and compiled by GWT and by J2CL.
| Type | What it offers |
|---|---|
|
Static helpers: |
|
A native type named |
|
A native type named |
|
An array-like object: |
|
A JavaScript constructor as a function: |
|
A marker for the |
Source: jsinterop-base on GitHub
The lab parses a JSON string and reads it without declaring a DTO:
JsPropertyMap and Any (excerpt of Lab.java)JsPropertyMap<Object> book = Js.asPropertyMap(
JsonNative.parse("{\"title\":\"Dune\",\"copies\":3,\"tags\":[\"sf\"]}"));
Any title = book.getAsAny("title");
String line = "title=" + title.asString()
+ " copies=" + book.getAsAny("copies").asInt()
+ " has(isbn)=" + book.has("isbn")
+ " typeof=" + Js.typeof(book.get("tags"));
Source: jsinterop-base on GitHub
In Chrome line was title=Dune copies=3 has(isbn)=false typeof=object.
A few notes from the sources and the runs:
-
Js.asPropertyMap(x)andJs.asAny(x)do not copy or wrap: they cast. The result is the same JavaScript value. -
Js.global()is the global object;Js.global().get("lateLib")returned the valuev1that a script set onwindow. -
Js.castreturns its argument with the target type inferred from the assignment (the Java compiler inserts the cast at the call site);Js.uncheckedCastis the unchecked variant, and its Javadoc warns that it lets wrong types spread through the program, so prefer a regular cast. -
The conversions check, but only when
jsinterop.checksisENABLED.Base.gwt.xmldefinesjsinterop.checkswith the valuesENABLEDandDISABLEDand sets it toDISABLED; its comment says that disabling the checks only affects production. With the default,Js.asAny(9.5).asInt()returned9.5; with<set-property name="jsinterop.checks" value="ENABLED"/>in the application module it threwClassCastException. -
A
JsPropertyMapis the right tool for reading JSON of unknown shape; for a stable shape, a native DTO is shorter and typed.
Promises and callbacks
GWT has two callback styles, and the browser has a third.
-
AsyncCallback<T>is the GWT-RPC interface withonFailure(Throwable)andonSuccess(T). It is a normal Java interface, not a JavaScript function. -
@JsFunctioninterfaces are JavaScript functions seen from Java, as shown above. -
Promises are the JavaScript
Promiseobject, which the browser returns fromfetch, fromResponse.json()and from many web APIs. Elemental2 provideselemental2.promise.Promise<T>andIThenable<T>as native types; their methods arethen,catch_(bound to JavaScript’scatchwith@JsMethod(name = "catch")),finally_, and the staticall,race,resolveandreject.
@JsAsync, the annotation for async functions, has no effect in GWT 2.13.1 (see the annotation table), so a GWT method returns a Promise explicitly instead of declaring async.
Consuming a Promise: fetch
fetch and the JSON it returns (Net.java)package com.example.lab.client;
import com.google.gwt.user.client.rpc.AsyncCallback;
import elemental2.dom.DomGlobal;
import elemental2.promise.Promise;
import java.util.function.Consumer;
import jsinterop.base.Any;
import jsinterop.base.Js;
import jsinterop.base.JsPropertyMap;
/** Promise-based code next to GWT's callback style. */
public final class Net {
private Net() {}
/** fetch() returns a Promise; the chain below never blocks. */
public static Promise<String> firstTitle(String url) {
return DomGlobal.fetch(url)
.then(response -> {
if (!response.ok) {
return Promise.reject("HTTP " + response.status);
}
return response.json();
})
.then(json -> {
JsPropertyMap<Object> page = Js.asPropertyMap(json);
Any[] books = page.getAsAny("books").asArray();
return Promise.resolve(books[0].asPropertyMap().getAsAny("title").asString());
});
}
/** Adapter: expose a callback-style call as a Promise. */
public static <T> Promise<T> toPromise(Consumer<AsyncCallback<T>> call) {
return new Promise<>((resolve, reject) ->
call.accept(new AsyncCallback<T>() {
@Override
public void onFailure(Throwable caught) {
reject.onInvoke(caught.getMessage());
}
@Override
public void onSuccess(T result) {
resolve.onInvoke(result);
}
}));
}
/** Adapter the other way: consume a Promise through an AsyncCallback. */
public static <T> void fromPromise(Promise<T> promise, AsyncCallback<T> callback) {
promise.then(value -> {
callback.onSuccess(value);
return null;
}, error -> {
callback.onFailure(new RuntimeException(String.valueOf(error)));
return null;
});
}
}
Source: MDN — fetch()
The lab served a one-book JSON file and called these methods:
-
Net.firstTitle("books.json")resolved toDune, readingbooks[0].titlethroughJsPropertyMapandAny. -
Net.firstTitle("missing.json")rejected withHTTP 404, the string passed toPromise.reject. -
Net.<String>toPromise(cb -> cb.onSuccess("rpc-ok"))turned a callback-style call into a Promise that resolved torpc-ok. -
Net.fromPromise(promise, callback)did the reverse: for the failedfetchit calledonFailurewith aRuntimeExceptionwhose message wasHTTP 404.
Three observations about the code:
-
The lambdas passed to
thenare@JsFunctionlambdas, and the generic types (Promise<String>,IThenable<...>) are erased as usual; the Java compiler infers the chain. -
A promise rejection value can be anything. The adapter converts it with
String.valueOf(error); a production adapter should inspect it (for afetchfailure it is aTypeError, for a rejectedResponsethe code above builds the message itself). -
The callbacks run later, on the browser’s event loop; the GWT code is single-threaded and needs no synchronisation, but a user can navigate away before the Promise settles, so check that the presenter is still attached.
Producing a Promise for JavaScript
An exported method can return a Promise and a JavaScript caller then uses .then and await on it directly.
Api.java)public static Promise<String> titleOf(double id) {
return new Promise<>((resolve, reject) ->
Scheduler.get().scheduleDeferred(() -> {
if (id < 0) {
reject.onInvoke("no such book: " + id);
} else {
resolve.onInvoke("Title #" + (long) id);
}
}));
}
bookshelf.Api.titleOf(5)
.then(function (t) { /* "Title #5" */ return bookshelf.Api.titleOf(-1); })
.catch(function (e) { /* "no such book: -1" */ });
The Promise constructor takes a @JsFunction executor (resolve, reject), and calling resolve.onInvoke(value) or reject.onInvoke(reason) is how Java settles it.
Scheduler.scheduleDeferred is the GWT way to run something after the current event; Delayed Logic & Scheduler covers it.
Web components
A custom element is a JavaScript class registered with customElements.define; from Java it is a native class that extends the DOM element type.
The lab registers book-badge in the host page and binds it:
BookBadge.java)class BookBadge extends HTMLElement {
get count() { return Number(this.getAttribute("count")); }
set count(v) { this.setAttribute("count", v); this.textContent = v + " books"; }
}
customElements.define("book-badge", BookBadge);
window.BookBadge = BookBadge;
package com.example.lab.client.shelf;
import elemental2.dom.HTMLElement;
import jsinterop.annotations.JsPackage;
import jsinterop.annotations.JsProperty;
import jsinterop.annotations.JsType;
/** Binding for a custom element registered by the page: customElements.define("book-badge", BookBadge). */
@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "BookBadge")
public class BookBadge extends HTMLElement {
@JsProperty
public native int getCount();
@JsProperty
public native void setCount(int count);
}
BookBadge badge = (BookBadge) DomGlobal.document.createElement("book-badge");
badge.setCount(3);
DomGlobal.document.body.appendChild(badge);
// badge.getCount() is 3, badge.textContent is "3 books", badge.tagName is "BOOK-BADGE"
Source: MDN — Web components
window.BookBadge = BookBadge is there because a native class is resolved by its global name: the cast (BookBadge) element compiles to an instanceof $wnd.BookBadge test.
The element’s rendering, styling and lifecycle belong to JavaScript; Java sees properties and methods.
The Polymer tutorial, as history
The Developer Guide’s tutorial menu still lists "GWT & Polymer", a six-page tutorial that builds a to-do application with Polymer Web Components through the vaadin-gwt-polymer-elements library.
It belongs to the early JsInterop period and should be read as history:
-
It targets the experimental JsInterop of GWT 2.7: its project page tells you to set
<jsInteropMode>JS</jsInteropMode>in the oldorg.codehaus.mojo:gwt-maven-pluginand explains that JsInterop had to be enabled explicitly in 2.7.0. The 2.8.0 (RC1) release notes list the removal of that legacy JsInterop, and the 2.8.0 (Beta1) notes say JsInterop had graduated from experimental, so the flag and the setting no longer exist. -
It creates the project with the SDK’s
webAppCreatorcommand and an oldpom.xmlfix-up, where this section uses themodular-webappMaven archetype. -
The library it uses,
com.vaadin.polymer:vaadin-gwt-polymer-elements, has its last Maven Central release (1.9.3.1) dated 2017-09-17; the GitHub repository has moved fromvaadintomanolo/gwt-polymer-elementsand its last push was 2018-10-23. It wraps Polymer 1.x elements. -
The Polymer project itself states in its README that LitElement is the successor of
PolymerElementand recommends considering it for new projects.
The tutorial still shows the technique that did not age: web components are DOM elements with properties, so they are native Java types, and a code generator (gwt-api-generator) can produce those bindings from the elements' metadata.
For a new application use the pattern shown in the section above, or Elemental2’s DOM types.
J2CL compatibility
J2CL is Google’s Java-to-JavaScript transpiler.
It is a separate product from GWT, and the J2CL README says the GWT 3 effort evolved into an SDK for libraries and enterprise tooling on top of it.
The point for a GWT 2.13 application is not to switch, but to write the boundary code (the annotations and the helper libraries on this page) so that it stays portable: GWT 2.9.0 made the compiler accept jsinterop-base, Elemental2 and jsinterop-annotations 2.0.0, which the release notes call compatibility across these tools with J2CL, except for @JsAsync and @JsEnum.
| Topic | GWT 2.13.1 (run in the lab) | J2CL (from its documentation) | What to do in shared code |
|---|---|---|---|
The annotations |
|
The same annotations from the same |
Use only these; they mean the same in both. |
Exports |
Nothing is exported unless |
J2CL’s cookbook says the generated Java classes are not directly callable by default because names are mangled, and that the annotations are the way to expose an API; its examples use |
Do not assume a global namespace in code that must also build with J2CL. |
|
Accepted, no effect. |
|
Avoid them in code that GWT 2.13 compiles. |
|
Honoured; primitive parameters rejected. |
Documented; the cookbook recommends also marking the parameter |
Use boxed types for optional parameters. |
|
|
|
Use |
Records |
Java 17 records compile (see JRE Emulation & Shared Code); their use with JsInterop was not tested here. |
The cookbook shows |
Test before you rely on records in exported types. |
Native JavaScript code |
JSNI ( |
No JSNI. |
Avoid JSNI and generators in shared code; wrap JavaScript with native types. |
Platform-specific code |
|
|
Keep browser-specific classes small and behind an interface. |
The sources of jsinterop-base show the dual-target discipline: Js.java contains // J2CL_ONLY comments before annotations that only J2CL needs, and the JSNI bodies of its helpers exist only for GWT.
The GWT roadmap says the project explores supporting some J2CL-exclusive JsInterop features in the GWT compiler, and the org.gwtproject.* J2CL-ready modules are still release candidates (1.0.0-RC1 and 1.0.0-RC2) on Maven Central.
J2CL & the Future treats those projects and the migration path.
Moving from JSNI to JsInterop
Code written with JSNI keeps compiling in GWT 2.13.1, but every pattern has a JsInterop counterpart, and the counterpart is usually shorter, typed and checked at compile time. JSNI & Overlay Types keeps the full side-by-side table; the essentials are:
| JSNI or overlay pattern | JsInterop replacement |
|---|---|
|
A native type |
A |
A native DTO named |
|
A native type with |
|
An exported |
A JavaScript callback stored as a |
A |
|
|
Pitfalls
The compiler messages below were printed by the 2.13.1 compiler for a probe module with one deliberately broken class per row.
They appear one per line under the heading Errors in com/.../File.java; when the compiler also prints Ignored 1 unit with compilation errors in first pass. followed by Compile with -strict or with -logLevel set to TRACE or DEBUG to see all errors., the other errors of that run were hidden until the first one was fixed (see Compiler & Compilation for -failOnError).
| Symptom (messages quoted as printed) | Cause | Fix |
|---|---|---|
|
The module was compiled without |
Add the flag to |
|
An |
Fix the regular expression; remember it is matched against qualified member names. |
|
A public member of a |
|
|
JavaScript has no overloading; two public methods of an exported type have the same name. |
|
|
A |
Mark one |
|
Two exported types have the same |
Rename one. |
|
A native type has a Java method body. |
Make the method |
|
A native constructor assigns fields. |
Keep it empty; set properties after |
|
A field of a native type has an initial value or is |
Declare plain, uninitialised fields. |
|
|
Make it |
|
A native class extends a Java class. |
Extend another native type, or none. |
|
JSNI inside a native type. |
Use |
|
A native interface has no runtime marker. |
Test with a native class, or check a property with |
|
|
Rename it, or give |
|
The getter and the setter of one property have different types. |
Use the same type for both. |
|
|
Box the type; keep optional parameters last. |
|
|
Namespaces are for static members; drop it. |
|
|
Move it to a top-level or static nested class. |
|
|
Delete the member instead. |
|
A class that implements a |
Implement |
|
The interface has two abstract methods. The message comes from the Java front end and ends with a help URL that is a broken link. |
Keep exactly one abstract method. |
|
The script that defines the global was not loaded before the module ran |
Load the script in the host page before the nocache script, or inject it and wait for the callback. |
|
A JavaScript array passed where Java expects a |
Use an array in the exported signature. |
|
The |
Validate in Java, or enable the property in development. |
|
Java fields are not stable JavaScript properties. |
Use native DTOs for data, and |
The chart keeps listeners after the widget is removed |
A native library object was created in |
Call |
The Bookshelf, end to end
In the section’s running scenario, JsInterop appears in three places, each one a pattern from this page.
-
Number formatting (native type).
NumberFormatJsformats the number of copies in the reader’s locale (see Internationalization); the exportedbookshelf.Api.formatCopiesmakes the same function reachable from the host page. -
Opening a book from outside GWT (exported type).
bookshelf.Api.open(id)calls thePlaceController(see MVP, Activities & Places), so a legacy script, a link on another page of the site or a test can navigate the application without reaching into its internals. Theinstall(ClientFactory)hook is Java-only (@JsIgnore). -
The admin chart (native type, callback, widget).
LoanChartwraps the chart library, callsdestroy()when detached, and routesonClickback into the presenter.
The pieces fit the earlier pages: the presenters stay in plain Java and know nothing about JavaScript, the native types are in one interop package, and the host page decides which scripts load.
Security rules from the other pages still apply across the boundary. A value that came from JavaScript is untrusted input like any other: bookshelf.Api.open(id) must check that the id is a valid number (an int parameter would not, as the table above shows), the server must authorise every operation behind it, and text from a script that is shown on the page goes through setText or SafeHtml (see SafeHtml & XSS), never setHTML with concatenated input.
What changed since the older material
-
JSNI chapter. The 2007 book (GWT 1.3 and 1.4) treats JavaScript interop as JSNI: handwritten JavaScript in
/*-{ ... }-*/comments,$wndand$doc,JavaScriptObjectwrappers and hand-built exports, including loading and wrapping third-party libraries. That material still compiles, but JsInterop (stable since 2.8.0, October 2016) replaced it, and the whole JsInterop vocabulary on this page postdates the book by years. -
Experimental flag. The
-XjsInteropModeflag of the 2.7 line and the<jsInteropMode>plugin setting no longer exist. -
Annotations jar. The annotations moved to their own artifact
com.google.jsinterop:jsinterop-annotations(2.8.0 notes); GWT 2.9.0 aligned withjsinterop-base1.0.0, Elemental2 1.0.0 andjsinterop-annotations2.0.0. -
Export filters.
-includeJsInteropExportsand-excludeJsInteropExportscame in 2.8.1; their notes describe wildcards, and the 2.13.1 implementation uses regular expressions. -
Overlay on default methods. Since 2.8.0
@JsOverlaymay be used on Java 8defaultmethods (asjsinterop-basedoes), and@JsOptionalwas added in the same line. -
Stale links. The Developer Guide’s annotation Javadoc link and the
DevGuideJsInterop.htmlname used elsewhere both return 404; the working page isDevGuideCodingBasicsJsInterop.html.
Source: GWT — Release Notes