Skip to content

Commit

Permalink
feat(grpc): added grpc transcoding
Browse files Browse the repository at this point in the history
  • Loading branch information
zZHorizonZz committed Apr 25, 2024
1 parent 323774b commit 2fb227c
Show file tree
Hide file tree
Showing 45 changed files with 2,665 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public enum Feature {
FLYWAY,
GRPC_CLIENT,
GRPC_SERVER,
GRPC_TRANSCODING,
HIBERNATE_ORM,
HIBERNATE_ENVERS,
HIBERNATE_ORM_PANACHE,
Expand Down
166 changes: 166 additions & 0 deletions docs/src/main/asciidoc/grpc-transcoding.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
////
This guide is maintained in the main Quarkus repository
and pull requests should be submitted there:
https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
////
= Using gRPC Transcoding
include::_attributes.adoc[]
:categories: serialization
:summary: This page explains how to enable gRPC Transcoding in your Quarkus application for RESTful interactions with gRPC services.
:topics: grpc, transcoding, rest, json
:extensions: io.quarkus:quarkus-grpc

gRPC Transcoding lets you expose your gRPC services as RESTful JSON endpoints.
This is particularly useful in these scenarios:

1. **Client-side limitations:** When you need to interact with gRPC services from environments (like web browsers) that don't directly support gRPC.
2. **Simplified local development:** While services like Google Cloud Run and Google Cloud Endpoints offer built-in gRPC transcoding, replicating this locally often requires setting up a proxy like Envoy. Transcoding directly within your Quarkus application streamlines your development process.
== Configuring Your Project

First, add the `quarkus-grpc` extension to your project:

[source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"]
.pom.xml
----
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-grpc</artifactId>
</dependency>
----

[source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"]
.build.gradle
----
implementation("io.quarkus:quarkus-grpc")
----

== Transcoding configuration

include::{generated-dir}/config/quarkus-grpc-transcoding-config-grpc-transcoding-config.adoc[opts=optional,leveloffset=+1]

== Example

Let's imagine you have a gRPC service defined.
Here's an example of a simple service:

[source,protobuf]
----
syntax = "proto3";
import "google/api/annotations.proto"; //<1>
option java_multiple_files = true;
option java_package = "examples";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
// The greeting service definition.
service Greeter {
// RPC with simple path
rpc SimplePath (HelloRequest) returns (HelloReply) {
option (google.api.http) = { //<2>
post: "/v1/simple"
body: "*"
};
}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
----

<1> We need to import the `google/api/annotations.proto` file so that we can use the `google.api.http` option.
<2> This option is used to define the RESTful path for the gRPC service.

Now we need to implement the service:

[source,java]
----
@GrpcService
public class HelloWorldNewService implements Greeter {
@Override
public Uni<HelloReply> simplePath(HelloRequest request) {
return Uni.createFrom().item(HelloReply.newBuilder().setMessage("Hello " + request.getName()).build());
}
}
----

To enable gRPC Transcoding, you need to add the following configuration to your `application.properties` file:

[source,properties]
----
quarkus.grpc.transcoding.enabled=true
----

Now you can access the gRPC service through a RESTful JSON interface.
For example, you can use the following `curl` command:

[source,shell]
----
curl -X POST http://localhost:8080/v1/simple -H "Content-Type: application/json" -d '{"name": "World"}'
----

This command should return response similar to the following:

[source,json]
----
{
"message": "Hello World"
}
----

== Advanced Usage

While the above example demonstrates a simple use case, gRPC Transcoding can be configured in more complex scenarios.
For example, you can define paths with variables, query parameters, and more.

For example you can define methods with path variables:

[source,protobuf]
----
service Greeter {
rpc PathWithVariable (HelloRequest) returns (HelloReply) {
option (google.api.http) = {
post: "/v1/path/{name}" //<1>
body: "*"
};
}
}
message HelloRequest {
string name = 1; //<2>
}
----

<1> The path variable is defined using curly braces.
<2> The `name` field is used to define the path variable.

Now if you send a request to the `/v1/path/World` path, like this:

[source,shell]
----
curl -X POST http://localhost:8080/v1/path/World
----

You should receive a response similar to the following:

[source,json]
----
{
"message": "Hello World"
}
----

**Important Notes:**

* You also should consult https://cloud.google.com/endpoints/docs/grpc/transcoding[google's documentation on gRPC transcoding] for more information.
* Consider whether you need a proxy like Envoy for advanced transcoding and routing.
1 change: 1 addition & 0 deletions docs/src/main/asciidoc/grpc.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ Quarkus gRPC is based on https://vertx.io/docs/vertx-grpc/java/[Vert.x gRPC].
* xref:grpc-service-consumption.adoc[Consuming a gRPC Service]
* xref:grpc-kubernetes.adoc[Deploying your gRPC Service in Kubernetes]
* xref:grpc-xds.adoc[Enabling xDS gRPC support]
* xref:grpc-transcoding.adoc[Enabling gRPC transcoding support]
* xref:grpc-generation-reference.adoc[gRPC code generation reference guide]
* xref:grpc-reference.adoc[gRPC reference guide]
4 changes: 4 additions & 0 deletions extensions/grpc-common/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
</dependency>
<dependency>
<groupId>com.google.api.grpc</groupId>
<artifactId>proto-google-common-protos</artifactId>
</dependency>
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-grpc</artifactId>
Expand Down
7 changes: 6 additions & 1 deletion extensions/grpc/api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,15 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.24.3</version>
</dependency>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
</dependency>
</dependencies>

</project>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.quarkus.grpc;

import com.google.protobuf.Message;

public interface GrpcTranscoding {

String getGrpcServiceName();

<Req extends Message, Resp extends Message> GrpcTranscodingDescriptor<Req, Resp> findTranscodingDescriptor(
String methodName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.grpc;

public class GrpcTranscodingDescriptor<Req extends com.google.protobuf.Message, Resp extends com.google.protobuf.Message> {

private final GrpcTranscodingMarshaller<Req> requestMarshaller;
private final GrpcTranscodingMarshaller<Resp> responseMarshaller;

public GrpcTranscodingDescriptor(GrpcTranscodingMarshaller<Req> requestMarshaller,
GrpcTranscodingMarshaller<Resp> responseMarshaller) {
this.requestMarshaller = requestMarshaller;
this.responseMarshaller = responseMarshaller;
}

public GrpcTranscodingMarshaller<Req> getRequestMarshaller() {
return requestMarshaller;
}

public GrpcTranscodingMarshaller<Resp> getResponseMarshaller() {
return responseMarshaller;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package io.quarkus.grpc;

import static com.google.common.base.Preconditions.checkNotNull;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

import org.jboss.logging.Logger;

import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import com.google.protobuf.util.JsonFormat;

import io.grpc.MethodDescriptor;
import io.grpc.Status;

public class GrpcTranscodingMarshaller<T extends Message> implements MethodDescriptor.PrototypeMarshaller<T> {

private final static Logger log = Logger.getLogger(GrpcTranscodingMarshaller.class);

private final T defaultInstance;

public GrpcTranscodingMarshaller(T defaultInstance) {
this.defaultInstance = checkNotNull(defaultInstance, "defaultInstance cannot be null");
}

@SuppressWarnings("unchecked")
@Override
public Class<T> getMessageClass() {
return (Class<T>) defaultInstance.getClass();
}

@Override
public T getMessagePrototype() {
return defaultInstance;
}

@Override
public InputStream stream(T value) {
try {
String response = JsonFormat.printer().omittingInsignificantWhitespace().print(value);
return new ByteArrayInputStream(response.getBytes(StandardCharsets.UTF_8));
} catch (InvalidProtocolBufferException e) {
throw Status.INTERNAL.withDescription("Unable to convert message to JSON").withCause(e).asRuntimeException();
}
}

@SuppressWarnings("unchecked")
@Override
public T parse(InputStream stream) {
try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) {
Message.Builder builder = defaultInstance.newBuilderForType();
JsonFormat.parser().ignoringUnknownFields().merge(reader, builder);
return (T) builder.build();
} catch (InvalidProtocolBufferException e) {
log.error("Unable to parse JSON to message", e);
throw Status.INTERNAL.withDescription("Unable to parse JSON to message").withCause(e).asRuntimeException();
} catch (IOException e) {
log.error("An I/O error occurred while parsing the stream", e);
throw Status.INTERNAL.withDescription("An I/O error occurred while parsing the stream").withCause(e)
.asRuntimeException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.quarkus.grpc;

import static java.lang.annotation.ElementType.METHOD;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface GrpcTranscodingMethod {

String grpcMethodName();

String httpMethod();

String httpPath();
}
Loading

0 comments on commit 2fb227c

Please sign in to comment.