Call GWT RPC services from a plain JVM application — without GWT's client compiler, without JSNI, and without touching your existing GWT services.
The library implements the GWT RPC wire protocol on top of java.net.http.HttpClient and exposes service interfaces through dynamic proxies, so any class annotated with @RemoteServiceRelativePath on the server can be invoked from a standard Java (or Android) client. An optional annotation processor generates both the Async interface and a reactive adapter that returns SmallRye Mutiny Unis.
This project started as a fork of GWT-SyncProxy and has been modernized: JDK 17 baseline, the modern HttpClient, JUnit 5 + Arquillian test stack, Maven Central publishing, and reactive adapters.
reactive-gwt-proxy— the runtime libraryreactive-gwt-processor— an annotation processor that scans GWT interfaces (@RemoteServiceRelativePath) and generates the GWTXxxAsynccompanion interface and theXxxMutinyreactive adapterexamples/— integration examples against GWT 2.0.3, 2.7.0 and 2.8.2, plus a Quarkus-based modern client that wraps a GWT RPC backend and re-exposes it as a JAX-RS REST endpoint.
The artifacts are published to Maven Central.
Add the following dependencies to your pom.xml:
<dependency>
<groupId>io.github.antoniomacri</groupId>
<artifactId>reactive-gwt-processor</artifactId>
<version>1.7.6</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.github.antoniomacri</groupId>
<artifactId>reactive-gwt-proxy</artifactId>
<version>1.7.6</version>
</dependency>Suppose you have an existing GWT RPC service shared between client and server:
@RemoteServiceRelativePath("orders")
public interface OrderService extends RemoteService {
OrderItem echo(OrderItem item);
int putList(List<OrderItem> list);
}Copy this interface from the sources of the legacy GWT application to the sources of your new Java backend or Android app.
You have two choices on how to use the library: directly following the Async style or via Mutiny.
From the OrderService interface (copied on your new modern backend), the annotation processor generates an OrderServiceAsync companion (the GWT-style callback-based interface):
@Generated("com.github.antoniomacri.reactivegwt.processor.ReactiveGwtProcessor")
public interface OrderServiceAsync {
Request echo(OrderItem item, AsyncCallback<OrderItem> callback);
Request putList(List<OrderItem> list, AsyncCallback<Integer> callback);
}You can now use ReactiveGWT.create to make the library generate the client proxy for the Async interface:
public class MyService {
String moduleBaseUrl = "http://localhost:8080/AppModule/";
OrderServiceAsync service = ReactiveGWT.create(OrderService.class, moduleBaseUrl);
public void addOrder() {
OrderItem item = new OrderItem();
service.echo(item, new AsyncCallback<OrderItem>() {
@Override
public void onSuccess(OrderItem result) { /* ... */ }
@Override
public void onFailure(Throwable caught) { /* ... */ }
});
}
}You need to specify also the URL of the backend application which exposes services via GWT-RPC (moduleBaseUrl).
The service variable holds an implementation of the Async interface which implements all the logic for communicating to the backend server via GWT-RPC.
When Mutiny is on the classpath, the annotation processor generates an OrderServiceMutiny adapter, alongside the async interface:
@Generated("com.github.antoniomacri.reactivegwt.processor.ReactiveGwtProcessor")
public class OrderServiceMutiny {
private final OrderServiceAsync async$;
public OrderServiceMutiny(OrderServiceAsync async) {
this.async$ = async;
}
private <T> AsyncCallback<T> createCallback(UniEmitter<? super T> em) {
return new AsyncCallback<T>() {
@Override public void onFailure(Throwable e) { em.fail(e); }
@Override public void onSuccess(T result) { em.complete(result); }
};
}
public Uni<OrderItem> echo(final OrderItem item) {
return Uni.createFrom().emitter(em -> async$.echo(item, createCallback(em)));
}
public Uni<Integer> putList(final List<OrderItem> list) {
return Uni.createFrom().emitter(em -> async$.putList(list, createCallback(em)));
}
}First, use ReactiveGWT.create to make the library generate the client proxy for the Async interface, and then instantiate the generated Mutiny service:
OrderServiceAsync async = ReactiveGWT.create(OrderService.class, moduleBaseUrl);
OrderServiceMutiny service = new OrderServiceMutiny(async);
Uni<OrderItem> result = service.echo(item);This composes naturally with reactive frameworks such as Quarkus. A typical setup uses a CDI producer to build the proxy and the Mutiny adapter once, and a JAX-RS resource to re-expose them as REST endpoints:
@ApplicationScoped
public class OrderServiceProducer {
@Produces
public OrderServiceAsync produce() {
String moduleBaseURL = "http://localhost:8090/AppModule/";
ProxySettings proxySettings = new ProxySettings(moduleBaseURL, OrderService.class.getName());
return ReactiveGWT.create(OrderService.class, proxySettings);
}
@Produces
public OrderServiceMutiny produce(OrderServiceAsync serviceAsync) {
return new OrderServiceMutiny(serviceAsync);
}
}@ApplicationScoped
@Path("orders")
public class OrderServiceRest {
@Inject
OrderServiceMutiny orderServiceMutiny;
@POST
@Path("echo")
public Uni<OrderItem> echo(OrderItem orderItem) {
return orderServiceMutiny.echo(orderItem);
}
@POST
@Path("putList")
public Uni<Integer> putList(List<OrderItem> list) {
return orderServiceMutiny.putList(list);
}
}Use ProxySettings for finer control (custom headers, cookies, executor, OAuth tokens, RPC stream version, etc.):
ProxySettings settings = new ProxySettings(moduleBaseUrl, OrderService.class.getName())
.setSerializationStreamVersion(5) // match older servers (e.g. GWT 2.0/2.7)
.setExecutor(myExecutor);
OrderServiceAsync service = ReactiveGWT.create(OrderService.class, settings);Apache License 2.0 — see License.md.
Check the token is currently valid or issue a new token. Login to https://central.sonatype.com then go to https://central.sonatype.com/usertoken and generate a new token.
Paste the token in the settings.xml as
<server>
<id>central</id>
<username>...</username>
<password>...</password>
</server>Make sure a variable pointing to a JDK 1.8 is defined in settings.xml, eg:
<profile>
<id>common</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<jdk8>/Library/Java/JavaVirtualMachines/jdk-1.8.jdk/Contents/Home</jdk8>
</properties>
</profile>Finally, run the release plugin:
./mvnw -Darguments='-Dmaven.test.skip=true' release:prepare
./mvnw release:perform