From 19d54a36a9b8957323fc7685adefa9b87db1cd8c Mon Sep 17 00:00:00 2001 From: Yechan Lim Date: Mon, 4 May 2026 18:09:14 +0900 Subject: [PATCH 1/2] Cache ManagedChannel in JsonToGrpcGatewayFilter Fixes gh-4164 Signed-off-by: Yechan Lim --- .../JsonToGrpcGatewayFilterFactory.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/JsonToGrpcGatewayFilterFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/JsonToGrpcGatewayFilterFactory.java index 5cb5059c0..914e2eb4c 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/JsonToGrpcGatewayFilterFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/JsonToGrpcGatewayFilterFactory.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.function.Function; import javax.net.ssl.SSLException; @@ -89,6 +91,8 @@ public class JsonToGrpcGatewayFilterFactory private final ResourceLoader resourceLoader; + private final ConcurrentMap managedChannelCache = new ConcurrentHashMap<>(); + public JsonToGrpcGatewayFilterFactory(GrpcSslConfigurer grpcSslConfigurer, ResourceLoader resourceLoader) { super(Config.class); this.grpcSslConfigurer = grpcSslConfigurer; @@ -122,6 +126,19 @@ public String toString() { return new OrderedGatewayFilter(filter, order); } + private ManagedChannel createChannelChannel(String host, int port) { + String key = host + ":" + port; + return managedChannelCache.computeIfAbsent(key, k -> { + NettyChannelBuilder builder = NettyChannelBuilder.forAddress(host, port); + try { + return grpcSslConfigurer.configureSsl(builder); + } + catch (SSLException e) { + throw new RuntimeException(e); + } + }); + } + public static class Config { private @Nullable String protoDescriptor; @@ -317,17 +334,6 @@ private Function wrapGRPCResponse() { .wrap(Objects.requireNonNull(new ObjectMapper().writeValueAsBytes(jsonResponse))); } - // We are creating this on every call, should optimize? - private ManagedChannel createChannelChannel(String host, int port) { - NettyChannelBuilder nettyChannelBuilder = NettyChannelBuilder.forAddress(host, port); - try { - return grpcSslConfigurer.configureSsl(nettyChannelBuilder); - } - catch (SSLException e) { - throw new RuntimeException(e); - } - } - } } From 9a135544a68079a3b8d53e033f73285e71a30a2f Mon Sep 17 00:00:00 2001 From: Yechan Lim Date: Wed, 13 May 2026 10:42:40 +0900 Subject: [PATCH 2/2] Cache GrpcCallContext in JsonToGrpcGatewayFilter Fixes gh-4164 Signed-off-by: Yechan Lim --- .../JsonToGrpcGatewayFilterFactory.java | 129 ++++++++++-------- 1 file changed, 74 insertions(+), 55 deletions(-) diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/JsonToGrpcGatewayFilterFactory.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/JsonToGrpcGatewayFilterFactory.java index 914e2eb4c..bd0bc923c 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/JsonToGrpcGatewayFilterFactory.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/factory/JsonToGrpcGatewayFilterFactory.java @@ -45,7 +45,6 @@ import io.grpc.netty.NettyChannelBuilder; import io.grpc.protobuf.ProtoUtils; import io.grpc.stub.ClientCalls; -import io.netty.buffer.PooledByteBufAllocator; import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -68,7 +67,6 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.codec.json.JacksonJsonDecoder; import org.springframework.http.server.reactive.ServerHttpResponseDecorator; import org.springframework.web.server.ServerWebExchange; @@ -106,10 +104,12 @@ public List shortcutFieldOrder() { @Override public GatewayFilter apply(Config config) { + GrpcCallContext callContext = new GrpcCallContext(config); + GatewayFilter filter = new GatewayFilter() { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { - GRPCResponseDecorator modifiedResponse = new GRPCResponseDecorator(exchange, config); + GRPCResponseDecorator modifiedResponse = new GRPCResponseDecorator(exchange, callContext); ServerWebExchangeUtils.setAlreadyRouted(exchange); return modifiedResponse.writeWith(exchange.getRequest().getBody()) @@ -176,68 +176,55 @@ public Config setMethod(String method) { } - class GRPCResponseDecorator extends ServerHttpResponseDecorator { + class GrpcCallContext { - private final ServerWebExchange exchange; + final Descriptors.Descriptor descriptor; - private final Descriptors.Descriptor descriptor; + final MethodDescriptor methodDescriptor; - private final ObjectReader objectReader; + final ObjectMapper objectMapper; - private final ClientCall clientCall; + final ObjectReader objectReader; - private final ObjectNode objectNode; + final ObjectNode objectNode; - GRPCResponseDecorator(ServerWebExchange exchange, Config config) { - super(exchange.getResponse()); - this.exchange = exchange; - try { - Descriptors.MethodDescriptor methodDescriptor = getMethodDescriptor(config); - Descriptors.ServiceDescriptor serviceDescriptor = methodDescriptor.getService(); - Descriptors.Descriptor outputType = methodDescriptor.getOutputType(); - this.descriptor = methodDescriptor.getInputType(); + final JsonFormat.Parser jsonToProtoParser; - clientCall = createClientCallForType(config, serviceDescriptor, outputType); + final JsonFormat.Printer protoToJsonPrinter; - ObjectMapper objectMapper = JsonMapper.builder() - .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) - .build(); + GrpcCallContext(Config config) { + try { + Descriptors.MethodDescriptor protoMethodDescriptor = getMethodDescriptor(config); + Descriptors.ServiceDescriptor serviceDescriptor = protoMethodDescriptor.getService(); + Descriptors.Descriptor outputType = protoMethodDescriptor.getOutputType(); + this.descriptor = protoMethodDescriptor.getInputType(); + + MethodDescriptor.Marshaller marshaller = ProtoUtils + .marshaller(DynamicMessage.newBuilder(outputType).build()); + + methodDescriptor = MethodDescriptor + .newBuilder() + .setType(MethodDescriptor.MethodType.UNKNOWN) + .setFullMethodName( + MethodDescriptor.generateFullMethodName(serviceDescriptor.getFullName(), config.getMethod())) + .setRequestMarshaller(marshaller) + .setResponseMarshaller(marshaller) + .build(); + + jsonToProtoParser = JsonFormat.parser(); + protoToJsonPrinter = JsonFormat.printer().omittingInsignificantWhitespace(); + + objectMapper = JsonMapper.builder() + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .build(); objectReader = objectMapper.readerFor(JsonNode.class); objectNode = objectMapper.createObjectNode(); - } catch (IOException | Descriptors.DescriptorValidationException e) { throw new RuntimeException(e); } } - @Override - public Mono writeWith(Publisher body) { - exchange.getResponse().getHeaders().set("Content-Type", "application/json"); - - return getDelegate().writeWith(deserializeJSONRequest().map(callGRPCServer()) - .map(serialiseGRPCResponse()) - .map(wrapGRPCResponse()) - .cast(DataBuffer.class) - .last()); - } - - private ClientCall createClientCallForType(Config config, - Descriptors.ServiceDescriptor serviceDescriptor, Descriptors.Descriptor outputType) { - MethodDescriptor.Marshaller marshaller = ProtoUtils - .marshaller(DynamicMessage.newBuilder(outputType).build()); - MethodDescriptor methodDescriptor = MethodDescriptor - .newBuilder() - .setType(MethodDescriptor.MethodType.UNKNOWN) - .setFullMethodName( - MethodDescriptor.generateFullMethodName(serviceDescriptor.getFullName(), config.getMethod())) - .setRequestMarshaller(marshaller) - .setResponseMarshaller(marshaller) - .build(); - Channel channel = createChannel(); - return channel.newCall(methodDescriptor, CallOptions.DEFAULT); - } - private Descriptors.MethodDescriptor getMethodDescriptor(Config config) throws IOException, Descriptors.DescriptorValidationException { Objects.requireNonNull(config.getProtoDescriptor(), "Proto Descriptor must not be null"); @@ -288,6 +275,36 @@ private FileDescriptor[] dependencies(FileDescriptorSet input, ProtocolStringLis return null; } + } + + class GRPCResponseDecorator extends ServerHttpResponseDecorator { + + private final ServerWebExchange exchange; + + private final GrpcCallContext ctx; + + GRPCResponseDecorator(ServerWebExchange exchange, GrpcCallContext ctx) { + super(exchange.getResponse()); + this.exchange = exchange; + this.ctx = ctx; + } + + @Override + public Mono writeWith(Publisher body) { + exchange.getResponse().getHeaders().set("Content-Type", "application/json"); + + return getDelegate().writeWith(deserializeJSONRequest().map(callGRPCServer()) + .map(serialiseGRPCResponse()) + .map(wrapGRPCResponse()) + .cast(DataBuffer.class) + .last()); + } + + private ClientCall createClientCallForType(MethodDescriptor methodDescriptor) { + Channel channel = createChannel(); + return channel.newCall(methodDescriptor, CallOptions.DEFAULT); + } + private ManagedChannel createChannel() { Route route = (Route) exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); URI requestURI = Objects.requireNonNull(route, "Route not found in exchange attributes").getUri(); @@ -297,8 +314,10 @@ private ManagedChannel createChannel() { private Function callGRPCServer() { return jsonRequest -> { try { - DynamicMessage.Builder builder = DynamicMessage.newBuilder(descriptor); - JsonFormat.parser().merge(jsonRequest.toString(), builder); + ClientCall clientCall = createClientCallForType(ctx.methodDescriptor); + + DynamicMessage.Builder builder = DynamicMessage.newBuilder(ctx.descriptor); + ctx.jsonToProtoParser.merge(jsonRequest.toString(), builder); return ClientCalls.blockingUnaryCall(clientCall, builder.build()); } catch (IOException e) { @@ -310,8 +329,8 @@ private Function callGRPCServer() { private Function serialiseGRPCResponse() { return gRPCResponse -> { try { - return objectReader - .readValue(JsonFormat.printer().omittingInsignificantWhitespace().print(gRPCResponse)); + return ctx.objectReader + .readValue(ctx.protoToJsonPrinter.print(gRPCResponse)); } catch (IOException e) { throw new RuntimeException(e); @@ -322,7 +341,7 @@ private Function serialiseGRPCResponse() { private Flux deserializeJSONRequest() { return exchange.getRequest().getBody().mapNotNull(dataBufferBody -> { if (dataBufferBody.capacity() == 0) { - return objectNode; + return ctx.objectNode; } ResolvableType targetType = ResolvableType.forType(JsonNode.class); return new JacksonJsonDecoder().decode(dataBufferBody, targetType, null, null); @@ -330,7 +349,7 @@ private Flux deserializeJSONRequest() { } private Function wrapGRPCResponse() { - return jsonResponse -> new NettyDataBufferFactory(new PooledByteBufAllocator()) + return jsonResponse -> exchange.getResponse().bufferFactory() .wrap(Objects.requireNonNull(new ObjectMapper().writeValueAsBytes(jsonResponse))); }