syntax = "proto3"; package org.greet; option java_package = "org.greet"; option java_outer_classname = "Greet_proto"; message Greeting { string s = 1; }
The RESTEasy gRPC Bridge Project (aka resteasy-grpc) project (https://github.com/resteasy/resteasy-grpc) has been developed to enable communication between gRPC clients and Jakarta REST servers. The classes in resteasy-grpc’s grpc-bridge module are able to scan a directory tree of Jakarta REST resource classes and generate classes that form an intermediary layer between the gRPC runtime and a Jakarta REST server. Those generated classes are supported by the grpc-bridge-runtime module. The process of generating the files is facilitated by the maven archetype https://github.com/resteasy/gRPCtoJakartaREST-archetype.
protobuf and gRPC
gRPC, introduced by Google, is a relatively recent member of the extended Remote Procedure Call (RPC) / Remote Method Invocation (RMI) family of client-server frameworks. An example of the former is the venerable CORBA, and an example of the latter is Java RMI. An early generation of JBossRemoting presented aspects of both.
protobuf IDL file
gRPC shares with CORBA the concept of a programming language independent Interface Definition Language (IDL) that can be translated into code in any language for which a compiler exists. gRPC is built on top of protobuf, a data definition framework, also from Google, which has an IDL and a precisely defined wire format. For example,
is a protobuf IDL file that defines a data type called Greeting
with a
single string field. Note that each message field is associated with a
distinct integer; e.g., s
is associated with 1, which supports the
translation to and from the wire format. When it is compiled by the protobuf compiler (protoc),
the output is a several hundred line class called org.greet.Greet_proto
:
package org.greet; public final class Greet_proto { public static final class Greeting extends com.google.protobuf.GeneratedMessageV3 implements GreetingOrBuilder { public java.lang.String getS() { ... } public static final class Builder extends com.google.protobuf.GeneratedMessageV3.Builder<Builder> implements org.jboss.greeting.Greeting_proto.GreetingOrBuilder { public Builder setS() { ... } } } }
The Builder
class supports creating a Greeting
org.greet.Greet_proto.Greeting.Builder builder = org.greet.Greet_proto.Greeting.newBuilder(); Greeting greeting = builder.setS("foo").build();
and the getS()
method supports retrieving the contents of the Greeting
.
gRPC extends the protobuf IDL with a syntax for defining methods. For example,
syntax = "proto3"; package org.greet; option java_package = "org.greet"; option java_outer_classname = "Greet_proto"; message Greeting { string s = 1; } message gString {string value = 2;} service GreetService { rpc greet (gString) returns (Greeting); }
adds the greet
remote call. Note that it also adds a gString
type
since remote calls can’t use the builtin string
type. When this
extended file is compiled by protoc together with its gRPC plugin, the result, in
addition to org.greet.Greet_proto
, is the class
org.greet.GreetServiceGrpc
, also with hundreds of lines:
public final class GreetServiceGrpc { ... public static final class GreetServiceBlockingStub extends io.grpc.stub.AbstractBlockingStub<GreetServiceBlockingStub> { ... public org.greet.Greet_proto.Greeting greet(org.greet.Greet_proto.gString request) { ... } } ... public static final class GreetServiceStub extends io.grpc.stub.AbstractAsyncStub<GreetServiceStub> { ... public void greet(org.greet.Greet_proto.gString request, io.grpc.stub.StreamObserver<org.greet.Greet_proto.gString> responseObserver) { ... } } }
Client stubs
The inner class GreetServiceGrpc.GreetServiceBlockingStub
is used on
the client side to invoke the methods defined in the IDL file. In
this case, there is only one, greet()
, and an invocation would look
something like:
private static String target = "localhost:8082"; private static ManagedChannel channel; private static GreetServiceBlockingStub blockingStub; public static void setup() throws Exception { channel = ManagedChannelBuilder.forTarget(target).usePlaintext().build(); blockingStub = GreetServiceGrpc.newBlockingStub(channel); } public void test() throws Exception { org.greet.Greet_proto.gString gs = org.greet.Greet_proto.gString.newBuilder().setS("foo").build(); org.greet.Greet_proto.Greeting response = blockingStub.greet(gs); String s = response.getS(); ... }
There are also
-
GreetServiceStub: asynchronous client stub
-
GreetServiceFutureStub: client stub which returns a GrpcFuture
Server stubs
The gRPC plugin also generates the inner class
GreetServiceGrpc.GreetServiceImplBase
, which has a default method for
each rpc entry in the IDL file. The default method will indicate that
the method is not implemented. The idea is that the developer should
create a class extending GreetServiceImplBase
with implementing
methods. A simple example is
@Override public void greet(org.greet.Greet_proto.gString request, StreamObserver<org.greet.Greet_proto.Greeting> responseObserver) { String name = request.getValue(); org.greet.Greet_proto.Greeting greeting = org.greet.Greet_proto.Greeting.newBuilder().setS("hello, " + name).build(); responseObserver.onNext(greeting); }
google.protobuf.Any
As we will see below, there are situations in which the actual type of a
message cannot be determined until runtime, and protobuf has a general
purpose type, google.protobuf.Any
, which can hold any type of message.
The definition of Any
is
message Any { string type_url = 1; bytes value = 2; }
The value
field has built-in type bytes
, which "May contain any
arbitrary sequence of bytes no longer than 2^32", according to
https://developers.google.com/protocol-buffers/docs/proto3. The type
of the message stored in the value
is described by the URL in the
type_url
field. Consider, for example,
gString gs = gString.newBuilder().setValue("abc").build(); Message m = Any.pack(gs); System.out.println(m);
The output is
type_url: "type.googleapis.com/org.greet.gString" value: "\272\001\003abc"
The string "\272\001\003abc" is the internal representation of a
gString
, the details of which are beyond the scope of this discussion.
See https://developers.google.com/protocol-buffers/docs/encoding for
details. The URL is "type.googleapis.com/org.greet.gString", where the
path "org.greet.gString" gives the type of the object represented in
the value
field.
The advantage of the type_url
field is that it can be used to retrieve
the value of the Any
. Consider, for example, the code
Any any = null; if (/* some predicate */) { gString gs = gString.newBuilder().setValue("abc").build(); any = Any.pack(gs); } else { gInteger gi = gInteger.newBuilder().setValue(7).build(); any = Any.pack(gi); } /* send any */
Then, the Any
can be unpacked as follows:
/* get any */ if (any.getTypeUrl().endsWith("org.greet.gString")) { gString gs = any.unpack(gString.class); System.out.println("gs: " + gs); } else if (any.getTypeUrl().endsWith("org.greet.gInteger")) { gInteger gi = any.unpack(gInteger.class); System.out.println("gi: " + gi); }
Connecting a gRPC client to a Jakarta REST server
A gRPC client needs to access the client stubs like
GreetServiceBlockingStub
, which are generated from an IDL file
by the protobuf compiler together with its gRPC plugin. That is, the process
starts with an IDL file. Where does the IDL file come from? In a typical
case, the IDL file is part of the design and is created manually early
in the process. In the situation we are addressing here, though, we have
a pre-existing Jakarta REST service to which the IDL file must conform.
Now, in principle, it could be generated manually from the Jakarta REST
resource classes, but that would be tedious and error prone.
resteasy-grpc’s grpc-bridge module automates the process.
Generating an IDL file
The class
dev.resteasy.grpc.bridge.generator.protobuf.JavaToProtobufGenerator
traverses, with the help of the Java parser
https://github.com/javaparser/javaparser, a set of Jakarta REST
resource classes. For each class that appears as an entity type or a
return type of a resource method or resource locator,
JavaToProtobufGenerator
generates a protobuf message. For each
resource method or resource locator, it generates an rpc entry.
Note that not all message types can be discovered by syntactic
examination, since a resource method could return a
jakarta.ws.rs.core.Response
, where the type of the actual entity
depends on the behavior of the method. Technically, it’s a
non-computable problem. There is a mechanism for manually specifying
additional classes, which we will discuss in Building the bridge project.
Given org.greet.Greeting
package org.greet; public class Greeting { private String s; public Greeting(String s) { this.s = s; } }
and org.greet.Greeter
package org.greet; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @Path("") public class Greeter { @GET @Path("greet") public Greeting greet(String s) { return new Greeting("hello, " + s); } }
JavaToProtobufGenerator
will generate the IDL file Greet.proto:
syntax = "proto3"; package org.greet; import "google/protobuf/any.proto"; import "google/protobuf/timestamp.proto"; option java_package = "org.greet"; option java_outer_classname = "Greet_proto"; service GreetService { // /greet gString org_greet___Greeting GET sync rpc greet (GeneralEntityMessage) returns (GeneralReturnMessage); } message org_greet___Greeting { string s = 1; } message gInteger {int32 value = 1;} message gFloat {float value = 1;} message gCharacter {string value = 1;} message gByte {int32 value = 1;} message gLong {int64 value = 1;} message gString {string value = 1;} message gBoolean {bool value = 1;} message gDouble {double value = 1;} message gShort {int32 value = 1;} message gHeader { repeated string values = 1; } message gCookie { string name = 1; string value = 2; int32 version = 3; string path = 4; string domain = 5; } message gNewCookie { string name = 1; string value = 2; int32 version = 3; string path = 4; string domain = 5; string comment = 6; int32 maxAge = 7; google.protobuf.Timestamp expiry = 8; bool secure = 9; bool httpOnly = 10; enum SameSite { NONE = 0; LAX = 1; STRICT = 2; } SameSite sameSite = 11; } message ServletInfo { string characterEncoding = 1; string clientAddress = 2; string clientHost = 3; int32 clientPort = 4; } 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 { gString gString_field = 6; FormMap form_field = 7; } } message GeneralReturnMessage { map<string, gHeader> headers = 1; repeated gNewCookie cookies = 2; gInteger status = 3; oneof messageType { org_greet___Greeting org_greet___Greeting_field = 4; } }
Clearly, the generated IDL file is more complicated than the one discussed earlier. The more interesting distinctions are the following:
-
protobuf does not have a notion of packages, so the class org.greet.Greeting is represented as org_greet___Greeting
-
Some information pertaining to the rpc entries is saved in comments for future use. In the example, "/greet gString org_greet___Greeting GET sync" means:
-
the path to the greet() method is "/greet"
-
the type of the entity parameter is
gString
-
the type of the response entity is
org_greet___Greeting
-
the HTTP verb on the resource method is GET
-
the resource method is synchronous
-
-
The
GeneralEntityMessage
message type is used as the request value for all methods. Something like this complex structure is necessary to bridge the gap between gRPC requests and Jakarta REST requests. In particular, while gRPC allows only a single request value, Jakarta REST allows, besides the entity parameter itself, things like headers, cookies, query parameters, etc.GeneralEntityMessage
can accomodate all of those. Also, consider the element
oneof messageType { gString gString_field = 5; FormMap form_field = 6; }
oneof
is a protobuf construct that allows a field to be populated with a
value whose type is one of the types listed in the
oneof
list. There’s only one rpc method here, so there’s only one type
in the list (other than the FormMap
field for form data, which isn’t used here).
But suppose there were another rpc method with the comment
// /float gFloat gInteger GET sync
Then the oneof
field would look like
oneof messageType { gString gString_field = 5; gFloat gFloat_field = 6; FormMap form_field = 7; }
This way, resource methods with a String
entity type or a float
entity type could be represented.
-
The
GeneralReturnMessage
message type plays a role likeGeneralEntityMessage
but for return values. -
The world of cookie specification is somewhat fragmented, but the definitions here are intended to be generally applicable.
Note. There is a version of the classes mentioned here, Greeting
,
etc., available to play with at
https://github.com/resteasy/resteasy-examples/tree/main/grpc-bridge-example.
Inheritance
gRPC and Jakarta REST have different semantics, and the classes
GeneralEntityMessage
and GeneralReturnMessage
introduced in the
preceding section help to bridge the differences. Another fundamental
difference is the lack of a notion of inheritance in gRPC. That is, the
protobuf IDL supports nested structures but does not have a notion of a
structure being derived from another structure.
JavaToProtobufGenerator
generates a special field to represent a
parent class. Let’s define the class GeneralGreeting
package org.greet; public class GeneralGreeting extends Greeting { private String salute; public GeneralGreeting(String salute, String s) { super(s); this.salute = salute; } }
and extend Greeter
:
@Path("") public class Greeter { ... @GET @Path("salute") public GeneralGreeting generalGreet(@QueryParam("salute") String salute, String s) { return getGeneralGreeting(salute, s); } private GeneralGreeting getGeneralGreeting(String salute, String name) { return new GeneralGreeting(salute, name); } }
Then JavaToProtobufGenerator
will make the following adjustments to
Greet.proto:
... service GreetService { // 1 // /greet gString org_greet___Greeting GET sync rpc greet (GeneralEntityMessage) returns (GeneralReturnMessage); // /salute gString org_greet___GeneralGreeting GET sync rpc generalGreet (GeneralEntityMessage) returns (GeneralReturnMessage); } ... message org_greet___GeneralGreeting { // 2 string salute = 1; org_greet___Greeting greeting___super = 2; } ... message GeneralReturnMessage { map<string, gHeader> headers = 1; repeated gNewCookie cookies = 2; gInteger status = 3; repeated gCookie cookies = 4; string httpMethod = 5; oneof messageType { // 3 org_greet___Greeting org_greet___Greeting_field = 6; org_greet___GeneralGreeting org_greet___GeneralGreeting_field = 7; } }
Note the following:
-
A second rpc entry is generated.
-
The new message type
org_greet___GeneralGreeting
is generated. -
A second message type option is added to the oneof field in
GeneralReturnMessage
.
Especially, note the field greeting___super
in
org_greet___GeneralGreeting
. The syntax "___super" indicates that
the content of that field represents, in Java terms, the parent class
org.greet.Greeting
. The classes in grpc-bridge that subsequently
process Greet.proto treat it accordingly.
Note, by the way, that getGeneralGreeting()
doesn’t lead to an rpc entry. That’s because,
lacking in Jakarta REST annotations, it’s not a resource method.
Runtime intermediary layer on the server
The gRPC runtime accepts a gRPC request and dispatches it to
GreetServiceGrpc
, whose methods are meant to be overridden by
"business logic" methods. Here, though, the business logic already exists
in the Jakarta REST resource class(es), so we want the request to be
forwarded to a Jakarta REST resource method, and we need code that
transforms a gRPC request to a Jakarta REST request. The class
dev.resteasy.grpc.bridge.generator.ServiceGrpcExtender
in grpc-bridge
will generate org.greet.GreetServiceGrpcImpl
with the necessary
methods.
Given the updated version of org.greet.Greeter
, there will be two
methods in GreetServiceGrpc
that need to be overridden. For example,
public void greet(org.greet.Greet_proto.GeneralEntityMessage param, StreamObserver<org.greet.Greet_proto.GeneralReturnMessage> responseObserver);
will be overridden by
@java.lang.Override public void greet(org.greet.Greet_proto.GeneralEntityMessage param, StreamObserver<org.greet.Greet_proto.GeneralReturnMessage> responseObserver) { HttpServletRequest request = null; try { HttpServletResponseImpl response = new HttpServletResponseImpl("org_greet___Greeting", "sync", Greet_Server.getContext(), builder, fd); // 1 GeneratedMessageV3 actualParam = param.getGStringField(); request = getHttpServletRequest(param, actualParam, "//greet", response, "GET", "org_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()); org_greet___Greeting reply = org_greet___Greeting.parseFrom(bais); // 6 org.greet.Greet_proto.GeneralReturnMessage.Builder grmb = createGeneralReturnMessageBuilder(response); grmb.setOrgGreetGreetingField(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); } }
The general mission of greet()
is to create a servlet environment for
the RESTEasy resource method to run in. More specifically, without going
into too much detail, the following steps occur:
-
create a servlet response
-
create a servlet request
-
find the target servlet inside RESTEasy
-
activate a CDI context
-
call the service() method of the target servlet
-
parse the response object
-
pass the response back to the gRPC runtime
Translating Java classes
Note that the sequence
org.greet.Greeting (Java class) -> (translated by JavaToProtobufGenerator) -> -> org_greet___Greeting (protobuf message) -> (translated by protoc) -> -> org.greet.Greet_proto.org_greet___Greeting (Java class)
turns the Java class org.greet.Greeting
into a second Java class
org.greet.Greet_proto.org_greet___Greeting
by way of the protobuf
message type org_greet___Greeting
. For clarity, we refer to
org.greet.Greet_proto.org_greet___Greeting
as the javabuf version
of org.greet.Greeting
.
Two classes are generated to translate back and forth between a Java
class and its javabuf counterpart. In particular, the class
dev.resteasy.grpc.bridge.generator.protobuf.JavabufTranslatorGenerator
generates a class like org.greet.GreetJavabufTranslator
, which has two
static methods
public static Message translateToJavabuf(Object o); public static Object translateFromJavabuf(Message message);
which do the translations. Note that all javabuf classes implement the
interface com.google.protobuf.Message
. Without going too deeply into
the details, GreetJavabufTranslator
has two classes for each message
type; for example,
static class org_greet___Greeting_ToJavabuf implements TranslateToJavabuf { ... } static class org_greet___Greeting_FromJavabuf implements TranslateFromJavabuf { ... }
Each class has a list of lambdas, each lambda being responsible for translating one field.
GreetJavabufTranslator
does the heavy lifting of the translations. It
is called from the class org.greet.GreetMessageBodyReaderWriter
, which
is generated by
dev.resteasy.grpc.bridge.generator.protobuf.ReaderWriterGenerator
.
GreetMessageBodyReaderWriter
implements
jakarta.ws.rs.ext.MessageBodyReader
and
jakarta.ws.rs.ext.MessageBodyWriter
, so it’s registered as a provider
with the RESTEasy runtime. The request entity and the response entity
are instances of javabuf classes, so it’s important that
GreetMessageBodyReaderWriter
is always used instead of any other
providers. Since some built-in providers like
org.jboss.resteasy.plugins.providers.StringTextStar
are very general,
it is important to guarantee that GreetMessageBodyReaderWriter
has the
highest priority. One strategy available in RESTEasy is to eliminate
all built-in providers and then add back any that are necessary. For
example, that can be accomplished in a web.xml file as follows:
<servlet> <servlet-name>GreetServlet</servlet-name> <servlet-class> dev.resteasy.grpc.bridge.runtime.servlet.GrpcHttpServletDispatcher </servlet-class> </servlet> <!-- The intention is that GreetMessageBodyReaderWriter (with the help of GreetJavabufTranslator) will handle all reading and writing of data objects. Therefore, we 1. eliminate all builtin providers, and then 2. add back builtin providers other than MessageBodyReaders and MessageBodyWriters. --> <context-param> <param-name>resteasy.use.builtin.providers</param-name> <param-value>false</param-value> </context-param> <context-param> <param-name>resteasy.servlet.mapping.prefix</param-name> <param-value>/grpcToJakartaRest</param-value> </context-param> ... <context-param> <param-name>resteasy.providers</param-name> <param-value> org.jboss.resteasy.client.jaxrs.internal.CompletionStageRxInvokerProvider, org.jboss.resteasy.plugins.interceptors.CacheControlFeature, org.jboss.resteasy.plugins.interceptors.ClientContentEncodingAnnotationFeature, org.jboss.resteasy.plugins.interceptors.MessageSanitizerContainerResponseFilter, org.jboss.resteasy.plugins.interceptors.ServerContentEncodingAnnotationFeature, org.jboss.resteasy.plugins.providers.AsyncStreamingOutputProvider, org.jboss.resteasy.plugins.providers.CompletionStageProvider, org.jboss.resteasy.plugins.providers.jackson.PatchMethodFilter, org.jboss.resteasy.plugins.providers.jackson.UnrecognizedPropertyExceptionHandler, org.jboss.resteasy.plugins.providers.jaxb.XmlJAXBContextFinder, org.jboss.resteasy.plugins.providers.jsonp.JsonpPatchMethodFilter, org.jboss.resteasy.plugins.providers.ReactiveStreamProvider, org.jboss.resteasy.plugins.validation.ResteasyViolationExceptionMapper, org.jboss.resteasy.plugins.validation.ValidatorContextResolver, org.jboss.resteasy.plugins.validation.ValidatorContextResolverCDI, org.jboss.resteasy.security.doseta.ClientDigitalSigningHeaderDecoratorFeature, org.jboss.resteasy.security.doseta.ClientDigitalVerificationHeaderDecoratorFeature, org.jboss.resteasy.security.doseta.DigitalSigningInterceptor, org.jboss.resteasy.security.doseta.DigitalVerificationInterceptor, org.jboss.resteasy.security.doseta.ServerDigitalSigningHeaderDecoratorFeature, org.jboss.resteasy.security.doseta.ServerDigitalVerificationHeaderDecoratorFeature </param-value> </context-param> <servlet-mapping> <servlet-name>GreetServlet</servlet-name> <url-pattern>/grpcToJakartaRest/*</url-pattern> </servlet-mapping>
Of course, the list of providers can be reduced to those that are actually needed.
SSE
One area in which gRPC has richer semantics than Jakarta REST is streaming, where gRPC supports streaming in two directions, client to server and server to client, while Jakarta REST supports streaming only from server to client. In particular, Jakarta REST adopts a version of the Server Sent Events (SSE) specification (https://html.spec.whatwg.org/multipage/server-sent-events.html) to describe server to client streaming. RESTEasy’s support of SSE is discussed in Section "Server-Sent Events (SSE)" of the RESTEasy User Guide.
The examples so far have demonstrated simple call / response semantics.
A few changes are necessary to support SSE streaming. Suppose
org.greet.Greeter
is extended with method sseGreet
:
private ArrayList<String> names = new ArrayList<String>(); @GET @Path("stream") @Produces(MediaType.SERVER_SENT_EVENTS) public void sseGreet(@Context SseEventSink eventSink, @Context Sse sse) { ExecutorService executor = Executors.newFixedThreadPool(3); final Map<Class<?>, Object> map = ResteasyContext.getContextDataMap(); executor.execute(() -> { ResteasyContext.addCloseableContextDataLevel(map); try (SseEventSink sink = eventSink) { Iterator<String> it = names.iterator(); while (it.hasNext()) { eventSink.send(sse.newEvent("hello, " + it.next())); } } }); }
A couple of additions appear in Greet.proto:
-
A new message type is added:
message org_jboss_resteasy_grpc_runtime_sse___SseEvent { string comment = 1; string id = 2; string name = 3; google.protobuf.Any data = 4; int64 reconnectDelay = 5; }
-
a new rpc entry is added:
// stream gEmpty org_jboss_resteasy_grpc_runtime_sse___SseEvent GET sse rpc sseGreet (GeneralEntityMessage) returns (stream org_jboss_resteasy_grpc_runtime_sse___SseEvent);
Note that returns (stream org_jboss_resteasy_grpc_runtime_sse___SseEvent)
indicates that the call returns a stream of
org_jboss_resteasy_grpc_runtime_sse___SseEvent
objects.
The overriding method in GreetServiceGrpcImpl
changes to handle
multiple return messages:
@java.lang.Override public void sseGreet(org.greet.Greet_proto.GeneralEntityMessage param, StreamObserver<org.greet.Greet_proto.org_jboss_resteasy_grpc_runtime_sse___SseEvent> responseObserver) { HttpServletRequest request = null; try { HttpServletResponseImpl response = new HttpServletResponseImpl("org_jboss_resteasy_grpc_sse_runtime___SseEvent", "sse", Greet_Server.getContext(), builder, fd); GeneratedMessageV3 actualParam = param.getGEmptyField(); request = getHttpServletRequest(param, actualParam, "/stream", response, "GET", "org_jboss_resteasy_grpc_sse_runtime___SseEvent"); HttpServletDispatcher servlet = getServlet(); activateRequestContext(); servlet.service(request.getMethod(), request, response); AsyncMockServletOutputStream amsos = (AsyncMockServletOutputStream) response.getOutputStream(); while (true) { if (amsos.isClosed()) { break; } ByteArrayOutputStream baos = amsos.await(); if (amsos.isClosed()) { break; } byte[] bytes = baos.toByteArray(); if (bytes.length == 2 && bytes[0] == 10 && bytes[1] == 10) { continue; } try { org_jboss_resteasy_grpc_runtime_sse___SseEvent sseEvent = org_jboss_resteasy_grpc_runtime_sse___SseEvent.parseFrom(bytes); responseObserver.onNext(sseEvent); } catch (Exception e) { continue; } } } catch (Exception e) { responseObserver.onError(e); } finally { responseObserver.onCompleted(); if (requestContextController != null) { requestContextController.deactivate(); } if (tccl != null) { Thread.currentThread().setContextClassLoader(tccl); } } }
These changes are generated automatically, so no intervention is required. However, the application code on the client side needs to be adjusted. It could look, for example, something like this:
Iterator<org_jboss_resteasy_grpc_runtime_sse___SseEvent> response = blockingStub.sseGreet(gem); while (response.hasNext()) { org_jboss_resteasy_grpc_runtime_sse___SseEvent sseEvent = response.next(); Any any = sseEvent.getData(); gString gString = any.unpack(gString.class); System.out.println(gString.getValue()); }
Note, in particular, the treatment of the data
field. The class
jakarta.ws.rs.sse.OutboundSseEvent
has a data
field of type
java.lang.Object
. The corresponding field in the definition of
org_jboss_resteasy_grpc_runtime_sse___SseEvent
in Greet.proto has type
google.protobuf.Any
, which translates to com.google.protobuf.Any
in
Greet_proto
. Note that the method Any.pack()
has signature
public static <T extends com.google.protobuf.Message> Any pack(T message, java.lang.String typeUrlPrefix);
so we have to translate the Object
into a Message
; The translation
is handled by GreetJavabufTranslator
, which implies that the type of
the field must be one processed by JavaToProtobufGenerator
. If it is
not discovered automatically, it can be passed into
JavaToProtobufGenerator
with the "classes" parameter, described in
Building the bridge project.
Other uses of google.protobuf.Any
Consider the resource method
public Response m() { if (test()) { return Response.ok(new X()).build(); } else { return Response.ok(new Y()).build(); } }
Will it return an X
or a Y
? If test()
is
public boolean test() { return true; }
it’s clear that m()
will return an X
, and, moreover, that can be
determined statically at compile time. But it’s a well known fact in
theoretical computer science, first proved by Alan Turing [see, for
example, https://en.wikipedia.org/wiki/Halting_problem], that not all
questions can be answered algorithmically.
We can’t tell if m()
returns an X
or a Y
, but we know it returns
an Object
. This is another case in which the protobuf type
google.protobuf.Any
is useful.
Suppose we add the resource method
@GET @Path("greet/response") public Response response(String name) { return Response.ok("hello " + name).build(); }
to org.greet.Greeter
. Then the oneof field of GeneralReturnMessage
becomes
oneof messageType { org_greet___Greeting org_greet___Greeting_field = 4; org_greet___GeneralGreeting org_greet___GeneralGreeting_field = 5; google.protobuf.Any google_protobuf_Any_field = 6; }
augmented by the google_protobuf_Any_field
field.
Together, GreetMessageBodyReaderWriter
and GreetJavabufTranslator
turn "hello " + name
in the Response
into a gString
and then pack it into an Any
. Then we might have the following on the
client side:
GeneralEntityMessage.Builder messageBuilder = GeneralEntityMessage.newBuilder(); gString gs = gString.newBuilder().setValue("Bill").build(); GeneralEntityMessage gem = messageBuilder.setGStringField(gs).build(); try { GeneralReturnMessage grm = blockingStub.response(gem); Any any = grm.getGoogleProtobufAnyField(); System.out.println(any.unpack(gString.class)); } catch (StatusRuntimeException e) { // }
Another case in which we can’t statically determine the return type is
when an asynchronous resource method uses the @Suspended
annotation:
@GET @Path("suspend") public void suspend(@Suspended final AsyncResponse response) { Thread t = new Thread() { @Override public void run() { try { response.resume("suspend"); } catch (Exception e) { response.resume(e); } } }; t.start(); }
Similar unpacking would happen on the client side.
Automating the generation of the intermediary classes
Building the bridge project
There are a lot of moving parts in the generation of the classes that make up the gRPC to Jakarta REST intermediary layer, so we’ve gathered the details together into a maven archetype in the gRPCtoJakartaREST-archetype github project (https://github.com/resteasy/gRPCtoJakartaREST-archetype).
gRPCtoJakartaREST-archetype starts with a Jakarta REST maven project, called the target project, and creates a bridge project, which extends the target project with additional classes that form an intermediate layer that liaises between the gRPC world and the Jakarta REST world. Note that the bridge project can function as a Jakarta REST project, and so it can replace the target project.
To begin, gRPCtoJakartaREST-archetype generates a mostly empty bridge project, consisting mainly of a pom.xml file that can build the intermediary classes and generate a WAR. It assumes that the target project’s source JAR is available in an accessible repository. In this example, we assume the existence of org.greet:greet:0.0.1-SNAPSHOT. To generate the initial state of the bridge project, run
mvn archetype:generate -B \ -DarchetypeGroupId=dev.resteasy.grpc \ -DarchetypeArtifactId=gRPCtoJakartaREST-archetype \ -DarchetypeVersion=1.0.0.Alpha5 \ -DgroupId=org.greet \ -DartifactId=greet \ -Dversion=0.0.1-SNAPSHOT \ -Dgenerate-prefix=Greet \ -Dgenerate-package=org.greet \ -Dresteasy-version=6.2.4.Final \ -Dgrpc-bridge-version=1.0.0.Alpha2
The following parameters need to be supplied:
-
archetypeGroupId: gRPCtoJakartaREST-archetype’s groupId
-
archetypeArtifactId: gRPCtoJakartaREST-archetype’s archetypeId
-
archetypeVersion: gRPCtoJakartaREST-archetype’s version
-
groupId: groupId of the target project
-
artifactId: artifactId of the target project
-
version: version of the target project
-
generate-prefix: the prefix for generated classes
-
generate-package: Java package for generated classes
-
resteasy-version: version of RESTEasy to use
-
grpc-bridge-version: version of resteasy-grpc to use
The values of archetypeVersion, resteasy-version, and grpc-bridge-version, of course, are subject to change.
The result of running the archetype is a new project with GAV groupId:artifactId.grpc:version. For example, from target project org.greet:greet:0.0.1-SNAPSHOT we will get bridge project org.greet:greet.grpc:0.0.1-SNAPSHOT. At this point, the layout of the new project is
+- pom.xml +- src/main/webapp | +- META-INF | | +- beans.xml | +- WEB-INF | +- web.xml +- src/main/resources | +- buildjar | +- deployjar
The most important file is pom.xml, which describes the sequence of events necessary for generating a WAR with the contents of the target project plus the intermediary layer. The other files are
-
beans.xml: empty file
-
web.xml: implements the Jakarta REST provider removal described above (See Translating Java classes.)
-
buildjar: a bash script that produces a JAR file
-
deployjar: a bash script that deploys the JAR built by buildjar to a maven repository (See Output products.)
Once the new project is created, the pom.xml can be used to copy the Java classes from the target project and generate the intermediary classes:
mvn clean install
There are also some optional parameters:
-
classes: additional classes not detected by syntactic scanning
-
release.type: deploy as a snapshot or otherwise. Defaults to "snapshot". (See (Output products.)
-
inWildFly: the generated WAR will be run in WildFly. Defaults to "true". (See Output products.)
The syntax for the "classes" parameter is
(DIR ":" CLASSNAME) ("," DIR ":" CLASSNAME)*
where
-
DIR: directory of the class’s source
-
CLASSNAME: fully qualified name of the class
For example,
mvn -Dclasses=/home/bob/greet/src/java/main:org.greet.Extra clean install
When the project is built, the layout is as follows:
+- pom.xml +- src/main/java | +- org.greet | +- GeneralGreeting.java | + Greeter.java | + Greeting.java +- src/main/proto | +- Greet.proto +- src/main/webapp | +- META-INF | | +- beans.xml | +- WEB-INF | +- web.xml +- src/main/resources | +- buildjar | +- deployjar +- target/generated-sources/protobuf | +- java | +- org.greet | | +- Greet_proto.java | +- grpc-java | +- org.greet | +- Greet_Server.java | +- GreetJavabufTranslator.java | +- GreetMessageBodyReaderWriter.java | +- GreetServiceGrpc.java | +- GreetServiceGrpcImpl.java | +- greet.grpc-0.0.1-SNAPSHOT.jar | +- greet.grpc-0.0.1-SNAPSHOT.war | +- greet.grpc-0.0.1-SNAPSHOT-sources.jar
Notes
-
The intermediary layer classes discussed above are in target/generated-sources/protobuf/grpc-java.
-
We’ll discuss
Greet_Server
below in Using the generated WAR -
For any of the files <prefix>.proto, <prefix>JavabufTranslator.java, <prefix>MessageBodyReaderWriter.java, or <prefix>ServiceGrpcImpl.java, if the file exists, the build process will not overwrite it. This makes it possible to build a file and then tweak it for subsequent builds.
Output products
The packaging type of the project created by the archetype is "war", so, in the example, mvn install creates greet.grpc-0.0.1-SNAPSHOT.war. The project also uses the src/main/resources/buildjar bash script to create greet.grpc-0.0.1-SNAPSHOT.jar with all of the compiled classes.
Going a step further, mvn deploy can deploy the WAR and JAR (where the deployjar bash script manages the latter) to a remote repository. Note that deployjar hard codes the JBoss repositories as follows:
if [ ${RELEASE_TYPE} == "snapshot" ]; then URL=https://repository.jboss.org/nexus/content/repositories/snapshots/ else URL=https://repository.jboss.org/nexus/service/local/staging/deploy/maven2/ fi
These presumably need to be modified. Maven repository configuration is beyond the scope of this document.
Depending on the environment to which it will be deployed, the WAR’s WEB-INF/lib directory can contain only grpc-bridge-runtime-1.0.0.Alpha2.jar or it can be populated with all of the protobuf, gRPC, and other JARs necessary for the intermediary classes to run. For example, if the WAR is to be deployed to an instance of WildFly running with the gRPC subsystem https://github.com/wildfly-extras/wildfly-grpc-feature-pack, then it should be built with parameter "inWildFly" set to "true" (or anything other than "false"), which will result in a WEB/lib directory with just grpc-bridge-runtime-1.0.0.Alpha2.jar. Setting it to "false" will populate WEB-INF/lib appropriately.
Using the generated WAR
If the WAR is deployed to an instance of WildFly running with the grpc
subsystem https://github.com/wildfly-extras/wildfly-grpc-feature-pack,
then GreetServiceGrpcImpl
will be recognized and registered with the
gRPC runtime.
Once GreetServiceGrpcImpl
is registered, there’s one more thing to do to set up the intermediary
layer. The overriding methods in GreetServiceGrpcImpl
need to be able
to dispatch the request to the appropriate servlet. A Jakarta REST
request to dev.resteasy.grpc.server.Greet_Server
in the bridge
project’s target/generated-sources/protobuf/grpc-java directory will
cause the handling servlet to be stored by GrpcHttpServletDispatcher
so that it can be retrieved by the intermediary code for subsequent gRPC
requests. Moreover, calling Greet_Server.startContext()
in particular
will accomplish the other initial requirement, which is storing a
reference to the servlet’s jakarta.servlet.ServletContext
. For
example,
curl http://localhost:8080/greet.grpc-0.0.1-SNAPSHOT/grpcToJakartaRest/grpcserver/context
Alternatively, if the generated WAR is not running in an instance of WildFly with the grpc subsystem,
curl http://localhost:8080/greet.grpc-0.0.1-SNAPSHOT/grpcToJakartaRest/grpcserver/start
will initiate the gRPC server runtime.
The step can also be done programmatically, as in org.jboss.restesy.test.grpc.AbstractGrpcToJakartaRESTTest
in the resteasy-grpc-testsuite in resteasy-grpc:
try ( Client client = ClientBuilder.newClient(); var response = client.target("http://localhost:8080/grpc-test/grpcserver/context") .request() .get()) { final var message = response.getStatus() + ": " + response.readEntity(String.class); Assert.assertEquals(message, response.getStatus(), 200); }
By the way, AbstractGrpcToJakartaRESTTest
has a lot of client side code that might be useful to look at.
Human intervention
As much as possible, grpc-bridge and grpc-bridge-runtime automate the conversion back and forth between the gRPC and Jakarta REST worlds, but there are some situations in which manual intervention is required, for one reason or another.
Bridge project creation time
The bridge project is meant to be an extension of the target project. Now, the main reason for installing a pom.xml file in the bridge project is to capture the sequence of events necessary to create the various classes in the intermediary layer. But it may be necessary to merge into it pieces of the target project’s pom.xml, dependencies, for example, in order to capture the construction of the target project.
One function of the bridge pom.xml is to copy the classes from the target project. Those are clearly necessary. But there may be other pieces of the target project like resource files that are also needed. They could be copied manually, or the bridge pom.xml could be extended.
Compile time
We have already discussed, in SSE and Other uses of google.protobuf.Any
,
situations in which it is not possible to determine
statically all classes that are sent over the network. For example, if a
resource method returns Response
, it may not be possible to determine
the type of the returned entity. That means that, when
JavaToProtobufGenerator
scans for classes, it may not find all of
those used, in which case the "classes" command line argument,
described in Building the bridge project, can be used
to supply those that are not detected.
Also, we mentioned in Building the bridge project that running maven to build the bridge project results in copying Java classes from the target project. If other files are needed, that would need to be handled separately.
Servlet environment
Although the Jakarta REST specification does not mandate its use, a servlet container is a common environment for running Jakarta REST applications, and, in that case, the spec mandates the availability by injection of certain servlet related types:
The @Context annotation can be used to indicate a dependency on a Servlet-defined resource. A Servlet-based implementation MUST support injection of the following Servlet-defined types: ServletConfig, ServletContext, HttpServletRequest, and HttpServletResponse.
RESTEasy supports servlets, and, accordingly, grpc-bridge creates a servlet environment for Jakarta REST resources to execute in, including the four mandated servlet types.
Note that Using the generated WAR discusses a step
involving a Jakarta REST client call that must be taken before gRPC
calls can be made. It is responsible not only for storing the servlet,
but it also results in storing references to a ServletContext
and
ServletConfig
for later use.
The other two injectable classes, HttpServletRequest
and
HttpServletResponse
, are supplied by the grpc-bridge runtime.
Normally, those classes would be created by a servlet container which
has an actual HTTP network connection from which information like URLs,
headers, and addresses can be obtained, but for the grpc-bridge runtime,
the HTTP connection is hidden by the gRPC runtime. Some information can
be derived or approximated; for example, in the absence of path
parameters, the path can be derived from the @Path
annotation(s). In
many cases, though, HttpServletRequest
relies on the client to spell
out any information needed for a given computation. Recall that
GeneralEntityMessage
has slots for all kinds of information:
message GeneralEntityMessage { ServletInfo servletInfo = 1; string URL = 2; map<string, gHeader> headers = 3; repeated gCookie cookies = 4; string httpMethod = 5; oneof messageType { ... } }
Some of these fields, e.g., cookies and headers, are naturally supplied by the client. On the other hand, the information in
message ServletInfo { string characterEncoding = 1; string clientAddress = 2; string clientHost = 3; int32 clientPort = 4; }
which would normally come from the network connection, must be supplied explicitly as part of the invocation.
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