diff --git a/docs/src/main/asciidoc/spring-cloud-gateway.adoc b/docs/src/main/asciidoc/spring-cloud-gateway.adoc index e20776e5..ff0a3581 100644 --- a/docs/src/main/asciidoc/spring-cloud-gateway.adoc +++ b/docs/src/main/asciidoc/spring-cloud-gateway.adoc @@ -158,7 +158,7 @@ This predicate extracts the URI template variables (like `sub` defined in the ex === Method Route Predicate Factory -The Method Route Predicate Factory takes one parameter: the HTTP method to match. +The Method Route Predicate Factory takes one or more parameters: the HTTP methods to match. .application.yml [source,yaml] @@ -170,10 +170,10 @@ spring: - id: method_route uri: https://example.org predicates: - - Method=GET + - Method=GET,POST ---- -This route would match if the request method was a `GET`. +This route would match if the request method was a `GET` or a `POST`. === Path Route Predicate Factory The Path Route Predicate Factory takes two parameters: a list of Spring `PathMatcher` patterns and an optional flag to `matchOptionalTrailingSeparator`. diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/MethodRoutePredicateFactory.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/MethodRoutePredicateFactory.java index 23249841..ffee8fd6 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/MethodRoutePredicateFactory.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/handler/predicate/MethodRoutePredicateFactory.java @@ -21,10 +21,14 @@ import java.util.List; import java.util.function.Predicate; import org.springframework.http.HttpMethod; +import org.springframework.validation.annotation.Validated; import org.springframework.web.server.ServerWebExchange; +import static java.util.Arrays.stream; + /** * @author Spencer Gibb + * @author Dennis Menge */ public class MethodRoutePredicateFactory extends AbstractRoutePredicateFactory { @@ -32,15 +36,26 @@ public class MethodRoutePredicateFactory /** * Method key. */ + @Deprecated public static final String METHOD_KEY = "method"; + /** + * Methods key. + */ + public static final String METHODS_KEY = "methods"; + public MethodRoutePredicateFactory() { super(Config.class); } @Override public List shortcutFieldOrder() { - return Arrays.asList(METHOD_KEY); + return Arrays.asList(METHODS_KEY); + } + + @Override + public ShortcutType shortcutType() { + return ShortcutType.GATHER_LIST; } @Override @@ -49,28 +64,42 @@ public class MethodRoutePredicateFactory @Override public boolean test(ServerWebExchange exchange) { HttpMethod requestMethod = exchange.getRequest().getMethod(); - return requestMethod == config.getMethod(); + return stream(config.getMethods()) + .anyMatch(httpMethod -> httpMethod == requestMethod); } @Override public String toString() { - return String.format("Method: %s", config.getMethod()); + return String.format("Methods: %s", Arrays.toString(config.getMethods())); } }; } + @Validated public static class Config { - private HttpMethod method; + private HttpMethod[] methods; + @Deprecated public HttpMethod getMethod() { - return method; + if (methods != null && methods.length > 0) { + return methods[0]; + } + return null; } + @Deprecated public void setMethod(HttpMethod method) { - this.method = method; + this.methods = new HttpMethod[] {method}; } + public HttpMethod[] getMethods() { + return methods; + } + + public void setMethods(HttpMethod... methods) { + this.methods = methods; + } } } diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java index 62d30a6b..4b8ee36f 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java @@ -40,6 +40,7 @@ import org.springframework.cloud.gateway.support.ipresolver.RemoteAddressResolve import org.springframework.http.HttpMethod; import org.springframework.web.server.ServerWebExchange; +import static java.util.Arrays.stream; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.toAsyncPredicate; /** @@ -147,22 +148,26 @@ public class PredicateSpec extends UriSpec { /** * A predicate that checks if the HTTP method matches - * @param method the name of the HTTP method + * @param methods the name of the HTTP methods * @return a {@link BooleanSpec} to be used to add logical operators */ - public BooleanSpec method(String method) { - return asyncPredicate(getBean(MethodRoutePredicateFactory.class) - .applyAsync(c -> c.setMethod(HttpMethod.resolve(method)))); + public BooleanSpec method(String... methods) { + return asyncPredicate(getBean(MethodRoutePredicateFactory.class).applyAsync(c -> { + HttpMethod[] httpMethods = stream(methods).map(HttpMethod::resolve) + .toArray(HttpMethod[]::new); + c.setMethods(httpMethods); + })); } /** * A predicate that checks if the HTTP method matches - * @param method the HTTP method + * @param methods the HTTP methods * @return a {@link BooleanSpec} to be used to add logical operators */ - public BooleanSpec method(HttpMethod method) { - return asyncPredicate(getBean(MethodRoutePredicateFactory.class) - .applyAsync(c -> c.setMethod(method))); + public BooleanSpec method(HttpMethod... methods) { + return asyncPredicate(getBean(MethodRoutePredicateFactory.class).applyAsync(c -> { + c.setMethods(methods); + })); } /** diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/MethodRoutePredicateFactoryTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/MethodRoutePredicateFactoryTests.java index 5a6652cd..6cfd3c38 100644 --- a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/MethodRoutePredicateFactoryTests.java +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/handler/predicate/MethodRoutePredicateFactoryTests.java @@ -21,14 +21,19 @@ import java.util.function.Predicate; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringBootConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping; import org.springframework.cloud.gateway.handler.predicate.MethodRoutePredicateFactory.Config; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.cloud.gateway.test.BaseWebClientTests; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; @@ -46,15 +51,49 @@ public class MethodRoutePredicateFactoryTests extends BaseWebClientTests { .expectStatus().isOk().expectHeader() .valueEquals(HANDLER_MAPPER_HEADER, RoutePredicateHandlerMapping.class.getSimpleName()) - .expectHeader().valueEquals(ROUTE_ID_HEADER, "method_test"); + .expectHeader().valueEquals(ROUTE_ID_HEADER, "method_test_get"); } @Test - public void toStringFormat() { + public void methodGetAndPostRouteWorks() { + testClient.post().uri("/multivalueheaders").header("Host", "www.method.org") + .exchange().expectStatus().isOk().expectHeader() + .valueEquals(HANDLER_MAPPER_HEADER, + RoutePredicateHandlerMapping.class.getSimpleName()) + .expectHeader().valueEquals(ROUTE_ID_HEADER, "method_test_get_and_post"); + + testClient.get().uri("/multivalueheaders").header("Host", "www.method.org") + .exchange().expectStatus().isOk().expectHeader() + .valueEquals(HANDLER_MAPPER_HEADER, + RoutePredicateHandlerMapping.class.getSimpleName()) + .expectHeader().valueEquals(ROUTE_ID_HEADER, "method_test_get_and_post"); + } + + @Test + public void methodRouteNotMatching() { + testClient.delete().uri("/multivalueheaders").header("Host", "www.method.org") + .exchange().expectStatus() + .value(integer -> integer.equals(HttpStatus.METHOD_NOT_ALLOWED)) + .expectHeader().valueEquals(HANDLER_MAPPER_HEADER, + RoutePredicateHandlerMapping.class.getSimpleName()).expectHeader() + /* Fallback to route with '/**' path predicate matches, see application.yml in test resources */ + .valueEquals(ROUTE_ID_HEADER, "default_path_to_httpbin"); + } + + @Test + public void toStringFormatSingleMethod() { Config config = new Config(); config.setMethod(HttpMethod.GET); Predicate predicate = new MethodRoutePredicateFactory().apply(config); - assertThat(predicate.toString()).contains("Method: " + config.getMethod()); + assertThat(predicate.toString()).contains("Methods: [GET]"); + } + + @Test + public void toStringFormatMultipleMethods() { + Config config = new Config(); + config.setMethods(HttpMethod.GET, HttpMethod.PUT); + Predicate predicate = new MethodRoutePredicateFactory().apply(config); + assertThat(predicate.toString()).contains("Methods: [GET, PUT]"); } @EnableAutoConfiguration @@ -62,6 +101,21 @@ public class MethodRoutePredicateFactoryTests extends BaseWebClientTests { @Import(DefaultTestConfig.class) public static class TestConfig { + @Value("${test.uri}") + String uri; + + @Bean + public RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("method_test_get", + r -> r.method("GET").and().path("/get") + .filters(f -> f.prefixPath("/httpbin")).uri(uri)) + .route("method_test_get_and_post", + r -> r.method("GET", "POST").and().path("/multivalueheaders") + .filters(f -> f.prefixPath("/httpbin")).uri(uri)) + .build(); + } + } }