gRPC and WildFly - Part II: Exposing Jakarta RESTFul Web Services to gRPC
By Ron Sigal | June 11, 2023
In Part I of this two part series (grpc and WildFly - Part I), we discussed WildFly’s grpc subsystem, which supports gRPC services. Here, we introduce the RESTEasy resteasy-grpc project, which allows gRPC clients to communicate with Jakarta RESTFul Web Services.
We assume here that the reader is familiar with WildFly, Jakarta REST, protobuf, and gRPC. There is a brief introduction to gRPC in Part I.
gRPC is a fairly open system. For example
- client and server code can be written in different programming languages;
- gRPC typically runs over the protobuf transport layer, but others can be substituted.
But it’s generally assumed that there’s a gRPC client on one end and a gRPC server on the other. Here, we go a step further and support communication between a gRPC client and a Jakarta REST server. A use case might be, for example, a desire to take advantage of protobuf’s network speed in communicating with an existing Jakarta REST application.
Building a bridge between gRPC and Jakarta REST
As we discussed in Part I, a gRPC application begins with a protobuf description of the supported datatypes and procedure calls. Now, however, we are assuming the prior existence of a Jakarta REST application with no such description. We call that the target project. To get started, then, we have to create a protobuf characterization of the API supported by the target project.
Generating a protobuf description
Using the JavaParser project, the grpc-bridge module in resteasy-grpc scans a directory of Java source files, checking annotations to discover Jakarta REST resource methods. From those it derives a set of message types, and from the types and methods it constructs a protobuf description.
Consider the grpc-bridge-example, a variation on the helloworld example we looked at in Part I. Given the resource method
@GET
@Path("greet/{s}")
@Produces(MediaType.APPLICATION_JSON)
public Greeting greet(@PathParam("s") String s) {
return new Greeting("hello, " + s);
}
we can deduce that we have to represent message types String
and Greeting
. A human looking at that and
the definition of Greeting
package dev.resteasy.example.grpc.greet;
public class Greeting {
private String s;
public Greeting(String s) {
this.s = s;
}
...
public String getS() {
return s;
}
public void setS(String s) {
this.s = s;
}
...
}
might devise a protobuf description something like
service Greeter {
rpc greet (String) returns (Greeting) {}
}
message Greeting {
string name = 1;
}
It turns out, though, that the semantic disparity between gRPC and Jakarta REST forces additional complexity on the description. Instead, we get something considerably more complicated:
option java_package = "example.grpc";
option java_outer_classname = "Greet_proto";
service GreetService {
// greet/{s} gEmpty dev_resteasy_example_grpc_greet___Greeting GET sync
rpc greet (GeneralEntityMessage) returns (GeneralReturnMessage);
}
message dev_resteasy_example_grpc_greet___Greeting {
string s = 1;
}
...
message gEmpty {}
message gInteger {int32 value = 1;}
...
message gHeader {
repeated string values = 1;
}
message gCookie {
string name = 1;
...
}
message gNewCookie {
string name = 1;
...
}
message ServletInfo {
string characterEncoding = 1;
...
}
message FormValues {
repeated string formValues_field = 1;
}
message FormMap {
map<string, FormValues> formMap_field = 1;
}
message GeneralEntityMessage {
ServletInfo servletInfo = 1;
string URL = 2;
map<string, gHeader> headers = 3;
repeated gCookie cookies = 4;
string httpMethod = 5;
oneof messageType {
gEmpty gEmpty_field = 6;
FormMap form_field = 7;
}
}
message GeneralReturnMessage {
map<string, gHeader> headers = 1;
repeated gNewCookie cookies = 2;
gInteger status = 3;
oneof messageType {
dev_resteasy_example_grpc_greet___Greeting dev_resteasy_example_grpc_greet___Greeting_field = 4;
}
}
Let’s unpack all this complexity.
-
One of the differences between gRPC and Jakarta REST concerns the arguments transmitted from the client to the server. While gRPC supports a single parameter of some message type, Jakarta REST supports, in addition to a single entity parameter, things like path parameters, query parameters, header parameters, form parameters, etc. That’s why we have the
GeneralEntityMessage
message type. Consider theoneof
field. In general, this field is meant to hold any entity message type that can be sent from the client. Oddly, there are no entries other than theFormMap
, which doesn’t happen to be relevant here, andgEmpty
. But go back to the definition ofgreet()
and note that there is no entity parameter. The single parameter is a path parameter, and it can be transmitted in theURL
field of theGeneralEntityMessage
. Similarly, the general purposeGeneralReturnMessage
has aoneof
field, and, in this case, the entry represents thedev_resteasy_example_grpc_greet___Greeting
type, which is the protobuf translation ofGreeting
. -
Another issue is the lack of a notion of packages in protobuf. We adopt the use of underscores to represent package names, e.g.,
dev_resteasy_example_grpc_greet___Greeting
. So, the packagedev.resteasy.example.grpc.greet
turns intodev_resteasy_example_grpc_greet
, and we use___
to separate the package from the class name. -
Without getting into the details, we’ll just mention that the comment
// greet/{s} gEmpty dev_resteasy_example_grpc_greet___Greeting GET sync
holds information that is passed on for subsequent processing.
Another significant semantic difference between gRPC and Jakarta REST is that protobuf data types, necessarily the least common denominator of various programming languages, have no notion of class inheritance, so we have to make use of what’s available to represent subclasses.
Suppose we introduce class GeneralGreeting
public class GeneralGreeting extends Greeting {
private String salute;
public GeneralGreeting(String salute, String s) {
super(s);
this.salute = salute;
}
...
public String getSalute() {
return salute;
}
public void setSalute(String salute) {
this.salute = salute;
}
...
}
and extend Greeter
:
@GET
@Path("salute/{s}")
@Produces(MediaType.APPLICATION_JSON)
public GeneralGreeting generalGreet(@QueryParam("salute") String salute, @PathParam("s") String s) {
return getGeneralGreeting(salute, s);
}
private GeneralGreeting getGeneralGreeting(String salute, String name) {
return new GeneralGreeting(salute, name);
}
Now, when we generate the protobuf file, we get another rpc call:
service GreetService {
...
// salute/{s} gEmpty dev_resteasy_example_grpc_greet___GeneralGreeting GET sync
rpc generalGreet (GeneralEntityMessage) returns (GeneralReturnMessage);
}
and a new message type:
message dev_resteasy_example_grpc_greet___GeneralGreeting {
string salute = 1;
dev_resteasy_example_grpc_greet___Greeting greeting___super = 2;
}
Here, the field called greeting___super
is meant to be interpreted as the superclass of
dev_resteasy_example_grpc_greet___GeneralGreeting
. That’s not enforced by protobuf, but
it will be treated as inheritance by the grpc-bridge code.
Note, by the way, that the method getGeneralGreeting()
does not lead to an rpc call because,
due to its lack of Jakarta REST annotations, it’s not a resource method.
Building an intermediate layer
Now that we have a protobuf description of message types and remote calls, we can apply the java version of the protobuf compiler together with its gRPC plugin to create the client and server side frameworks. There are two outputs.
The protobuf compiler itself generates a class like example.grpc.Greet_proto
, which has an inner class
for each 1. For example,
public static final class dev_resteasy_example_grpc_greet___Greeting extends
com.google.protobuf.GeneratedMessageV3 implements
dev_resteasy_example_grpc_greet___GreetingOrBuilder {
...
@java.lang.Override
public java.lang.String getS() {
...
}
...
}
dev_resteasy_example_grpc_greet___Greeting
has over 500 lines of code and isn’t really meant for human consumption.
What is interesting to note is the relationship between dev.resteasy.example.grpc.greet.Greeting
and
dev_resteasy_example_grpc_greet___Greeting
. The former is the original Java class, but the latter is
also a Java class. They live in different worlds, though. dev.resteasy.example.grpc.greet.Greeting
is
part of the Jakarta REST application. dev_resteasy_example_grpc_greet___Greeting
is a Java
expression of the dev_resteasy_example_grpc_greet___Greeting
protobuf message type. To disambiguate
the two Java classes, we refer to to dev_resteasy_example_grpc_greet___Greeting
as the
javabuf class corresponding to dev.resteasy.example.grpc.greet.Greeting
.
The other output, which is generated by the gRPC plugin to the protobuf compiler, is
example.grpc.GreetServiceGrpc
, which has the client and server side infrastructure for making
remote calls. For our purposes, the important subclass is
public static abstract class GreetServiceImplBase implements io.grpc.BindableService {
public void greet(org.greet.Greet_proto.GeneralEntityMessage request,
io.grpc.stub.StreamObserver<org.greet.Greet_proto.GeneralReturnMessage> responseObserver) {
io.grpc.stub.ServerCalls.asyncUnimplementedUnaryCall(getGreetMethod(), responseObserver);
}
which has a method like greet()
for each rpc entry in the protobuf description file. The default
implementation indicates that the method is not implemented.
The next step, then, is to override each such method. Typically, that would look something like
public class GreeterServiceImpl extends GreeterGrpc.GreeterImplBase {
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
String name = request.getName();
String message = "Hello " + name;
responseObserver.onNext(HelloReply.newBuilder().setMessage(message).build());
responseObserver.onCompleted();
}
...
}
from the helloworld example; that is, it would have some business logic. Here, though, the business
logic is already implemented in the Jakarta REST target project, and we want to override the methods
with code that will appropriately dispatch control to the right method in the target project. In
particular, we will create a servlet environment analogous to the one RESTEasy would create. grpc-bridge
will generate class example.grpc.GreetServiceGrpcImpl
, where the overriding code will look like
@java.lang.Override
public void greet(example.grpc.Greet_proto.GeneralEntityMessage param, StreamObserver<example.grpc.Greet_proto.GeneralReturnMessage> responseObserver) {
try {
HttpServletResponseImpl response = new HttpServletResponseImpl("dev_resteasy_example_grpc_greet___Greeting", "sync", Greet_Server.getServletContext(), builder, fd); // 1
GeneratedMessageV3 actualParam = param.getGEmptyField();
HttpServletRequest request = getHttpServletRequest(param, actualParam, "/greet/{s}", response, "GET", "dev_resteasy_example_grpc_greet___Greeting"); // 2
HttpServletDispatcher servlet = getServlet(); // 3
activateRequestContext(); // 4
servlet.service(request.getMethod(), request, response); // 5
MockServletOutputStream msos = (MockServletOutputStream) response.getOutputStream();
ByteArrayOutputStream baos = msos.getDelegate();
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
dev_resteasy_example_grpc_greet___Greeting reply = dev_resteasy_example_grpc_greet___Greeting.parseFrom(bais); // 6
example.grpc.Greet_proto.GeneralReturnMessage.Builder grmb = createGeneralReturnMessageBuilder(response);
grmb.setDevResteasyGreetGreetingField(reply);
responseObserver.onNext(grmb.build()); // 7
} catch (Exception e) {
responseObserver.onError(e);
} finally {
responseObserver.onCompleted();
if (requestContextController != null) {
requestContextController.deactivate();
}
if (tccl != null) {
Thread.currentThread().setContextClassLoader(tccl);
}
}
}
Without getting into all of the details, the following steps occur:
- create an
HttpServletResponse
- create an
HttpServletRequest
- find the targeted servlet
- activate a CDI context
- pass control into RESTEasy
- retrieve the response
- send the response up to the gRPC runtime
grpc-bridge also generates two more classes that are part of the intermediate layer.
example.grpc.GreetJavabufTranslator
knows how to translate back and forth between each Java message type and its corresponding javabuf class.example.grpc.GreetMessageBodyReaderWriter
implements the Jakarta REST interfacesjakarta.ws.rs.ext.MessageBodyReader
andjakarta.ws.rs.ext.MessageBodyWriter
. It’s registered with the RESTEasy runtime, and it usesGreetMessageBodyReaderWriter
to do the translating.
Note that the response retrieved in step 6 has already been translated by GreetMessageBodyReaderWriter
into a javabuf class, so it’s ready to be transmitted back to the gRPC client.
Automating the build
As much as possible we want to simplify the process of creating the intermediary layer, so we’ve built the process into a maven archetype https://github.com/resteasy/gRPCtoJakartaREST-archetype. Its purpose is to create a bridge project, which will wrap up the classes of the target project together with the classes of the intermediary layer into a WAR which can support both Jakarta REST and gRPC clients.
Running, for example,
mvn archetype:generate -B \
-DarchetypeGroupId=dev.resteasy.grpc \
-DarchetypeArtifactId=gRPCtoJakartaREST-archetype \
-DarchetypeVersion=1.0.0.Alpha5 \
-DgroupId=dev.resteasy.examples \
-DartifactId=grpcToRest.example \
-Dversion=1.0.0.Final-SNAPSHOT \
-Dgenerate-prefix=Greet \
-Dgenerate-package=dev.resteasy.example.grpc.greet \
-Dresteasy-version=6.2.4.Final \
-Dgrpc-bridge-version=1.0.0.Alpha2
for target project grpc-bridge-example (dev.resteasy.examples:grpcToRest.example:1.0.0.Final-SNAPSHOT) will create the skeleton bridge project dev.resteasy.examples:grpcToRest.example.grpc:1.0.0.Final-SNAPSHOT with contents
grpcToRest.example/pom.xml
grpcToRest.example/src/main/resources/buildjar
grpcToRest.example/src/main/resources/deployjar
grpcToRest.example/src/main/webapp/META-INF/beans.xml
grpcToRest.example/src/main/webapp/WEB-INF/web.xml
grpcToRest.example/src/test/java/dev/resteasy/grpc/server/Greet_Server.java
The important element is the pom.xml file, which is able to put everything together.
Next, enter grpcToRest.example and execute
mvn install
which will add the following:
...
grpcToRest.example/src/main/java/dev/resteasy/example/grpc/greet/GeneralGreeting.java
grpcToRest.example/src/main/java/dev/resteasy/example/grpc/greet/Greeter.java
grpcToRest.example/src/main/java/dev/resteasy/example/grpc/greet/Greeting.java
grpcToRest.example/src/main/java/GreetingTest
grpcToRest.example/src/main/proto/Greet.proto
grpcToRest.example/src/test/java/org/jboss/resteasy/grpc/server/Greet_Server.java
...
grpcToRest.example/target/generated-sources/protobuf/java/org/greet/Greet_proto.java
...
grpcToRest.example/target/generated-sources/protobuf/grpc-java/org/greet/Greet_Server.java
grpcToRest.example/target/generated-sources/protobuf/grpc-java/org/greet/GreetServiceGrpc.java
grpcToRest.example/target/generated-sources/protobuf/grpc-java/org/greet/GreetServiceGrpcImpl.java
grpcToRest.example/target/generated-sources/protobuf/grpc-java/org/greet/GreetJavabufTranslator.java
grpcToRest.example/target/generated-sources/protobuf/grpc-java/org/greet/GreetMessageBodyReaderWriter.java
grpcToRest.example/target/grpcToRest.example.grpc-1.0.0.Final-SNAPSHOT.war
Here we see
- the Java classes
GeneralGreeting
,Greeter
, andGreeting
from the target project - the protubuf description file Greet.proto
- the intermediary layer classes
GreetServiceGrpc
,GreetServiceGrpcImpl
,GreetJavabufTranslator
, andGreetMessageBodyReaderWriter
Greet_Server
, generated by the archetype, which is a Jakarta REST resource class. We’ll discuss that shortly.GreetingTest
2, which comes from grpc-bridge-example, the target project. If it’s renamed GreetingTest.java, it can be used to communicate with and test the WAR.- the deployable grpcToRest.example.grpc-1.0.0.Final-SNAPSHOT.war
Notes.
- The output of grpc-bridge-example is grpcToRest.example-1.0.0.Final-SNAPSHOT.war, but the WAR is not deployed to any repository. That is, if you want to follow along, you need to download resteasy-examples and build it in the usual maven manner.
- The WEB-INF/lib directory in grpcToRest.example.grpc-1.0.0.Final-SNAPSHOT.war includes grpc-bridge-runtime-1.0.0.Alpha1-SNAPSHOT.jar, generated from the resteasy-grpc module grpc-bridge-runtime, which supports the intermediary layer at runtime.
Deploying the bridge project
The simplest environment for deploying the bridge project WAR would be an instance of WildFly with the grpc feature pack https://github.com/wildfly-extras/wildfly-grpc-feature-pack that we introduced in Part I. It will recognize that the WAR holds a gRPC service and register it with the gRPC runtime.
One non-obvious step is necessary to “activate” the bridge project. Recall the Greet_Server
resource class
added to the bridge project by the archetype. Making a “native” Jakarta REST call on Greet_Server
will cause
dev.resteasy.grpc.bridge.runtime.servlet.GrpcHttpServletDispatcher
in resteasy-grpc’s grpc-bridge-runtime module
to capture the bridge project’s servlet for subsequent use by GreetServiceGrpcImpl
. Moreover, the specific
resource method Greet_Server.startContext()
exists to capture the ServletContext
for the servlet, and a
“native” Jakarta REST call to startContext()
will get that done. For example,
curl http://localhost:9555/grpcToRest.example.grpc-1.0.0.Final-SNAPSHOT/grpcToJakartaRest/grpcserver/context
where
- 9555 is the default port monitored by the grpc feature pack;
- “grpcToRest.example.grpc-1.0.0.Final-SNAPSHOT” is the default context for grpcToRest.example.grpc-1.0.0.Final-SNAPSHOT.war;
- “grpcToJakartaRest” is the servlet-mapping in the bridge project web.xml;
- “grpcserver” and “context” are derived from the
PATH
annotations inGreet_Server
.
Note. If the target environment is a WildFly without the grpc feature pack, or even something other than WildFly,
calling Greet_Server.start()
will set up an ad hoc gRPC server on port 8082.
TODO
The current release of the resteasy-grpc project is preliminary. The goal is to be able to cope with all Jakarta REST semantics. For example,
- Java type semantics (primitives, inheritance, inner classes)
- HTTP elements (cookies, headers)
- asynchronous resource methods
- SSE streaming
- @Context injection
A good indication of what is supported can be seen in the test class org.jboss.resteasy.test.grpc.AbstractGrpcToJakartaRESTTest
in https://github.com/resteasy/resteasy-grpc. In fact, all of these are supported except for some aspects of @Context injection.
Jakarta REST requires that implementations that support servlets, which RESTEasy does, should support resource methods like
public String method(@Context HttpServletRequest request, @Context HttpServletResponse response) {
...
}
Currently, some but not all of the semantics of HttpServletRequest
and HttpServletResponse
are supported.
Another missing piece is support for CDI scopes other than the request scope.
We invite feedback, including questions, suggestions, bug reports, etc., at https://github.com/resteasy/resteasy-grpc/issues.
More information
-
The documentation in https://github.com/resteasy/resteasy-grpc/blob/main/docs/grpc-bridge.md is the most comprehensive treatment of the grpc-bridge project.
-
The “README.md” file in https://github.com/resteasy/gRPCtoJakartaREST-archetype discusses the archetype.
-
The “README.md” file in grpc-bridge-example has a discussion about the example.
Notes
-
Note that if “option java_multiple_files = true;” appears at the beginning of the protobuf description, each message type is represented as a separate class. ↩
-
The reason we don’t treat
GreetingTest
as a Java class is that it depends on classes in the intermediary layer, which don’t exist until the bridge project is built. ↩
Useful Links
YourKit supports open source projects with innovative and intelligent tools for monitoring and profiling Java and .NET applications. YourKit is the creator of YourKit Java Profiler, YourKit .NET Profiler, and YourKit YouMonitor