J2CL & the Future
|
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 (released 2026-06-19), gwt-maven-plugin 1.3.0 (net.ltgt.gwt.maven), JDK 21 running the compiler with -sourceLevel 17, the J2CL repository release v20260402 (2026-04-02) and j2cl-maven-plugin 0.22.0 (2023-11-13), checked on 2026-10-09.
It concerns shared code (Java that is compiled by javac, by the GWT compiler and by J2CL), client code (the part that each of the two compilers turns into JavaScript) and build/tooling (Maven, Bazel, the module XML).
Every build, error message, size and browser result in the worked example was produced by running the Bookshelf scenario on this page: one shared module, one application built by gwt-maven-plugin 1.3.0 with GWT 2.13.1 and one built by j2cl-maven-plugin 0.22.0, both loaded in headless Chrome.
Statements about projects that were not run (the J2CL Bazel workflow, J2Wasm, the watch goal) come from the project documentation and say so.
GWT 2.13.1 is the toolkit you can ship with today.
J2CL is a second, different Java-to-JavaScript compiler from Google, and the org.gwtproject libraries are the part of GWT that was rewritten to run on both.
This page explains how the three relate, what is and is not available, and how to keep the code you write today portable.
Two compilers, one language
The GWT compiler and J2CL solve the same problem, running Java in the browser, with different designs.
+ module XML"] --> gjdt["JDT front end
whole program"] gjdt --> gdb["Deferred binding
generators, permutations"] gdb --> gopt["GWT optimiser
per permutation"] gopt --> gjs["permutation.cache.js
+ nocache.js"] end subgraph J["J2CL"] direction LR jsrc["Java sources
per library"] --> jtr["J2CL transpiler
one library at a time"] jtr --> jclos["Closure-style JavaScript
cached per library"] jclos --> jcc["Closure Compiler
whole application"] jcc --> jjs["optimised bundle"] end
J2CL’s own README describes it as a transpiler from Java to Closure style JavaScript, tightly integrated with the Closure Compiler and built with Bazel. It is the technology behind several large Google applications (the README names Gmail, Inbox, Docs, Slides and Calendar) and says it is used in production inside Google. The same README also says that the workflow for open-source users is not finalised, that Windows is not supported outside WSL, and in its disclaimer section that the project is an alpha developer preview and not an official Google product. Its Wasm backend is described as working but still work in progress.
| Aspect | GWT 2.13.1 (this section) | J2CL |
|---|---|---|
What it is |
A Java-to-JavaScript compiler plus a runtime library, widget set, UiBinder, RPC and tooling, all in one SDK |
A transpiler from Java to Closure-style JavaScript; the optimisation is done by the Closure Compiler; no widget set and no RPC |
Unit of compilation |
The whole application, described by a module XML file and compiled in one run into one or more permutations |
One library at a time (the Maven plugin hashes sources and dependencies and caches each result), then one Closure Compiler run for the application |
Entry point |
|
None. Plain Java classes plus JavaScript that calls them (the sample archetype uses a |
Deferred binding, generators, linkers |
|
No equivalent in the J2CL documentation. Code generation is done with annotation processors (the README names Dagger and AutoValue among the libraries it supports) |
JSNI |
Supported (legacy, see JSNI & Overlay Types) |
The J2CL documentation uses JsInterop and |
JsInterop |
Annotations from |
Same annotations, J2CL semantics; exports use |
JRE emulation |
|
Its own |
Java language level |
Up to Java 17 syntax with |
Whatever the J2CL build in use supports. |
Build tool |
Maven ( |
Bazel (Google’s |
UI |
Widgets, panels, UiBinder, cell widgets, Editors, ClientBundle, i18n, RPC and RequestFactory |
None. Bring Elemental2 and a UI library (the repository has samples such as |
Status checked 2026-10-09 |
Stable, latest 2.13.1 (2026-06-19) |
Repository active (last push the day this page was checked), releases |
The practical consequence is that the two compilers do not replace one another feature by feature.
A GWT application that uses widgets, UiBinder, GWT-RPC, ClientBundle, i18n Messages and GWT.create cannot be handed to J2CL; the code that CAN be moved is the plain Java underneath it (model, validation, state machines, event bus, parsing) and the libraries that were rewritten to be independent of gwt-user.
Where J2CL differs from GWT in the language
The J2CL documentation lists the places where Java semantics are deliberately different. They matter when shared code runs on both compilers, because the same method can behave differently.
| Topic | J2CL behaviour (from its documentation) |
|---|---|
Reflection |
|
Array access |
No bounds check on array access |
|
Emulated with a JavaScript |
|
Mapped to |
Boxed |
Represented by the JavaScript value itself (unboxed), while |
|
Not emulated, because it needs large localisation code; the J2CL documentation suggests Guava |
|
Intentionally left out of the emulation |
Switch on an enum |
Does not trigger class initialisation, unlike |
JsInterop namespaces |
A non-native |
Source: J2CL — Limitations, J2CL — Semantics
GWT 2.13.1 has its own list of differences (long as a composite of doubles, String.split with Java 8 semantics, configurable jre.checks.*), documented in JRE Emulation & Shared Code.
Code that is correct on a JVM, correct in GWT and correct in J2CL avoids all of these corners: no reflection, no String.format, no reliance on float rounding or array-bounds exceptions.
What "GWT 3" means today
"GWT 3" was the name given to a planned next major version built on J2CL. Every public source checked for this page agrees on one fact: that release does not exist.
| Source | Statement |
|---|---|
Maven Central, |
Versions published: 2.10.0, 2.10.1, 2.11.0, 2.12.0, 2.12.1, 2.12.2, 2.13.0, 2.13.1. No 3.x |
GitHub releases of |
Latest 2.13.1 (2026-06-19); milestones 2.14 (open, 23 open and 29 closed issues) and 2.15 (empty) |
J2CL README, "J2CL vs. GWT?" |
The GWT team decided in early 2015 to build a separate product from scratch; the strategy later turned GWT 3 into an SDK focused on libraries and enterprise tooling rather than a compiler |
GWT roadmap page |
Names Java 21 language and JRE emulation as the goal of the next releases, and says GWT explores supporting some J2CL-exclusive JsInterop features in its own compiler. It does not announce a GWT 3 |
|
Describe each module as a future-proof port of a |
|
Calls a J2CL-built application a GWT 3 project, and its cache parameter is named |
Today "GWT 3" therefore means a direction, not a product:
-
GWT 2.x remains the SDK. Releases 2.10 to 2.13.1 (2022 to 2026) added Java 11 to 17 syntax, Jakarta artifacts,
org.gwtprojectcoordinates and many JRE additions. The roadmap commits to Java 21 parity. -
J2CL is a separate compiler that Google maintains for its own applications, usable by outsiders through Bazel or the community Maven plugin.
-
The
org.gwtprojectlibraries are the shared part: they compile with GWT 2.9 or later and with J2CL, and are what a "GWT 3" would be assembled from. -
Shared compatibility work in GWT itself. The 2.9.0 release notes say GWT can compile projects that use
jsinterop-base1.0.0, Elemental2 1.0.0 andjsinterop-annotations2.0.0 (everything except@JsAsyncand@JsEnum), and the 2.10.0 notes mention JRE emulation changes that keep the GWT and J2CL code bases consistent for J2CL’s Wasm work.
An application started today should be written as GWT 2.13.1 code, with the J2CL-friendly rules from the later section applied to whatever is cheap to apply, and without any plan that depends on a "GWT 3" release date.
The org.gwtproject modules
Since the GWT 2.10.0 release the Maven groupId of the SDK is org.gwtproject.
A second family of artifacts lives under the same groupId prefix: libraries in the form org.gwtproject.<area>:gwt-<area>, each in its own repository in the gwtproject GitHub organisation.
The READMEs I read (core, event, dom, editor, safehtml, http, json, timer, xhr, regexp, aria, layout, animation) describe each module as a port of a com.google.gwt module with no dependency on gwt-user apart from the Java runtime emulation, aimed at GWT 3 and J2CL; the gwt-core README requires GWT 2.9.0 or newer.
gwt-core goes further: its repository contains separate GWT 2 and J2CL test modules and its README says mvn clean verify runs the tests on the JVM, with J2CL and with GWT 2.
All of them are still release candidates. The table lists the artifacts that were on Maven Central on 2026-10-09 (module XML names read from the jars).
| Artifact | Latest (Central date) | GWT module to inherit |
|---|---|---|
|
1.0.0-RC2 (2023-11-04) |
|
|
1.0.0-RC1 (2020-05-22) |
|
|
1.0.0-RC1 (2020-05-22) |
|
|
1.0.0-RC1 (2020-05-22) |
|
|
1.0.0-RC2 (2022-04-16) |
|
|
1.0.0-RC1 (2020-05-22) |
|
|
1.0.0-RC2 (2022-04-14) |
|
|
1.0.0-RC1 (2020-10-05) |
|
|
1.0.0-RC1 (2020-10-26) |
|
|
1.0.0-RC1 (2020-10-26) |
|
|
1.0.0-RC1 (2020-10-05) |
|
|
1.0.0-RC2 (2022-04-16) |
|
|
1.0.0-RC2 (2022-04-16) |
|
|
1.0.0-RC1 (2020-10-05) |
|
|
1.0.0-RC1 (2020-10-26) |
|
|
1.0.0-RC3 (2022-02-28) |
|
|
1.0.0-RC1 (2020-12-03) |
|
|
1.0.0-RC1 (2020-12-03) |
|
|
1.0.0-RC1 (2020-05-22) |
|
|
1.0.0-RC1 (2020-10-05) |
|
|
1.0.0-RC1 (2020-10-05) |
|
|
1.0.0-RC2 (2020-10-24) |
|
|
1.0.0-RC1 (2020-05-22) |
|
|
1.0.0-RC3 (2022-11-19) |
|
|
1.0.0-RC1 (2020-10-26) |
|
|
1.0.0-RC1 (2020-10-05) |
|
Three facts shape how much to rely on this family:
-
No stable release. There is no
1.0.0for any of them, and the newest artifact on Central isgwt-core1.0.0-RC2 from November 2023. The newest commit in any of the module repositories at the time of the check was 2024-12-17 (gwt-editor);gwt-editorhas commits after its last Central release. -
The big UI libraries were not ported. The Central listing has no artifact for widgets, panels, UiBinder, cell widgets,
ClientBundle, i18n, GWT-RPC or validation. The onlyorg.gwtproject.webartifacts are the RequestFactory ones (org.gwtproject.web.bindery:requestfactory*), which are part of the 2.13.1 SDK, not J2CL-ready ports. -
Mixing is allowed. The module XML names do not collide with the
com.google.gwtones, andgwt-event-compatis a bridge: itsCompatEventBuswraps a new-style event bus as acom.google.gwt.event.shared.EventBus, so a code base can move to the new event classes while libraries still use the old ones.
The libraries bring Elemental2 with them: the gwt-core 1.0.0-RC2 POM declares elemental2-core and elemental2-dom 1.1.0, and gwt-dom additionally depends on gwt-safehtml, gwt-safecss, gwt-regexp and gwt-dom-style-definitions.
An application that wants a newer Elemental2 declares it itself; Maven’s nearest-wins rule then picks the application’s version.
Using a ported module in a GWT 2.13.1 project
The migration recipe in the README of every module is the same three steps, shown here for gwt-event (it is the one used in the worked example):
<!-- pom.xml -->
<dependency>
<groupId>org.gwtproject.event</groupId>
<artifactId>gwt-event</artifactId>
<version>1.0.0-RC1</version>
</dependency>
<!-- module .gwt.xml: replaces com.google.web.bindery.event.Event -->
<inherits name="org.gwtproject.event.Event"/>
import org.gwtproject.event.shared.Event;
import org.gwtproject.event.shared.EventBus;
import org.gwtproject.event.shared.HandlerRegistration;
import org.gwtproject.event.shared.SimpleEventBus;
Events & Handlers covers the com.google.gwt.event handler model itself, which is what most widget code uses.
Because the widget set still depends on that model, the ported event module is most useful for non-UI code: models, services and presenters that fire domain events.
Likewise MVP, Activities & Places and Editors use the gwt-user versions; gwt-places and gwt-editor exist as the J2CL-ready alternatives.
Writing code that works with both
The rules below are the ones that kept the Bookshelf core module compiling with javac, GWT 2.13.1 and J2CL.
| Rule | GWT 2.13.1 | J2CL |
|---|---|---|
Keep shared code free of |
Allowed, but then the module is GWT-only |
|
No JSNI |
Legacy JSNI works |
Not supported; use JsInterop plus |
No |
Deferred binding works |
No equivalent; use constructors, factories, annotation processors |
Use JsInterop and Elemental2 for the browser API |
Elemental2 1.3.2 is the latest stable and the one the other pages of this section use |
The same libraries, but in the lab the Maven plugin needed Elemental2 1.1.0 (see Pitfalls) |
Strip server-only code with |
Honoured by name, whatever package it is in |
Honoured by name too; the Maven plugin replaces the stripped members with whitespace |
Platform differences through replacement classes |
|
The J2CL documentation recommends a replacement file in a |
Avoid reflection, |
Not emulated either ( |
Not emulated ( |
No dependence on |
Each compiler has its own differences |
Documented differences listed above |
Use Java 8 level syntax unless both build tools are verified for more |
|
|
Source: J2CL — Best practices, GWT Javadoc — GwtIncompatible, GWT — Organizing Projects: Module XML files
The J2CL best-practices page also warns against using @GwtIncompatible to switch application logic on and off, because the divergence then spreads through the code.
Its advice is to hide the incompatible part behind an interface and supply a different implementation per platform.
The GWT-side detail of @GwtIncompatible and super-source is explained in JRE Emulation & Shared Code, and the JsInterop annotations used for the browser edge in JsInterop and DOM & Elemental2.
Worked example: one core module, two compilers
The Bookshelf scenario gets a small module that both compilers build: an ISBN-13 checker, a catalog and a BookAddedEvent that uses the ported gwt-event module.
Three Maven modules make the dual build:
imports org.gwtproject:gwt 2.13.1 BOM"] core["bookshelf-core (jar + sources)
Isbn, Catalog, BookAddedEvent
gwt-event 1.0.0-RC1, JUnit test"] gwtapp["bookshelf-gwt-app (gwt-app)
gwt-maven-plugin 1.3.0
GwtApp entry point"] j2clapp["bookshelf-j2cl-app (war)
j2cl-maven-plugin 0.22.0
J2clApp + native.js"] parent --> core parent --> gwtapp parent --> j2clapp core --> gwtapp core --> j2clapp gwtapp --> out1["gwtapp.nocache.js
2 permutations"] j2clapp --> out2["j2clapp.js
1 optimised file"]
The shared module
The core module has no dependency on gwt-user.
Its only dependencies are the ported event module, the JsInterop annotations (they ship with GWT and J2CL alike) and JUnit for the JVM test.
<dependencies>
<dependency>
<groupId>org.gwtproject.event</groupId>
<artifactId>gwt-event</artifactId>
<version>1.0.0-RC1</version>
</dependency>
<dependency>
<groupId>com.google.jsinterop</groupId>
<artifactId>jsinterop-annotations</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.4.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals><goal>jar-no-fork</goal></goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
The sources jar matters: both compilers read Java source, not bytecode, from the dependencies. The GWT module file sits next to the classes and names the packages that are client code:
<?xml version="1.0" encoding="UTF-8"?>
<module>
<inherits name="org.gwtproject.event.Event"/>
<inherits name="com.google.gwt.core.Core"/>
<source path="shared"/>
</module>
J2CL ignores the file; it only needs the sources jar.
package com.example.bookshelf.core.shared;
import java.io.File;
import jsinterop.annotations.JsMethod;
import jsinterop.annotations.JsType;
@JsType(namespace = "bookshelf", name = "Isbn")
public final class Isbn {
private Isbn() {}
@JsMethod
public static String normalize(String raw) {
return raw.replace("-", "").replace(" ", "");
}
@JsMethod
public static boolean isValid(String raw) {
String digits = normalize(raw);
if (digits.length() != 13) {
return false;
}
int sum = 0;
for (int i = 0; i < 13; i++) {
char c = digits.charAt(i);
if (c < '0' || c > '9') {
return false;
}
sum += (c - '0') * (i % 2 == 0 ? 1 : 3);
}
return sum % 10 == 0;
}
/** Server-only: java.io.File is not part of either emulation. */
@GwtIncompatible("java.io.File is not emulated")
public static String coverFileName(String raw) {
return new File("covers", normalize(raw) + ".jpg").getPath();
}
}
GwtIncompatible is a six-line annotation declared in the same package (@Retention(CLASS), targets type, method, field and constructor, one String value()): both tools look at its simple name only, so there is no dependency on GWT to import.
public class BookAddedEvent extends Event<BookAddedEvent.Handler> {
public interface Handler {
void onBookAdded(BookAddedEvent event);
}
public static final Type<Handler> TYPE = new Type<>();
private final String title;
public BookAddedEvent(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
@Override
public Type<Handler> getAssociatedType() {
return TYPE;
}
@Override
protected void dispatch(Handler handler) {
handler.onBookAdded(this);
}
}
public class Catalog {
private final EventBus bus = new SimpleEventBus();
private final List<String> titles = new ArrayList<>();
public EventBus events() {
return bus;
}
public boolean add(String title, String isbn) {
if (!Isbn.isValid(isbn)) {
return false;
}
titles.add(title);
bus.fireEvent(new BookAddedEvent(title));
return true;
}
public int size() {
return titles.size();
}
}
The plain JVM test needs no browser and no compiler:
@Test
public void coverFileNameIsServerOnly() {
assertEquals("covers/9780441172719.jpg", Isbn.coverFileName("978-0-441-17271-9"));
}
@Test
public void addsOnlyValidIsbns() {
Catalog catalog = new Catalog();
final String[] seen = new String[1];
catalog.events().addHandler(BookAddedEvent.TYPE, e -> seen[0] = e.getTitle());
assertTrue(catalog.add("Dune", "978-0-441-17271-9"));
assertFalse(catalog.add("Broken", "978-0-441-17271-0"));
assertEquals(1, catalog.size());
assertEquals("Dune", seen[0]);
}
The GWT application
<packaging>gwt-app</packaging>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>bookshelf-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>bookshelf-core</artifactId>
<version>${project.version}</version>
<classifier>sources</classifier>
</dependency>
<dependency>
<groupId>org.gwtproject</groupId>
<artifactId>gwt-user</artifactId>
</dependency>
<dependency>
<groupId>org.gwtproject</groupId>
<artifactId>gwt-dev</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>net.ltgt.gwt.maven</groupId>
<artifactId>gwt-maven-plugin</artifactId>
<version>1.3.0</version>
<extensions>true</extensions>
<configuration>
<moduleName>com.example.bookshelf.gwtapp.GwtApp</moduleName>
<moduleShortName>gwtapp</moduleShortName>
<sourceLevel>17</sourceLevel>
<failOnError>true</failOnError>
<compilerArgs>
<arg>-generateJsInteropExports</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
<module rename-to="gwtapp">
<inherits name="com.google.gwt.user.User"/>
<inherits name="com.example.bookshelf.core.Core"/>
<entry-point class="com.example.bookshelf.gwtapp.GwtApp"/>
<source path=""/>
</module>
public class GwtApp implements EntryPoint {
@Override
public void onModuleLoad() {
Catalog catalog = new Catalog();
StringBuilder out = new StringBuilder();
catalog.events().addHandler(BookAddedEvent.TYPE,
e -> out.append("event=").append(e.getTitle()).append(' '));
boolean ok = catalog.add("Dune", "978-0-441-17271-9");
boolean bad = catalog.add("Broken", "978-0-441-17271-0");
out.append("added=").append(ok).append(" rejected=").append(!bad)
.append(" size=").append(catalog.size())
.append(" normalized=").append(Isbn.normalize("978-0-441-17271-9"));
Document.get().getBody().setInnerText("gwt: " + out);
}
}
The page is plain HTML with <script src="gwtapp/gwtapp.nocache.js">; setInnerText is used because the string is application data, never setInnerHTML.
The J2CL application
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>bookshelf-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.elemental2</groupId>
<artifactId>elemental2-dom</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.vertispan.j2cl</groupId>
<artifactId>j2cl-maven-plugin</artifactId>
<version>0.22.0</version>
<executions>
<execution>
<goals><goal>build</goal></goals>
</execution>
</executions>
<configuration>
<compilationLevel>ADVANCED</compilationLevel>
<webappDirectory>${project.build.directory}/${project.build.finalName}/j2clapp</webappDirectory>
<initialScriptFilename>j2clapp.js</initialScriptFilename>
</configuration>
</plugin>
</plugins>
</build>
@JsType
public class J2clApp {
public void run() {
Catalog catalog = new Catalog();
StringBuilder out = new StringBuilder();
catalog.events().addHandler(BookAddedEvent.TYPE,
e -> out.append("event=").append(e.getTitle()).append(' '));
boolean ok = catalog.add("Dune", "978-0-441-17271-9");
boolean bad = catalog.add("Broken", "978-0-441-17271-0");
out.append("added=").append(ok).append(" rejected=").append(!bad)
.append(" size=").append(catalog.size())
.append(" normalized=").append(Isbn.normalize("978-0-441-17271-9"));
DomGlobal.document.body.textContent = "j2cl: " + out;
}
}
// Runs after the page has loaded the bundle; J2CL has no EntryPoint interface.
setTimeout(function () {
new J2clApp().run();
}, 0);
The .native.js pattern is the one the j2cl-archetype-simple archetype generates.
Only the browser edge (Document.get().getBody() in GWT, DomGlobal.document.body in Elemental2) differs between the two applications; every line of model and logic comes from the shared jar.
Build and run
mvn -B package
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
Compiling 2 permutations
Compilation succeeded -- 3,498s
bookshelf-core ..................................... SUCCESS [ 0.601 s]
bookshelf-gwt-app .................................. SUCCESS [ 4.737 s]
bookshelf-j2cl-app ................................. SUCCESS [ 20.996 s]
BUILD SUCCESS
Both outputs were served from localhost and loaded in headless Chrome; the text each page wrote into its body is the DOM that Chrome reported after the scripts ran:
<body>gwt: event=Dune added=true rejected=true size=1 normalized=9780441172719</body>
<body>j2cl: event=Dune added=true rejected=true size=1 normalized=9780441172719
The behaviour of the shared code is identical, including the ported event bus, the StringBuilder and the Isbn checks.
| File | Bytes | Gzipped |
|---|---|---|
GWT |
6,658 |
2,930 |
GWT one |
47,905 and 47,907 |
14,750 and 14,752 |
J2CL |
20,955 |
6,721 |
These numbers describe this 30-line application only: the GWT figure includes the runtime and the user agent bootstrap, and the J2CL figure includes Elemental2’s DomGlobal and the JRE parts that the code reaches.
They show that both pipelines produce a small file for a small program and say nothing about the size of a real application.
The JsInterop export behaves differently, as the J2CL semantics page predicts.
A probe page that calls bookshelf.Isbn.isValid(...) after loading each bundle returned [true,false,"9780441172719"] for the GWT build (compiled with -generateJsInteropExports) and failed with Uncaught ReferenceError: bookshelf is not defined for the J2CL ADVANCED build, which is consistent with the J2CL statement that exports go through goog.export.
Code that must be callable from outside JavaScript needs an explicit export on the J2CL side.
What happens without the annotation
Removing @GwtIncompatible from coverFileName made both builds fail, with the messages each tool produces:
Errors in 'jar:file:core/target/bookshelf-core-1.0-SNAPSHOT-sources.jar!/com/example/bookshelf/core/shared/Isbn.java'
Line 40: No source code is available for type java.io.File; did you forget to inherit a required module?
Aborting compile due to errors in some input files
bookshelf-core:1.0-SNAPSHOT/transpiled_js: Error:Isbn.java:6: The import java.io.File cannot be resolved
bookshelf-core:1.0-SNAPSHOT/transpiled_js: Error:Isbn.java:40: File cannot be resolved to a type
Source: GWT Javadoc — GwtIncompatible
With the annotation, both builds succeed. The J2CL preprocessor replaces the annotated member by blank lines, so line numbers in later messages still point at the original source.
The j2cl-maven-plugin
The Maven plugin is the way to build J2CL code without Bazel. It is a community project by Vertispan, not part of the GWT SDK or of Google’s J2CL repository.
| Item | Value |
|---|---|
Coordinates |
|
Latest release |
0.22.0 (Central, 2023-11-13); earlier 0.19, 0.20, 0.21.0 |
Repository |
https://github.com/Vertispan/j2clmavenplugin (not archived; last push 2024-10-14) |
J2CL build it bundles |
|
Archetypes |
|
Goals (from the plugin descriptor) |
|
Selected |
|
What the Readme documents (not all of it exercised here):
-
Per-library builds with a cache. Each dependency is hashed together with its dependencies and the plugin version, stripped of
@GwtIncompatiblemembers, run through annotation processors, transpiled and cached; only changed libraries are rebuilt. The cache lives intargetby default and can be moved to a shared directory with thegwt3.cache.dirproperty. -
mvn j2cl:cleanremoves cache entries,-Dartifact=<artifactId>limits it to one artifact and-Dartifact=*empties the cache directory. -
Dependency rewriting. By default the plugin replaces the
jsinterop-basecoordinates withcom.vertispan.jsinterop:base, which the build log confirmed (Removing dependency com.google.jsinterop:base:jar:1.0.1:compile, replacing with com.vertispan.jsinterop:base:jar:1.0.1-1). -
Compilation levels.
ADVANCED_OPTIMIZATIONSfor production (the sample archetype writesADVANCED, which worked),SIMPLE_OPTIMIZATIONS,WHITESPACE_ONLY,BUNDLE(concatenation only) andBUNDLE_JAR(one bundle per classpath entry, loaded in order). -
Dev mode. The
watchgoal recompiles the changed libraries of a whole reactor, so it is meant to be run from the parent project. It was not run for this page.
The test goal runs JUnit tests compiled to JavaScript (the archetype’s test runs under HtmlUnit); the Readme claims the plugin is the only way to build general J2CL projects outside Bazel.
The GWT counterpart of such tests is covered in Testing GWT Applications.
Gradle, Bazel and the rest
The community GWT Gradle plugins mentioned in Build Tools: Maven & Gradle build GWT 2 applications; none of them was checked for J2CL.
Google’s own J2CL workflow is Bazel (its getting-started page uses Bazelisk and bazel build/bazel run on the helloworld sample, and says the output of that sample is a single optimised line of JavaScript).
The Bazel path was not run for this page.
For a Java team that already builds with Maven, the plugin is the lower-friction route; the plugin bundles the older J2CL build v20230718-1, so features added since (the J2CL README mentions Wasm) are reached through Bazel; whether the plugin exposes them was not checked.
Moving a GWT code base toward J2CL
A realistic migration is incremental and always keeps the GWT build green:
-
Isolate the portable core. Move models, validation, formatting and domain events into a module that depends on no
gwt-usertype, asbookshelf-coredoes. Compile it withjavacand JUnit first. -
Replace JSNI with JsInterop. The pages JSNI & Overlay Types and JsInterop describe the mapping; JsInterop works in both compilers.
-
Replace
GWT.createand generators in the core with factories or annotation processors (see Deferred Binding & Generators). -
Swap
gwt-userclasses for ported ones where a module exists and is acceptable to depend on at release-candidate quality (gwt-event,gwt-core,gwt-dom,gwt-safehtml,gwt-editor), or for Elemental2. -
Add the second build (J2CL) in CI and keep it as a canary. Anything that breaks it shows which GWT assumption crept into the core.
-
Leave the UI and the transport alone. Widgets, UiBinder, cell widgets,
ClientBundle, i18n and GWT-RPC have no J2CL-ready counterpart inorg.gwtproject.
Mapping of the old coordinates and packages, with the other changes between GWT versions, is collected in What’s Changed & Migration, in particular the recipes for coordinates and packages, Jakarta, JSNI and Elemental. A catalogue of the libraries around GWT, including alternatives for the pieces that were not ported, is in Ecosystem & Libraries, and IDE support for GWT and J2CL projects in Tooling & IDE Support.
The roadmap
The GWT roadmap page is short and says the following (checked on 2026-10-09):
-
planning happens in GitHub milestones; the milestones are the place to look, because priorities change;
-
the focus of the next releases is Java language features and JRE emulation up to Java 21;
-
GWT explores supporting some J2CL-exclusive JsInterop features in its compiler;
-
the SDK is open source and looks for contributors and sponsors; donations go through Open Collective.
The open milestone 2.14 (updated 2026-09-19) contains items that show the direction. They are listed by issue number and title, as of the same date:
| Issue | Title |
|---|---|
9794 |
Explore |
10011 |
Explore replacing build/project setup and modularizing codebase |
10236 |
Remove j2cl |
10038 |
Enable |
10253 |
OptimizedOnlyCompilerSuite is never run, fails |
10323 |
Improve incremental tracking of method references and lambdas |
The @JsEnum item matches the 2.13.1 behaviour found in this section: the annotation compiles and is ignored.
Nothing in the milestones or the roadmap promises a date for any of this, and the 2.14 milestone has no due date.
Open Collective for the project: opencollective.com/gwt-project.
Outdated material in older books and posts
Books written for GWT 1.x and 2.0 to 2.5 predate J2CL, which Google began in 2015, and the org.gwtproject modules, so they cannot be used to decide anything about either.
Their chapters on events, DOM access, RPC and widgets describe the com.google.gwt packages, which remain valid for GWT 2.13.1; what changed is that the same code is no longer the only option for non-UI libraries.
Pitfalls
| Symptom (exact message) | Cause | Fix |
|---|---|---|
|
A |
Remove the line; the annotations compile in GWT 2.13.1 without it |
|
A JRE class neither emulation provides is used in shared code |
Mark the member |
|
The |
Delete |
|
|
Use |
|
Elemental2 1.3.2 declares a JSpecify dependency; the transpile step of 0.22.0 could not resolve the annotations although |
Same fix; or build with a newer J2CL than the plugin bundles (untested here) |
|
|
Keep shared code at Java 8 syntax while |
|
|
Export explicitly with |
|
The plain concatenated bundle (6.4 MB for this application) referenced |
Use |
|
|
Keep |
|
Generic wrapper; the real cause is an earlier |
Read the first error; delete |
What changed recently
-
GWT 2.9.0: compiles against
jsinterop-base1.0.0, Elemental2 1.0.0 andjsinterop-annotations2.0.0; Elemental (version 1) officially deprecated in favour of Elemental2. -
GWT 2.10.0:
org.gwtprojectbecomes the groupId; JRE emulation simplifications to keep the GWT and J2CL code bases consistent for J2CL’s Wasm backend. -
GWT 2.12 and 2.13: Java 17 language level and JRE additions; the roadmap now names Java 21 and J2CL-exclusive JsInterop features as the goals.
-
J2CL releases on GitHub:
v20250623,v20250630,v20260402. -
jsinterop-base1.2.0 (2026-06-30); Elemental2 1.3.2 stable, 1.4.0-RC1 published 2026-07-07. -
j2cl-maven-plugin: last release 0.22.0 on 2023-11-13; last repository push 2024-10-14. -
org.gwtprojectmodules: newest Central artifactgwt-core1.0.0-RC2 (2023-11-04); no stable 1.0.0.