Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ The JSONToGRPC GatewayFilter Factory converts a JSON payload to a gRPC request.

The filter takes the following arguments:

* `service`: Short name of the service that handles the request.

* `method`: Method name in the service that handles the request.

* `protoDescriptor`: Proto descriptor file.

This file can be generated using `protoc` and specifying the `--descriptor_set_out` flag:
Expand All @@ -16,12 +20,6 @@ protoc --proto_path=src/main/resources/proto/ \
src/main/resources/proto/hello.proto
----

* `protoFile`: Proto definition file.

* `service`: Short name of the service that handles the request.

* `method`: Method name in the service that handles the request.

NOTE: `streaming` is not supported.


Expand All @@ -33,11 +31,10 @@ NOTE: `streaming` is not supported.
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("json-grpc", r -> r.path("/json/hello").filters(f -> {
String protoDescriptor = "file:src/main/proto/hello.pb";
String protoFile = "file:src/main/proto/hello.proto";
String service = "HelloService";
String method = "hello";
return f.jsonToGRPC(protoDescriptor, protoFile, service, method);
String protoDescriptor = "file:src/main/proto/hello.pb";
return f.jsonToGRPC(service, method, protoDescriptor);
}).uri(uri))
----

Expand All @@ -48,17 +45,15 @@ spring:
gateway:
routes:
- id: json-grpc
uri: https://localhost:6565/testhello
uri: https://localhost:6565
predicates:
- Path=/json/**
filters:
- name: JsonToGrpc
args:
protoDescriptor: file:proto/hello.pb
protoFile: file:proto/hello.proto
service: HelloService
method: hello

protoDescriptor: file:proto/hello.pb
----

When a request is made through the gateway to `/json/hello`, the request is transformed by using the definition provided in `hello.proto`, sent to `HelloService/hello`, and the response back is transformed to JSON.
Expand Down
11 changes: 6 additions & 5 deletions spring-cloud-gateway-integration-tests/grpc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

<properties>
<protoc.version>3.25.1</protoc.version>
<protobuf.version>4.31.1</protobuf.version>
<grpc.version>1.72.0</grpc.version>
</properties>

Expand Down Expand Up @@ -45,15 +46,16 @@
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-protobuf</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
Expand Down Expand Up @@ -129,4 +131,3 @@
</plugins>
</build>
</project>

Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,19 @@
import org.junit.jupiter.api.Test;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.web.server.test.LocalServerPort;
import org.springframework.test.annotation.DirtiesContext;

import static io.grpc.Status.FAILED_PRECONDITION;
import static io.grpc.netty.NegotiationType.TLS;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;

/**
* @author Alberto C. Ríos
*/
@SpringBootTest(classes = org.springframework.cloud.gateway.tests.grpc.GRPCApplication.class,
webEnvironment = WebEnvironment.RANDOM_PORT)
@DirtiesContext
public class GRPCApplicationTests {

@LocalServerPort
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,22 +36,21 @@
import org.apache.hc.core5.ssl.TrustStrategy;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.web.server.test.LocalServerPort;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.web.client.RestTemplate;

import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;

/**
* @author Alberto C. Ríos
* @author Abel Salgado Romero
*/
@Disabled
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@DirtiesContext
public class JsonToGrpcApplicationTests {

@LocalServerPort
Expand All @@ -72,7 +71,7 @@ public void shouldConvertFromJSONToGRPC() {
final RouteConfigurer configurer = new RouteConfigurer(gatewayPort);
int grpcServerPort = gatewayPort + 1;
configurer.addRoute(grpcServerPort, "/json/hello",
"JsonToGrpc=file:src/main/proto/hello.pb,file:src/main/proto/hello.proto,HelloService,hello");
"JsonToGrpc=HelloService,hello,file:src/main/proto/hello.pb");

String response = restTemplate
.postForEntity("https://localhost:" + this.gatewayPort + "/json/hello",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ server:
management:
endpoint:
health:
show-details: when_authorized
show-details: when-authorized
gateway:
enabled: true
endpoints:
Expand Down
12 changes: 7 additions & 5 deletions spring-cloud-gateway-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<properties>
<main.basedir>${basedir}/..</main.basedir>
<grpc.version>1.72.0</grpc.version>
<protobuf.version>4.31.1</protobuf.version>
<context-propagation.version>1.0.0</context-propagation.version>
</properties>

Expand Down Expand Up @@ -103,11 +104,6 @@
<artifactId>reactor-kotlin-extensions</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-protobuf</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
Expand All @@ -125,6 +121,12 @@
<optional>true</optional>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<optional>true</optional>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
package org.springframework.cloud.gateway.filter.factory;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
Expand All @@ -31,15 +30,17 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.protobuf.ProtobufFactory;
import com.fasterxml.jackson.dataformat.protobuf.schema.ProtobufSchema;
import com.fasterxml.jackson.dataformat.protobuf.schema.ProtobufSchemaLoader;
import com.google.protobuf.DescriptorProtos;
import com.google.protobuf.DescriptorProtos.FileDescriptorProto;
import com.google.protobuf.DescriptorProtos.FileDescriptorSet;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Descriptors.DescriptorValidationException;
import com.google.protobuf.Descriptors.FileDescriptor;
import com.google.protobuf.DynamicMessage;
import com.google.protobuf.ProtocolStringList;
import com.google.protobuf.util.JsonFormat;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
Expand Down Expand Up @@ -95,7 +96,7 @@ public JsonToGrpcGatewayFilterFactory(GrpcSslConfigurer grpcSslConfigurer, Resou

@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("protoDescriptor", "protoFile", "service", "method");
return Arrays.asList("service", "method", "protoDescriptor");
}

@Override
Expand Down Expand Up @@ -124,8 +125,6 @@ public static class Config {

private String protoDescriptor;

private String protoFile;

private String service;

private String method;
Expand All @@ -139,15 +138,6 @@ public Config setProtoDescriptor(String protoDescriptor) {
return this;
}

public String getProtoFile() {
return protoFile;
}

public Config setProtoFile(String protoFile) {
this.protoFile = protoFile;
return this;
}

public String getService() {
return service;
}
Expand All @@ -174,8 +164,6 @@ class GRPCResponseDecorator extends ServerHttpResponseDecorator {

private final Descriptors.Descriptor descriptor;

private final ObjectWriter objectWriter;

private final ObjectReader objectReader;

private final ClientCall<DynamicMessage, DynamicMessage> clientCall;
Expand All @@ -186,26 +174,16 @@ class GRPCResponseDecorator extends ServerHttpResponseDecorator {
super(exchange.getResponse());
this.exchange = exchange;
try {
Resource descriptorFile = resourceLoader.getResource(config.getProtoDescriptor());
Resource protoFile = resourceLoader.getResource(config.getProtoFile());

descriptor = DescriptorProtos.FileDescriptorProto.parseFrom(descriptorFile.getInputStream())
.getDescriptorForType();

Descriptors.MethodDescriptor methodDescriptor = getMethodDescriptor(config,
descriptorFile.getInputStream());
Descriptors.MethodDescriptor methodDescriptor = getMethodDescriptor(config);
Descriptors.ServiceDescriptor serviceDescriptor = methodDescriptor.getService();
Descriptors.Descriptor outputType = methodDescriptor.getOutputType();
this.descriptor = methodDescriptor.getInputType();

clientCall = createClientCallForType(config, serviceDescriptor, outputType);

ProtobufSchema schema = ProtobufSchemaLoader.std.load(protoFile.getInputStream());
ProtobufSchema responseType = schema.withRootType(outputType.getName());

ObjectMapper objectMapper = new ObjectMapper(new ProtobufFactory());
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
objectWriter = objectMapper.writer(schema);
objectReader = objectMapper.readerFor(JsonNode.class).with(responseType);
objectReader = objectMapper.readerFor(JsonNode.class);
objectNode = objectMapper.createObjectNode();

}
Expand Down Expand Up @@ -241,13 +219,14 @@ private ClientCall<DynamicMessage, DynamicMessage> createClientCallForType(Confi
return channel.newCall(methodDescriptor, CallOptions.DEFAULT);
}

private Descriptors.MethodDescriptor getMethodDescriptor(Config config, InputStream descriptorFile)
private Descriptors.MethodDescriptor getMethodDescriptor(Config config)
throws IOException, Descriptors.DescriptorValidationException {
Resource descriptorFile = resourceLoader.getResource(config.getProtoDescriptor());
DescriptorProtos.FileDescriptorSet fileDescriptorSet = DescriptorProtos.FileDescriptorSet
.parseFrom(descriptorFile);
.parseFrom(descriptorFile.getInputStream());
DescriptorProtos.FileDescriptorProto fileProto = fileDescriptorSet.getFile(0);
Descriptors.FileDescriptor fileDescriptor = Descriptors.FileDescriptor.buildFrom(fileProto,
new Descriptors.FileDescriptor[0]);
dependencies(fileDescriptorSet, fileProto.getDependencyList()));

Descriptors.ServiceDescriptor serviceDescriptor = fileDescriptor.findServiceByName(config.getService());
if (serviceDescriptor == null) {
Expand All @@ -262,6 +241,33 @@ private Descriptors.MethodDescriptor getMethodDescriptor(Config config, InputStr
.orElseThrow(() -> new NoSuchElementException("No Method found"));
}

private FileDescriptor[] dependencies(FileDescriptorSet input, ProtocolStringList list) {
FileDescriptor[] deps = new FileDescriptor[list.size()];
for (int i = 0; i < list.size(); i++) {
String name = list.get(i);
FileDescriptorProto file = findFileByName(input, name);
if (file == null) {
throw new IllegalStateException("Missing dependency: " + name);
}
try {
deps[i] = FileDescriptor.buildFrom(file, dependencies(input, file.getDependencyList()));
}
catch (DescriptorValidationException e) {
throw new IllegalStateException("Invalid descriptor: " + file.getName(), e);
}
}
return deps;
}

private FileDescriptorProto findFileByName(FileDescriptorSet input, String name) {
for (FileDescriptorProto file : input.getFileList()) {
if (file.getName().equals(name)) {
return file;
}
}
return null;
}

private ManagedChannel createChannel() {
URI requestURI = ((Route) exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR)).getUri();
return createChannelChannel(requestURI.getHost(), requestURI.getPort());
Expand All @@ -270,8 +276,9 @@ private ManagedChannel createChannel() {
private Function<JsonNode, DynamicMessage> callGRPCServer() {
return jsonRequest -> {
try {
byte[] request = objectWriter.writeValueAsBytes(jsonRequest);
return ClientCalls.blockingUnaryCall(clientCall, DynamicMessage.parseFrom(descriptor, request));
DynamicMessage.Builder builder = DynamicMessage.newBuilder(descriptor);
JsonFormat.parser().merge(jsonRequest.toString(), builder);
return ClientCalls.blockingUnaryCall(clientCall, builder.build());
}
catch (IOException e) {
throw new RuntimeException(e);
Expand All @@ -282,7 +289,8 @@ private Function<JsonNode, DynamicMessage> callGRPCServer() {
private Function<DynamicMessage, Object> serialiseGRPCResponse() {
return gRPCResponse -> {
try {
return objectReader.readValue(gRPCResponse.toByteArray());
return objectReader
.readValue(JsonFormat.printer().omittingInsignificantWhitespace().print(gRPCResponse));
}
catch (IOException e) {
throw new RuntimeException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -284,16 +284,13 @@ public GatewayFilterSpec circuitBreaker(Consumer<SpringCloudCircuitBreakerFilter

/**
* A filter that transforms a JSON request into a gRPC one.
* @param protoDescriptor relative path to the proto descriptor file.
* @param protoFile relative path to the proto definition file.
* @param service fully qualified name of the service that will handle the request.
* @param method method name in the service that will handle the request.
* @param protoDescriptor relative path to the proto descriptor file.
*/
public GatewayFilterSpec jsonToGRPC(String protoDescriptor, String protoFile, String service, String method) {
return filter(getBean(JsonToGrpcGatewayFilterFactory.class).apply(c -> c.setMethod(method)
.setProtoDescriptor(protoDescriptor)
.setProtoFile(protoFile)
.setService(service)));
public GatewayFilterSpec jsonToGRPC(String service, String method, String protoDescriptor) {
return filter(getBean(JsonToGrpcGatewayFilterFactory.class)
.apply(c -> c.setMethod(method).setProtoDescriptor(protoDescriptor).setService(service)));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@
org.springframework.cloud.gateway.route.RouteTests.class,
org.springframework.cloud.gateway.route.CachingRouteLocatorTests.class,
org.springframework.cloud.gateway.route.RouteRefreshListenerTests.class,
org.springframework.cloud.gateway.route.builder.RouteDslTests.class,
org.springframework.cloud.gateway.route.builder.RouteBuilderTests.class,
org.springframework.cloud.gateway.route.builder.GatewayFilterSpecTests.class,
org.springframework.cloud.gateway.route.CachingRouteDefinitionLocatorTests.class,
Expand Down
Loading