Adds support for multiple HTTP methods in MethodRoutePredicate

Fixes gh-1473
This commit is contained in:
Dennis Menge
2019-12-13 12:22:05 +01:00
committed by Spencer Gibb
parent c9d3adae12
commit 73bebc9962
4 changed files with 108 additions and 20 deletions

View File

@@ -165,7 +165,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]
@@ -177,10 +177,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 parameter: a list of Spring `PathMatcher` patterns and an optional flag to `matchOptionalTrailingSeparator`.

View File

@@ -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<MethodRoutePredicateFactory.Config> {
@@ -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<String> 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;
}
}
}

View File

@@ -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);
}));
}
/**

View File

@@ -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();
}
}
}