Merge branch '3.1.x'
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
|spring.cloud.gateway.filter.add-request-header.enabled | `+++true+++` | Enables the add-request-header filter.
|
||||
|spring.cloud.gateway.filter.add-request-parameter.enabled | `+++true+++` | Enables the add-request-parameter filter.
|
||||
|spring.cloud.gateway.filter.add-response-header.enabled | `+++true+++` | Enables the add-response-header filter.
|
||||
|spring.cloud.gateway.filter.json-to-grpc.enabled | `+++true+++` | Enables the JSON to gRPC filter.
|
||||
|spring.cloud.gateway.filter.circuit-breaker.enabled | `+++true+++` | Enables the circuit-breaker filter.
|
||||
|spring.cloud.gateway.filter.dedupe-response-header.enabled | `+++true+++` | Enables the dedupe-response-header filter.
|
||||
|spring.cloud.gateway.filter.fallback-headers.enabled | `+++true+++` | Enables the fallback-headers filter.
|
||||
@@ -25,7 +26,7 @@
|
||||
|spring.cloud.gateway.filter.preserve-host-header.enabled | `+++true+++` | Enables the preserve-host-header filter.
|
||||
|spring.cloud.gateway.filter.redirect-to.enabled | `+++true+++` | Enables the redirect-to filter.
|
||||
|spring.cloud.gateway.filter.remove-hop-by-hop.headers | |
|
||||
|spring.cloud.gateway.filter.remove-hop-by-hop.order | `+++0+++` |
|
||||
|spring.cloud.gateway.filter.remove-hop-by-hop.order | `+++0+++` |
|
||||
|spring.cloud.gateway.filter.remove-request-header.enabled | `+++true+++` | Enables the remove-request-header filter.
|
||||
|spring.cloud.gateway.filter.remove-request-parameter.enabled | `+++true+++` | Enables the remove-request-parameter filter.
|
||||
|spring.cloud.gateway.filter.remove-response-header.enabled | `+++true+++` | Enables the remove-response-header filter.
|
||||
@@ -41,16 +42,16 @@
|
||||
|spring.cloud.gateway.filter.rewrite-path.enabled | `+++true+++` | Enables the rewrite-path filter.
|
||||
|spring.cloud.gateway.filter.rewrite-response-header.enabled | `+++true+++` | Enables the rewrite-response-header filter.
|
||||
|spring.cloud.gateway.filter.save-session.enabled | `+++true+++` | Enables the save-session filter.
|
||||
|spring.cloud.gateway.filter.secure-headers.content-security-policy | `+++default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.content-type-options | `+++nosniff+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.content-security-policy | `+++default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.content-type-options | `+++nosniff+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.disable | |
|
||||
|spring.cloud.gateway.filter.secure-headers.download-options | `+++noopen+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.download-options | `+++noopen+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.enabled | `+++true+++` | Enables the secure-headers filter.
|
||||
|spring.cloud.gateway.filter.secure-headers.frame-options | `+++DENY+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.permitted-cross-domain-policies | `+++none+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.referrer-policy | `+++no-referrer+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.strict-transport-security | `+++max-age=631138519+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.xss-protection-header | `+++1 ; mode=block+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.frame-options | `+++DENY+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.permitted-cross-domain-policies | `+++none+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.referrer-policy | `+++no-referrer+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.strict-transport-security | `+++max-age=631138519+++` |
|
||||
|spring.cloud.gateway.filter.secure-headers.xss-protection-header | `+++1 ; mode=block+++` |
|
||||
|spring.cloud.gateway.filter.set-path.enabled | `+++true+++` | Enables the set-path filter.
|
||||
|spring.cloud.gateway.filter.set-request-header.enabled | `+++true+++` | Enables the set-request-header filter.
|
||||
|spring.cloud.gateway.filter.set-request-host-header.enabled | `+++true+++` | Enables the set-request-host-header filter.
|
||||
@@ -105,7 +106,7 @@
|
||||
|spring.cloud.gateway.httpclient.websocket.proxy-ping | `+++true+++` | Proxy ping frames to downstream services, defaults to true.
|
||||
|spring.cloud.gateway.httpclient.wiretap | `+++false+++` | Enables wiretap debugging for Netty HttpClient.
|
||||
|spring.cloud.gateway.httpserver.wiretap | `+++false+++` | Enables wiretap debugging for Netty HttpServer.
|
||||
|spring.cloud.gateway.loadbalancer.use404 | `+++false+++` |
|
||||
|spring.cloud.gateway.loadbalancer.use404 | `+++false+++` |
|
||||
|spring.cloud.gateway.metrics.enabled | `+++false+++` | Enables the collection of metrics data.
|
||||
|spring.cloud.gateway.metrics.prefix | `+++spring.cloud.gateway+++` | The prefix of all metrics emitted by gateway.
|
||||
|spring.cloud.gateway.metrics.tags | | Tags map that added to metrics.
|
||||
|
||||
@@ -1830,6 +1830,86 @@ spring:
|
||||
|
||||
NOTE: This filter only works with http request (including https).
|
||||
|
||||
=== The `JsonToGrpc` `GatewayFilter` Factory
|
||||
|
||||
The JSONToGRPCFilter GatewayFilter Factory converts a JSON payload to a gRPC request.
|
||||
|
||||
The filter takes the following arguments:
|
||||
|
||||
* `protoDescriptor` Proto descriptor file.
|
||||
|
||||
This file can be generated using `protoc` specifying the `--descriptor_set_out` flag:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
protoc --proto_path=src/main/resources/proto/ \
|
||||
--descriptor_set_out=src/main/resources/proto/hello.pb \
|
||||
src/main/resources/proto/hello.proto
|
||||
----
|
||||
|
||||
* `protoFile` Proto definition file.
|
||||
|
||||
* `service` Fully qualified name of the service that will handle the request.
|
||||
|
||||
* `method` Method name in the service that will handle the request.
|
||||
|
||||
NOTE: `streaming` is not supported.
|
||||
|
||||
|
||||
*application.yml.*
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
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);
|
||||
}).uri(uri))
|
||||
----
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
spring:
|
||||
cloud:
|
||||
gateway:
|
||||
routes:
|
||||
- id: json-grpc
|
||||
uri: https://localhost:6565/testhello
|
||||
predicates:
|
||||
- Path=/json/**
|
||||
filters:
|
||||
- name: JsonToGrpc
|
||||
args:
|
||||
protoDescriptor: file:proto/hello.pb
|
||||
protoFile: file:proto/hello.proto
|
||||
service: com.example.grpcserver.hello.HelloService
|
||||
method: hello
|
||||
|
||||
----
|
||||
|
||||
When a request is made through the gateway to `/json/hello` the request will be transformed using the definition provided in `hello.proto`, sent to `com.example.grpcserver.hello.HelloService/hello`, and transform the response back to JSON.
|
||||
|
||||
By default, it will create a `NettyChannel` using the default `TrustManagerFactory`. However, this `TrustManager` can be customized by creating a bean of type `GRPCSSLContext`:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
|
||||
@Configuration
|
||||
public class GRPCLocalConfiguration {
|
||||
@Bean
|
||||
public GRPCSSLContext sslContext() {
|
||||
TrustManager trustManager = trustAllCerts();
|
||||
return new GRPCSSLContext(trustManager);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
=== Default Filters
|
||||
|
||||
To add a filter and apply it to all routes, you can use `spring.cloud.gateway.default-filters`.
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<description>Spring Cloud Gateway gRPC Integration Test</description>
|
||||
|
||||
<properties>
|
||||
<grpc.version>1.47.0</grpc.version>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
@@ -29,26 +30,38 @@
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-netty-shaded</artifactId>
|
||||
<version>1.44.0</version>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<version>1.44.0</version>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
<version>1.44.0</version>
|
||||
<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>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.netty</groupId>
|
||||
<artifactId>netty-tcnative-boringssl-static</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>annotations-api</artifactId>
|
||||
|
||||
@@ -18,8 +18,13 @@ package org.springframework.cloud.gateway.tests.grpc;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import io.grpc.Grpc;
|
||||
import io.grpc.Server;
|
||||
import io.grpc.ServerCredentials;
|
||||
@@ -31,6 +36,7 @@ import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.cloud.gateway.config.GRPCSSLContext;
|
||||
import org.springframework.cloud.gateway.route.RouteLocator;
|
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
|
||||
import org.springframework.cloud.test.TestSocketUtils;
|
||||
@@ -53,8 +59,31 @@ public class GRPCApplication {
|
||||
|
||||
@Bean
|
||||
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
|
||||
return builder.routes().route("grpc", r -> r.predicate(p -> true).uri("https://localhost:" + GRPC_SERVER_PORT))
|
||||
.build();
|
||||
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);
|
||||
}).uri("https://localhost:" + GRPC_SERVER_PORT))
|
||||
.route("grpc", r -> r.predicate(p -> true).uri("https://localhost:" + GRPC_SERVER_PORT)).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public GRPCSSLContext sslContext() throws SSLException {
|
||||
TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return new X509Certificate[0];
|
||||
}
|
||||
|
||||
public void checkClientTrusted(X509Certificate[] certs, String authType) {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] certs, String authType) {
|
||||
}
|
||||
} };
|
||||
|
||||
return new GRPCSSLContext(trustAllCerts[0]);
|
||||
}
|
||||
|
||||
@Component
|
||||
@@ -74,7 +103,7 @@ public class GRPCApplication {
|
||||
server = Grpc.newServerBuilderForPort(GRPC_SERVER_PORT, creds).addService(new HelloService()).build()
|
||||
.start();
|
||||
|
||||
System.out.println("Starting server in port " + GRPC_SERVER_PORT);
|
||||
System.out.println("Starting gRPC server in port " + GRPC_SERVER_PORT);
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
try {
|
||||
@@ -104,7 +133,7 @@ public class GRPCApplication {
|
||||
public void hello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
|
||||
|
||||
String greeting = "Hello, " + request.getFirstName() + " " + request.getLastName();
|
||||
System.out.println(greeting);
|
||||
System.out.println("Sending response: " + greeting);
|
||||
|
||||
HelloResponse response = HelloResponse.newBuilder().setGreeting(greeting).build();
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
|
||||
<EFBFBD>
|
||||
hello.proto"H
|
||||
HelloRequest
|
||||
firstName ( R firstName
|
||||
lastName ( RlastName"+
|
||||
|
||||
HelloResponse
|
||||
greeting ( Rgreeting26
|
||||
HelloService&
|
||||
hello
|
||||
@@ -3,12 +3,12 @@ option java_multiple_files = true;
|
||||
option java_package = "org.springframework.cloud.gateway.tests.grpc";
|
||||
|
||||
message HelloRequest {
|
||||
string firstName = 1;
|
||||
string lastName = 2;
|
||||
optional string firstName = 1;
|
||||
optional string lastName = 2;
|
||||
}
|
||||
|
||||
message HelloResponse {
|
||||
string greeting = 1;
|
||||
optional string greeting = 1;
|
||||
}
|
||||
|
||||
service HelloService {
|
||||
|
||||
@@ -27,10 +27,8 @@ import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
|
||||
@@ -40,7 +38,6 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
|
||||
/**
|
||||
* @author Alberto C. Ríos
|
||||
*/
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
@SpringBootTest(classes = GRPCApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
@DirtiesContext
|
||||
public class GRPCApplicationTests {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.gateway.tests.grpc;
|
||||
|
||||
import java.security.KeyManagementException;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.http.config.Registry;
|
||||
import org.apache.http.config.RegistryBuilder;
|
||||
import org.apache.http.conn.socket.ConnectionSocketFactory;
|
||||
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
|
||||
import org.apache.http.conn.ssl.NoopHostnameVerifier;
|
||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
|
||||
import org.apache.http.conn.ssl.TrustStrategy;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
|
||||
import org.apache.http.ssl.SSLContexts;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
|
||||
/**
|
||||
* @author Alberto C. Ríos
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
public class JsonToGrpcApplicationTests {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
restTemplate = createUnsecureClient();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldConvertFromJSONToGRPC() {
|
||||
String response = restTemplate.postForEntity("https://localhost:" + port + "/json/hello",
|
||||
"{\"firstName\":\"Duff\", \"lastName\":\"McKagan\"}", String.class).getBody();
|
||||
|
||||
Assertions.assertThat(response).isNotNull();
|
||||
Assertions.assertThat(response).contains("{\"greeting\":\"Hello, Duff McKagan\"}");
|
||||
}
|
||||
|
||||
private RestTemplate createUnsecureClient() {
|
||||
TrustStrategy acceptingTrustStrategy = (cert, authType) -> true;
|
||||
SSLContext sslContext;
|
||||
try {
|
||||
sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
|
||||
}
|
||||
catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
SSLConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory(sslContext,
|
||||
NoopHostnameVerifier.INSTANCE);
|
||||
|
||||
Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
|
||||
.register("https", sslSocketFactory).register("http", new PlainConnectionSocketFactory()).build();
|
||||
|
||||
BasicHttpClientConnectionManager connectionManager = new BasicHttpClientConnectionManager(
|
||||
socketFactoryRegistry);
|
||||
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(sslSocketFactory)
|
||||
.setConnectionManager(connectionManager).build();
|
||||
|
||||
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||
|
||||
return new RestTemplate(requestFactory);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,4 +24,4 @@ spring:
|
||||
logging:
|
||||
level:
|
||||
reactor.netty: DEBUG
|
||||
org.springframework.cloud.gateway.filter: TRACE
|
||||
org.springframework.cloud.gateway.filter: DEBUG
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<description>Spring Cloud Gateway Server</description>
|
||||
<properties>
|
||||
<main.basedir>${basedir}/..</main.basedir>
|
||||
<grpc.version>1.47.0</grpc.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -81,6 +82,34 @@
|
||||
<groupId>io.projectreactor.addons</groupId>
|
||||
<artifactId>reactor-extra</artifactId>
|
||||
</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>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-netty-shaded</artifactId>
|
||||
<optional>true</optional>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-protobuf</artifactId>
|
||||
<optional>true</optional>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.grpc</groupId>
|
||||
<artifactId>grpc-stub</artifactId>
|
||||
<optional>true</optional>
|
||||
<version>${grpc.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure-processor</artifactId>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.gateway.config;
|
||||
|
||||
import javax.net.ssl.TrustManager;
|
||||
|
||||
/**
|
||||
* @author Alberto C. Ríos
|
||||
*/
|
||||
public class GRPCSSLContext {
|
||||
|
||||
private final TrustManager trustManager;
|
||||
|
||||
public GRPCSSLContext(TrustManager trustManager) {
|
||||
this.trustManager = trustManager;
|
||||
}
|
||||
|
||||
public TrustManager getTrustManager() {
|
||||
return trustManager;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,10 +16,16 @@
|
||||
|
||||
package org.springframework.cloud.gateway.config;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
import io.grpc.Channel;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -68,6 +74,7 @@ import org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGateway
|
||||
import org.springframework.cloud.gateway.filter.factory.CacheRequestBodyGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.JsonToGrpcGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory;
|
||||
@@ -147,6 +154,7 @@ import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;
|
||||
import org.springframework.security.oauth2.client.ReactiveOAuth2AuthorizedClientManager;
|
||||
@@ -292,6 +300,27 @@ public class GatewayAutoConfiguration {
|
||||
return new GRPCResponseHeadersFilter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnEnabledFilter
|
||||
@ConditionalOnProperty(name = "server.http2.enabled", matchIfMissing = true)
|
||||
@ConditionalOnClass(Channel.class)
|
||||
public JsonToGrpcGatewayFilterFactory jsonToGRPCFilterFactory(GRPCSSLContext gRPCSSLContext,
|
||||
ResourceLoader resourceLoader) {
|
||||
return new JsonToGrpcGatewayFilterFactory(gRPCSSLContext, resourceLoader);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnEnabledFilter(JsonToGrpcGatewayFilterFactory.class)
|
||||
@ConditionalOnMissingBean(GRPCSSLContext.class)
|
||||
@ConditionalOnClass(Channel.class)
|
||||
public GRPCSSLContext gRPCSSLContext() throws KeyStoreException, NoSuchAlgorithmException {
|
||||
TrustManagerFactory trustManagerFactory = TrustManagerFactory
|
||||
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
trustManagerFactory.init(KeyStore.getInstance(KeyStore.getDefaultType()));
|
||||
|
||||
return new GRPCSSLContext(trustManagerFactory.getTrustManagers()[0]);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TransferEncodingNormalizationHeadersFilter transferEncodingNormalizationHeadersFilter() {
|
||||
return new TransferEncodingNormalizationHeadersFilter();
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
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;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
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.Descriptors;
|
||||
import com.google.protobuf.DynamicMessage;
|
||||
import io.grpc.CallOptions;
|
||||
import io.grpc.Channel;
|
||||
import io.grpc.ClientCall;
|
||||
import io.grpc.ManagedChannel;
|
||||
import io.grpc.MethodDescriptor;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts;
|
||||
import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder;
|
||||
import io.grpc.protobuf.ProtoUtils;
|
||||
import io.grpc.stub.ClientCalls;
|
||||
import io.netty.buffer.PooledByteBufAllocator;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.gateway.config.GRPCSSLContext;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilter;
|
||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
||||
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
|
||||
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
|
||||
import org.springframework.cloud.gateway.route.Route;
|
||||
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
|
||||
import org.springframework.core.ResolvableType;
|
||||
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.Jackson2JsonDecoder;
|
||||
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static io.grpc.netty.shaded.io.grpc.netty.NegotiationType.TLS;
|
||||
import static org.springframework.cloud.gateway.support.GatewayToStringStyler.filterToStringCreator;
|
||||
|
||||
/**
|
||||
* This filter takes a JSON payload, transform it into a protobuf object, send it to a
|
||||
* given gRPC channel, and transform the response back to JSON.
|
||||
*
|
||||
* Making it transparent for the consumer that the service under the gateway is a gRPC
|
||||
* one.
|
||||
*
|
||||
* @author Alberto C. Ríos
|
||||
*/
|
||||
public class JsonToGrpcGatewayFilterFactory
|
||||
extends AbstractGatewayFilterFactory<JsonToGrpcGatewayFilterFactory.Config> {
|
||||
|
||||
private final GRPCSSLContext sslContext;
|
||||
|
||||
private final ResourceLoader resourceLoader;
|
||||
|
||||
public JsonToGrpcGatewayFilterFactory(GRPCSSLContext sslContext, ResourceLoader resourceLoader) {
|
||||
super(Config.class);
|
||||
this.sslContext = sslContext;
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> shortcutFieldOrder() {
|
||||
return Arrays.asList("protoDescriptor", "protoFile", "service", "method");
|
||||
}
|
||||
|
||||
@Override
|
||||
public GatewayFilter apply(Config config) {
|
||||
GatewayFilter filter = new GatewayFilter() {
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
GRPCResponseDecorator modifiedResponse = new GRPCResponseDecorator(exchange, config);
|
||||
|
||||
ServerWebExchangeUtils.setAlreadyRouted(exchange);
|
||||
return modifiedResponse.writeWith(exchange.getRequest().getBody())
|
||||
.then(chain.filter(exchange.mutate().response(modifiedResponse).build()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return filterToStringCreator(JsonToGrpcGatewayFilterFactory.this).toString();
|
||||
}
|
||||
};
|
||||
|
||||
int order = NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
|
||||
return new OrderedGatewayFilter(filter, order);
|
||||
}
|
||||
|
||||
public static class Config {
|
||||
|
||||
private String protoDescriptor;
|
||||
|
||||
private String protoFile;
|
||||
|
||||
private String service;
|
||||
|
||||
private String method;
|
||||
|
||||
public String getProtoDescriptor() {
|
||||
return protoDescriptor;
|
||||
}
|
||||
|
||||
public Config setProtoDescriptor(String protoDescriptor) {
|
||||
this.protoDescriptor = protoDescriptor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getProtoFile() {
|
||||
return protoFile;
|
||||
}
|
||||
|
||||
public Config setProtoFile(String protoFile) {
|
||||
this.protoFile = protoFile;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getService() {
|
||||
return service;
|
||||
}
|
||||
|
||||
public Config setService(String service) {
|
||||
this.service = service;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public Config setMethod(String method) {
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class GRPCResponseDecorator extends ServerHttpResponseDecorator {
|
||||
|
||||
private final ServerWebExchange exchange;
|
||||
|
||||
private final Descriptors.Descriptor descriptor;
|
||||
|
||||
private final ObjectWriter objectWriter;
|
||||
|
||||
private final ObjectReader objectReader;
|
||||
|
||||
private final ClientCall<DynamicMessage, DynamicMessage> clientCall;
|
||||
|
||||
private final ObjectNode objectNode;
|
||||
|
||||
GRPCResponseDecorator(ServerWebExchange exchange, Config config) {
|
||||
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.Descriptor outputType = getOutputTypeDescriptor(config, descriptorFile.getInputStream());
|
||||
|
||||
clientCall = createClientCallForType(config, outputType);
|
||||
|
||||
ProtobufSchema schema = ProtobufSchemaLoader.std.load(protoFile.getInputStream());
|
||||
ProtobufSchema responseType = schema.withRootType(outputType.getName());
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper(new ProtobufFactory());
|
||||
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||
objectWriter = objectMapper.writer(schema);
|
||||
objectReader = objectMapper.readerFor(JsonNode.class).with(responseType);
|
||||
objectNode = objectMapper.createObjectNode();
|
||||
|
||||
}
|
||||
catch (IOException | Descriptors.DescriptorValidationException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeWith(Publisher<? extends DataBuffer> 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<DynamicMessage, DynamicMessage> createClientCallForType(Config config,
|
||||
Descriptors.Descriptor outputType) {
|
||||
MethodDescriptor.Marshaller<DynamicMessage> marshaller = ProtoUtils
|
||||
.marshaller(DynamicMessage.newBuilder(outputType).build());
|
||||
MethodDescriptor<DynamicMessage, DynamicMessage> methodDescriptor = MethodDescriptor
|
||||
.<DynamicMessage, DynamicMessage>newBuilder().setType(MethodDescriptor.MethodType.UNKNOWN)
|
||||
.setFullMethodName(MethodDescriptor.generateFullMethodName(config.getService(), config.getMethod()))
|
||||
.setRequestMarshaller(marshaller).setResponseMarshaller(marshaller).build();
|
||||
Channel channel = createChannel();
|
||||
return channel.newCall(methodDescriptor, CallOptions.DEFAULT);
|
||||
}
|
||||
|
||||
private Descriptors.Descriptor getOutputTypeDescriptor(Config config, InputStream descriptorFile)
|
||||
throws IOException, Descriptors.DescriptorValidationException {
|
||||
DescriptorProtos.FileDescriptorSet fileDescriptorSet = DescriptorProtos.FileDescriptorSet
|
||||
.parseFrom(descriptorFile);
|
||||
DescriptorProtos.FileDescriptorProto fileProto = fileDescriptorSet.getFile(0);
|
||||
Descriptors.FileDescriptor fileDescriptor = Descriptors.FileDescriptor.buildFrom(fileProto,
|
||||
new Descriptors.FileDescriptor[0]);
|
||||
|
||||
List<Descriptors.MethodDescriptor> methods = fileDescriptor.findServiceByName(config.getService())
|
||||
.getMethods();
|
||||
|
||||
return methods.stream().filter(method -> method.getName().equals(config.getMethod())).findFirst()
|
||||
.orElseThrow(() -> new NoSuchElementException("No Method found")).getOutputType();
|
||||
}
|
||||
|
||||
private ManagedChannel createChannel() {
|
||||
URI requestURI = ((Route) exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR)).getUri();
|
||||
return createChannelChannel(requestURI.getHost(), requestURI.getPort());
|
||||
}
|
||||
|
||||
private Function<JsonNode, DynamicMessage> callGRPCServer() {
|
||||
return jsonRequest -> {
|
||||
try {
|
||||
byte[] request = objectWriter.writeValueAsBytes(jsonRequest);
|
||||
return ClientCalls.blockingUnaryCall(clientCall, DynamicMessage.parseFrom(descriptor, request));
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Function<DynamicMessage, Object> serialiseGRPCResponse() {
|
||||
return gRPCResponse -> {
|
||||
try {
|
||||
return objectReader.readValue(gRPCResponse.toByteArray());
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Flux<JsonNode> deserializeJSONRequest() {
|
||||
return exchange.getRequest().getBody().mapNotNull(dataBufferBody -> {
|
||||
if (dataBufferBody.capacity() == 0) {
|
||||
return objectNode;
|
||||
}
|
||||
ResolvableType targetType = ResolvableType.forType(JsonNode.class);
|
||||
return new Jackson2JsonDecoder().decode(dataBufferBody, targetType, null, null);
|
||||
}).cast(JsonNode.class);
|
||||
}
|
||||
|
||||
private Function<Object, DataBuffer> wrapGRPCResponse() {
|
||||
return jsonResponse -> {
|
||||
try {
|
||||
return new NettyDataBufferFactory(new PooledByteBufAllocator())
|
||||
.wrap(Objects.requireNonNull(new ObjectMapper().writeValueAsBytes(jsonResponse)));
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
return new NettyDataBufferFactory(new PooledByteBufAllocator()).allocateBuffer();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private ManagedChannel createChannelChannel(String host, int port) {
|
||||
try {
|
||||
return NettyChannelBuilder.forAddress(host, port).useTransportSecurity()
|
||||
.sslContext(GrpcSslContexts.forClient().trustManager(sslContext.getTrustManager()).build())
|
||||
.negotiationType(TLS).build();
|
||||
}
|
||||
catch (SSLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import org.springframework.cloud.gateway.filter.factory.CacheRequestBodyGatewayF
|
||||
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory.Strategy;
|
||||
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.JsonToGrpcGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory;
|
||||
@@ -210,6 +211,18 @@ public class GatewayFilterSpec extends UriSpec {
|
||||
return filter(filterFactory.apply(this.routeBuilder.getId(), configConsumer));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps headers from one name to another.
|
||||
* @param fromHeader the header name of the original header.
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
"description": "Enables the add-response-header filter.",
|
||||
"defaultValue": "true"
|
||||
},
|
||||
{
|
||||
"name": "spring.cloud.gateway.filter.json-to-grpc.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Enables the JSON to gRPC filter.",
|
||||
"defaultValue": "true"
|
||||
},
|
||||
{
|
||||
"name": "spring.cloud.gateway.filter.modify-request-body.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
|
||||
@@ -82,6 +82,7 @@ public class DisableBuiltInFiltersTests {
|
||||
"spring.cloud.gateway.filter.map-request-header.enabled=false",
|
||||
"spring.cloud.gateway.filter.add-request-parameter.enabled=false",
|
||||
"spring.cloud.gateway.filter.add-response-header.enabled=false",
|
||||
"spring.cloud.gateway.filter.json-to-grpc.enabled=false",
|
||||
"spring.cloud.gateway.filter.modify-request-body.enabled=false",
|
||||
"spring.cloud.gateway.filter.dedupe-response-header.enabled=false",
|
||||
"spring.cloud.gateway.filter.modify-response-body.enabled=false",
|
||||
|
||||
@@ -30,6 +30,7 @@ import org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayF
|
||||
import org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.FallbackHeadersGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.JsonToGrpcGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.AfterRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.CloudFoundryRouteServiceRoutePredicateFactory;
|
||||
@@ -88,13 +89,14 @@ class NameUtilsTests {
|
||||
void shouldNormalizeFiltersNamesAsProperties() {
|
||||
List<Class<? extends GatewayFilterFactory<?>>> predicates = Arrays.asList(
|
||||
AddRequestHeaderGatewayFilterFactory.class, DedupeResponseHeaderGatewayFilterFactory.class,
|
||||
FallbackHeadersGatewayFilterFactory.class, MapRequestHeaderGatewayFilterFactory.class);
|
||||
FallbackHeadersGatewayFilterFactory.class, MapRequestHeaderGatewayFilterFactory.class,
|
||||
JsonToGrpcGatewayFilterFactory.class);
|
||||
|
||||
List<String> resultNames = predicates.stream().map(NameUtils::normalizeFilterFactoryNameAsProperty)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<String> expectedNames = Arrays.asList("add-request-header", "dedupe-response-header", "fallback-headers",
|
||||
"map-request-header");
|
||||
"map-request-header", "json-to-grpc");
|
||||
|
||||
assertThat(resultNames).isEqualTo(expectedNames);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user