Merge remote-tracking branch 'Upstream/2.0.x' into refactor-readbodypredicate-2.0.x

This commit is contained in:
Ryan Baxter
2018-10-03 10:39:26 -04:00
13 changed files with 180 additions and 28 deletions

View File

@@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>2.0.3.RELEASE</version>
<version>2.0.4.BUILD-SNAPSHOT</version>
<relativePath/>
</parent>
<scm>

View File

@@ -25,6 +25,7 @@ import com.netflix.hystrix.HystrixObservableCommand;
import io.netty.channel.ChannelOption;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.ipc.netty.http.client.HttpClient;
import reactor.ipc.netty.http.client.HttpClientOptions;
import reactor.ipc.netty.options.ClientProxyOptions;
@@ -122,11 +123,14 @@ import org.springframework.core.env.Environment;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.util.StringUtils;
import org.springframework.validation.Validator;
import org.springframework.web.filter.reactive.HiddenHttpMethodFilter;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient;
import org.springframework.web.reactive.socket.client.WebSocketClient;
import org.springframework.web.reactive.socket.server.WebSocketService;
import org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilterChain;
import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.DISABLED;
import static org.springframework.cloud.gateway.config.HttpClientProperties.Pool.PoolType.FIXED;
@@ -243,6 +247,19 @@ public class GatewayAutoConfiguration {
}
}
//TODO: remove when not needed anymore
// either https://jira.spring.io/browse/SPR-17291 or
// https://github.com/spring-projects/spring-boot/issues/14520 needs to be fixed
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new HiddenHttpMethodFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange);
}
};
}
@Bean
public RouteLocatorBuilder routeLocatorBuilder(ConfigurableApplicationContext context) {
return new RouteLocatorBuilder(context);

View File

@@ -19,6 +19,7 @@ package org.springframework.cloud.gateway.discovery;
import java.net.URI;
import java.util.Map;
import java.util.function.Predicate;
import reactor.core.publisher.Flux;
@@ -35,7 +36,6 @@ import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.util.StringUtils;
/**
* TODO: developer configuration, in zuul, this was opt out, should be opt in
* TODO: change to RouteLocator? use java dsl
* @author Spencer Gibb
*/
@@ -44,6 +44,7 @@ public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLoc
private final DiscoveryClient discoveryClient;
private final DiscoveryLocatorProperties properties;
private final String routeIdPrefix;
private final SimpleEvaluationContext evalCtxt;
public DiscoveryClientRouteDefinitionLocator(DiscoveryClient discoveryClient, DiscoveryLocatorProperties properties) {
this.discoveryClient = discoveryClient;
@@ -53,30 +54,37 @@ public class DiscoveryClientRouteDefinitionLocator implements RouteDefinitionLoc
} else {
this.routeIdPrefix = this.discoveryClient.getClass().getSimpleName() + "_";
}
evalCtxt = SimpleEvaluationContext
.forReadOnlyDataBinding()
.withInstanceMethods()
.build();
}
@Override
public Flux<RouteDefinition> getRouteDefinitions() {
SimpleEvaluationContext evalCtxt = SimpleEvaluationContext
.forReadOnlyDataBinding()
.withInstanceMethods()
.build();
SpelExpressionParser parser = new SpelExpressionParser();
Expression includeExpr = parser.parseExpression(properties.getIncludeExpression());
Expression urlExpr = parser.parseExpression(properties.getUrlExpression());
Predicate<ServiceInstance> includePredicate;
if (properties.getIncludeExpression() == null || "true".equalsIgnoreCase(properties.getIncludeExpression())) {
includePredicate = instance -> true;
} else {
includePredicate = instance -> {
Boolean include = includeExpr.getValue(evalCtxt, instance, Boolean.class);
if (include == null) {
return false;
}
return include;
};
}
return Flux.fromIterable(discoveryClient.getServices())
.map(discoveryClient::getInstances)
.filter(instances -> !instances.isEmpty())
.map(instances -> instances.get(0))
.filter(instance -> {
Boolean include = includeExpr.getValue(evalCtxt, instance, Boolean.class);
if (include == null) {
return false;
}
return include;
})
.filter(includePredicate)
.map(instance -> {
String serviceId = instance.getServiceId();

View File

@@ -19,9 +19,12 @@ package org.springframework.cloud.gateway.filter;
import java.net.URI;
import java.util.List;
import java.util.concurrent.TimeUnit;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.timeout.ReadTimeoutException;
import io.netty.handler.timeout.ReadTimeoutHandler;
import reactor.core.publisher.Mono;
import reactor.ipc.netty.NettyPipeline;
import reactor.ipc.netty.http.client.HttpClient;
@@ -111,17 +114,16 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered {
proxyRequest.header(HttpHeaders.HOST, host);
}
if (properties.getResponseTimeout() != null) {
proxyRequest.context(ctx -> ctx.addHandlerFirst(
new ReadTimeoutHandler(properties.getResponseTimeout().toMillis(), TimeUnit.MILLISECONDS)));
}
return proxyRequest.sendHeaders() //I shouldn't need this
.send(request.getBody().map(dataBuffer ->
((NettyDataBuffer) dataBuffer).getNativeBuffer()));
});
if (properties.getResponseTimeout() != null) {
responseMono.timeout(properties.getResponseTimeout(),
Mono.error(new TimeoutException("Response took longer than timeout: " +
properties.getResponseTimeout())));
}
return responseMono.doOnNext(res -> {
ServerHttpResponse response = exchange.getResponse();
// put headers and status so filters can modify the response
@@ -150,6 +152,10 @@ public class NettyRoutingFilter implements GlobalFilter, Ordered {
// Defer committing the response until all route filters have run
// Put client response as ServerWebExchange attribute and write response later NettyWriteResponseFilter
exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res);
}).then(chain.filter(exchange));
})
.onErrorMap(t -> properties.getResponseTimeout() != null && t instanceof ReadTimeoutException,
t -> new TimeoutException("Response took longer than timeout: " +
properties.getResponseTimeout()))
.then(chain.filter(exchange));
}
}

View File

@@ -23,7 +23,10 @@ import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.OrderedGatewayFilter;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
@@ -31,13 +34,10 @@ import org.springframework.cloud.gateway.route.Route;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.web.server.WebHandler;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
import reactor.core.publisher.Mono;
/**
* WebHandler that delegates to a chain of {@link GlobalFilter} instances and
* {@link GatewayFilterFactory} instances then to the target {@link WebHandler}.
@@ -82,7 +82,9 @@ public class FilteringWebHandler implements WebHandler {
//TODO: needed or cached?
AnnotationAwareOrderComparator.sort(combined);
logger.debug("Sorted gatewayFilterFactories: "+ combined);
if (logger.isDebugEnabled()) {
logger.debug("Sorted gatewayFilterFactories: "+ combined);
}
return new DefaultGatewayFilterChain(combined).filter(exchange);
}

View File

@@ -61,7 +61,7 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
if (managmentPort != null && exchange.getRequest().getURI().getPort() == managmentPort.intValue()) {
return Mono.empty();
}
exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getClass().getSimpleName());
exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());
return lookupRoute(exchange)
// .log("route-predicate-handler-mapping", Level.FINER) //name this
@@ -146,4 +146,7 @@ public class RoutePredicateHandlerMapping extends AbstractHandlerMapping {
protected void validateRoute(Route route, ServerWebExchange exchange) {
}
protected String getSimpleName() {
return "RoutePredicateHandlerMapping";
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.gateway.handler.predicate;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Predicate;
@@ -40,6 +41,7 @@ import static org.springframework.http.server.PathContainer.parsePath;
*/
public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<PathRoutePredicateFactory.Config> {
private static final Log log = LogFactory.getLog(RoutePredicateFactory.class);
private static final String MATCH_OPTIONAL_TRAILING_SEPARATOR_KEY = "matchOptionalTrailingSeparator";
private PathPatternParser pathPatternParser = new PathPatternParser();
@@ -53,12 +55,13 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<Pat
@Override
public List<String> shortcutFieldOrder() {
return Collections.singletonList(PATTERN_KEY);
return Arrays.asList(PATTERN_KEY, MATCH_OPTIONAL_TRAILING_SEPARATOR_KEY);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
synchronized (this.pathPatternParser) {
pathPatternParser.setMatchOptionalTrailingSeparator(config.isMatchOptionalTrailingSeparator());
config.pathPattern = this.pathPatternParser.parse(config.pattern);
}
return exchange -> {
@@ -88,6 +91,7 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<Pat
public static class Config {
private String pattern;
private PathPattern pathPattern;
private boolean matchOptionalTrailingSeparator = true;
public String getPattern() {
return pattern;
@@ -98,10 +102,20 @@ public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<Pat
return this;
}
public boolean isMatchOptionalTrailingSeparator() {
return matchOptionalTrailingSeparator;
}
public Config setMatchOptionalTrailingSeparator(boolean matchOptionalTrailingSeparator) {
this.matchOptionalTrailingSeparator = matchOptionalTrailingSeparator;
return this;
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("pattern", pattern)
.append("matchOptionalTrailingSeparator", matchOptionalTrailingSeparator)
.toString();
}
}

View File

@@ -170,6 +170,19 @@ public class PredicateSpec extends UriSpec {
.applyAsync(c -> c.setPattern(pattern)));
}
/**
* A predicate that checks if the path of the request matches the given pattern
* @param pattern the pattern to check the path against.
* The pattern is a {@link org.springframework.util.PathMatcher} pattern
* @param matchOptionalTrailingSeparator set to false if you do not want this path to match
* when there is a trailing <code>/</code>
* @return a {@link BooleanSpec} to be used to add logical operators
*/
public BooleanSpec path(String pattern, boolean matchOptionalTrailingSeparator) {
return asyncPredicate(getBean(PathRoutePredicateFactory.class)
.applyAsync(c -> c.setPattern(pattern).setMatchOptionalTrailingSeparator(matchOptionalTrailingSeparator)));
}
/**
* This predicate is BETA and may be subject to change in a future release.
* A predicate that checks the contents of the request body

View File

@@ -0,0 +1,22 @@
{
"properties": [
{
"name": "spring.cloud.gateway.enabled",
"type": "java.lang.Boolean",
"description": "Enables gateway functionality.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.forwarded.enabled",
"type": "java.lang.Boolean",
"description": "Enables the ForwardedHeadersFilter.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.metrics.enabled",
"type": "java.lang.Boolean",
"description": "Enables the collection of metrics data.",
"defaultValue": "false"
}
]
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2013-2018 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
*
* http://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;
import java.util.Map;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(properties = "spring.cloud.gateway.httpclient.response-timeout=3s", webEnvironment = RANDOM_PORT)
@DirtiesContext
@SuppressWarnings("unchecked")
public class NettyRoutingFilterIntegrationTests extends BaseWebClientTests {
@Test
public void responseTimeoutWorks() {
testClient.get()
.uri("/delay/5")
.exchange()
.expectStatus().is5xxServerError()
.expectBody(Map.class)
.consumeWith(result -> {
Map body = result.getResponseBody();
assertThat(body).containsEntry("message", "Response took longer than timeout: PT3S");
});
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
public static class TestConfig {
}
}

View File

@@ -44,6 +44,12 @@ public class PathRoutePredicateFactoryTests extends BaseWebClientTests {
.expectStatus().isOk()
.expectHeader().valueEquals(HANDLER_MAPPER_HEADER, RoutePredicateHandlerMapping.class.getSimpleName())
.expectHeader().valueEquals(ROUTE_ID_HEADER, "path_test");
//since the configuration does not allow the trailing / to match this should fail
testClient.get().uri("/abc/123/function/")
.header(HttpHeaders.HOST, "www.path.org")
.exchange()
.expectStatus().is4xxClientError();
}
@Test

View File

@@ -128,7 +128,7 @@ spring:
- id: path_test
uri: ${test.uri}
predicates:
- Path=/{org}/{scope}/function
- Path=/{org}/{scope}/function,false
- Host=**.path.org
filters:
- SetPath=/anything/{org}{scope}

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>spring-cloud-dependencies-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>2.0.3.RELEASE</version>
<version>2.0.4.BUILD-SNAPSHOT</version>
<relativePath/>
</parent>