diff --git a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyExchangeArgumentResolver.java b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyExchangeArgumentResolver.java index 87381573..f07987ac 100644 --- a/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyExchangeArgumentResolver.java +++ b/spring-cloud-gateway-mvc/src/main/java/org/springframework/cloud/gateway/mvc/config/ProxyExchangeArgumentResolver.java @@ -33,6 +33,8 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +import static java.util.stream.Collectors.toSet; + /** * @author Dave Syer * @author Tim Ysewyn @@ -56,7 +58,8 @@ public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResol } public void setAutoForwardedHeaders(Set autoForwardedHeaders) { - this.autoForwardedHeaders = autoForwardedHeaders; + this.autoForwardedHeaders = autoForwardedHeaders == null ? null + : autoForwardedHeaders.stream().map(String::toLowerCase).collect(toSet()); } public void setSensitive(Set sensitive) { @@ -100,7 +103,7 @@ public class ProxyExchangeArgumentResolver implements HandlerMethodArgumentResol HttpHeaders headers = new HttpHeaders(); while (headerNames.hasMoreElements()) { String header = headerNames.nextElement(); - if (this.autoForwardedHeaders.contains(header)) { + if (this.autoForwardedHeaders.contains(header.toLowerCase())) { headers.addAll(header, Collections.list(nativeRequest.getHeaders(header))); } diff --git a/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java b/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java index 669d6205..ddfe456b 100644 --- a/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java +++ b/spring-cloud-gateway-mvc/src/test/java/org/springframework/cloud/gateway/mvc/ProductionConfigurationTests.java @@ -58,7 +58,7 @@ import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; @RunWith(SpringRunner.class) -@SpringBootTest(properties = { "spring.cloud.gateway.proxy.auto-forward=baz" }, +@SpringBootTest(properties = { "spring.cloud.gateway.proxy.auto-forward=Baz" }, webEnvironment = WebEnvironment.RANDOM_PORT) @ContextConfiguration(classes = TestApplication.class) public class ProductionConfigurationTests { diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java index 5a4ff127..14b555ab 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java @@ -30,6 +30,8 @@ import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory; import org.springframework.cloud.gateway.route.Route; +import org.springframework.cloud.gateway.route.RouteDefinition; +import org.springframework.cloud.gateway.route.RouteDefinitionLocator; import org.springframework.cloud.gateway.route.RouteDefinitionWriter; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.http.ResponseEntity; @@ -46,9 +48,15 @@ public class GatewayControllerEndpoint extends AbstractGatewayControllerEndpoint public GatewayControllerEndpoint(List globalFilters, List gatewayFilters, List routePredicates, - RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) { - super(null, globalFilters, gatewayFilters, routePredicates, routeDefinitionWriter, - routeLocator); + RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator, + RouteDefinitionLocator routeDefinitionLocator) { + super(routeDefinitionLocator, globalFilters, gatewayFilters, routePredicates, + routeDefinitionWriter, routeLocator); + } + + @GetMapping("/routedefinitions") + public Flux routesdef() { + return this.routeDefinitionLocator.getRouteDefinitions(); } // TODO: Flush out routes without a definition diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 671a731c..6d35bf78 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -793,9 +793,11 @@ public class GatewayAutoConfiguration { List globalFilters, List gatewayFilters, List routePredicates, - RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator) { + RouteDefinitionWriter routeDefinitionWriter, RouteLocator routeLocator, + RouteDefinitionLocator routeDefinitionLocator) { return new GatewayControllerEndpoint(globalFilters, gatewayFilters, - routePredicates, routeDefinitionWriter, routeLocator); + routePredicates, routeDefinitionWriter, routeLocator, + routeDefinitionLocator); } @Bean diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilter.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilter.java index ef19fa7a..bb7e85fe 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilter.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilter.java @@ -31,6 +31,7 @@ import org.springframework.cloud.gateway.config.LoadBalancerProperties; import org.springframework.cloud.gateway.support.DelegatingServiceInstance; import org.springframework.cloud.gateway.support.NotFoundException; import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer; +import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.core.Ordered; import org.springframework.web.server.ServerWebExchange; @@ -121,17 +122,18 @@ public class ReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered { return LoadBalancerUriTools.reconstructURI(serviceInstance, original); } + @SuppressWarnings("deprecation") private Mono> choose(ServerWebExchange exchange) { URI uri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); ReactorLoadBalancer loadBalancer = this.clientFactory - .getInstance(uri.getHost(), ReactorLoadBalancer.class, - ServiceInstance.class); + .getInstance(uri.getHost(), ReactorServiceInstanceLoadBalancer.class); if (loadBalancer == null) { throw new NotFoundException("No loadbalancer available for " + uri.getHost()); } return loadBalancer.choose(createRequest()); } + @SuppressWarnings("deprecation") private Request createRequest() { return ReactiveLoadBalancer.REQUEST; } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java index 0746b831..65e979e9 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/handler/AsyncPredicate.java @@ -39,6 +39,10 @@ public interface AsyncPredicate extends Function> { return new NegateAsyncPredicate<>(this); } + default AsyncPredicate not(AsyncPredicate other) { + return new NegateAsyncPredicate<>(other); + } + default AsyncPredicate or(AsyncPredicate other) { return new OrAsyncPredicate<>(this, other); } @@ -84,7 +88,7 @@ public interface AsyncPredicate extends Function> { @Override public String toString() { - return String.format("!%s", this.predicate); + return String.format("!(%s)", this.predicate); } } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RouteRefreshListener.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RouteRefreshListener.java index 4625ad41..5841a53c 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RouteRefreshListener.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/RouteRefreshListener.java @@ -16,6 +16,7 @@ package org.springframework.cloud.gateway.route; +import org.springframework.boot.web.context.WebServerApplicationContext; import org.springframework.cloud.client.discovery.event.HeartbeatEvent; import org.springframework.cloud.client.discovery.event.HeartbeatMonitor; import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; @@ -43,8 +44,14 @@ public class RouteRefreshListener implements ApplicationListener fn) { + return fn + .apply(new NotOpSpec(this.routeBuilder, this.builder, this.operator)); + } + + } + + public static class NotOpSpec extends BooleanOpSpec { + + NotOpSpec(Route.AsyncBuilder routeBuilder, RouteLocatorBuilder.Builder builder, + Operator operator) { + super(routeBuilder, builder, operator); + } + + @Override + public BooleanSpec asyncPredicate(AsyncPredicate predicate) { + AsyncPredicate negated = this.routeBuilder.getPredicate() + .not(predicate); + return super.asyncPredicate(negated); + } + } } diff --git a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java index 6eb4f298..38d1b381 100644 --- a/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java +++ b/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/route/builder/PredicateSpec.java @@ -19,6 +19,7 @@ package org.springframework.cloud.gateway.route.builder; import java.time.ZonedDateTime; import java.util.Arrays; import java.util.Collections; +import java.util.function.Function; import java.util.function.Predicate; import org.springframework.cloud.gateway.handler.AsyncPredicate; @@ -294,4 +295,8 @@ public class PredicateSpec extends UriSpec { return predicate(exchange -> true); } + public BooleanSpec not(Function fn) { + return alwaysTrue().and().not(fn); + } + } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilterTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilterTests.java index 753f1d6e..76716d13 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilterTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/ReactiveLoadBalancerClientFilterTests.java @@ -32,7 +32,7 @@ import org.springframework.cloud.client.DefaultServiceInstance; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.gateway.config.LoadBalancerProperties; import org.springframework.cloud.gateway.support.NotFoundException; -import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer; +import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer; import org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers; @@ -124,8 +124,7 @@ public class ReactiveLoadBalancerClientFilterTests { RoundRobinLoadBalancer loadBalancer = new RoundRobinLoadBalancer( ServiceInstanceListSuppliers.toProvider("myservice", serviceInstance), "myservice", -1); - when(clientFactory.getInstance("myservice", ReactorLoadBalancer.class, - ServiceInstance.class)).thenReturn(loadBalancer); + when(clientFactory.getInstance("myservice", ReactorServiceInstanceLoadBalancer.class)).thenReturn(loadBalancer); when(chain.filter(exchange)).thenReturn(Mono.empty()); @@ -134,8 +133,8 @@ public class ReactiveLoadBalancerClientFilterTests { assertThat((LinkedHashSet) exchange .getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR)).contains(url); - verify(clientFactory).getInstance("myservice", ReactorLoadBalancer.class, - ServiceInstance.class); + verify(clientFactory).getInstance("myservice", + ReactorServiceInstanceLoadBalancer.class); verifyNoMoreInteractions(clientFactory); @@ -249,8 +248,7 @@ public class ReactiveLoadBalancerClientFilterTests { exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, uri); RoundRobinLoadBalancer loadBalancer = new RoundRobinLoadBalancer( ServiceInstanceListSuppliers.toProvider("service1"), "service1", -1); - when(clientFactory.getInstance("service1", ReactorLoadBalancer.class, - ServiceInstance.class)).thenReturn(loadBalancer); + when(clientFactory.getInstance("service1", ReactorServiceInstanceLoadBalancer.class)).thenReturn(loadBalancer); properties.setUse404(true); ReactiveLoadBalancerClientFilter filter = new ReactiveLoadBalancerClientFilter( clientFactory, properties); @@ -279,8 +277,7 @@ public class ReactiveLoadBalancerClientFilterTests { new DefaultServiceInstance("service1_1", "service1", "service1-host1", 8081, false)), "service1", -1); - when(clientFactory.getInstance("service1", ReactorLoadBalancer.class, - ServiceInstance.class)).thenReturn(loadBalancer); + when(clientFactory.getInstance("service1", ReactorServiceInstanceLoadBalancer.class)).thenReturn(loadBalancer); ReactiveLoadBalancerClientFilter filter = new ReactiveLoadBalancerClientFilter( clientFactory, properties); diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateTest.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateTest.java index 8b02dec9..80118149 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateTest.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/AsyncPredicateTest.java @@ -74,6 +74,20 @@ public class AsyncPredicateTest { right.assertTested(); } + @Test + public void negateOperatorWorks() { + TestAsyncPredicate falsePredicate = new TestAsyncPredicate<>(o -> false); + TestAsyncPredicate truePredicate = new TestAsyncPredicate<>(o -> true); + Publisher falseNot = falsePredicate.negate().apply(new Object()); + Publisher trueNot = truePredicate.negate().apply(new Object()); + + StepVerifier.create(falseNot).expectNext(true).expectComplete().verify(); + StepVerifier.create(trueNot).expectNext(false).expectComplete().verify(); + + falsePredicate.assertTested(); + truePredicate.assertTested(); + } + /** * An AsyncPredicate decorator that records if the apply method was called. */ diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingIntegrationTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingIntegrationTests.java index de8f7152..1627bc5a 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingIntegrationTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/handler/RoutePredicateHandlerMappingIntegrationTests.java @@ -21,14 +21,21 @@ import org.junit.BeforeClass; 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.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.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.util.SocketUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT; @@ -57,11 +64,51 @@ public class RoutePredicateHandlerMappingIntegrationTests extends BaseWebClientT .uri("/get").exchange().expectStatus().isNotFound(); } + @Test + public void andNotWorksWithMissingParameter() { + testClient.get().uri("/andnotquery").exchange().expectBody(String.class) + .isEqualTo("notsupplied"); + } + + @Test + public void andNotWorksWithParameter() { + testClient.get().uri("/andnotquery?myquery=shouldnotsee").exchange() + .expectBody(String.class).isEqualTo("hasquery"); + } + @EnableAutoConfiguration @SpringBootConfiguration @Import(DefaultTestConfig.class) + @RestController public static class TestConfig { + @Value("${test.uri:http://httpbin.org:80}") + String uri; + + @GetMapping("/httpbin/andnotquery") + String andnotquery(@RequestParam(name = "myquery", + defaultValue = "notsupplied") String myquery) { + return myquery; + } + + @GetMapping("/httpbin/hasquery") + String hasquery() { + return "hasquery"; + } + + @Bean + RouteLocator testRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("and_not_missing_myquery", + r -> r.path("/andnotquery").and().not(p -> p.query("myquery")) + .filters(f -> f.prefixPath("/httpbin")).uri(uri)) + .route("and_not_has_myquery", + r -> r.path("/andnotquery").and().query("myquery") + .filters(f -> f.setPath("/httpbin/hasquery")) + .uri(uri)) + .build(); + } + } } diff --git a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/route/RouteRefreshListenerTests.java b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/route/RouteRefreshListenerTests.java index a23ac451..6e94b548 100644 --- a/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/route/RouteRefreshListenerTests.java +++ b/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/route/RouteRefreshListenerTests.java @@ -18,19 +18,48 @@ package org.springframework.cloud.gateway.route; import org.junit.Test; +import org.springframework.boot.web.context.WebServerApplicationContext; import org.springframework.cloud.client.discovery.event.HeartbeatEvent; import org.springframework.cloud.client.discovery.event.InstanceRegisteredEvent; import org.springframework.cloud.client.discovery.event.ParentHeartbeatEvent; import org.springframework.cloud.gateway.event.RefreshRoutesEvent; +import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.ContextRefreshedEvent; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; public class RouteRefreshListenerTests { + @Test + public void onContextRefreshedEventManagement() { + ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class); + RouteRefreshListener listener = new RouteRefreshListener(publisher); + + WebServerApplicationContext applicationContext = mock( + WebServerApplicationContext.class); + when(applicationContext.getServerNamespace()).thenReturn("management"); + listener.onApplicationEvent(new ContextRefreshedEvent(applicationContext)); + + verifyNoInteractions(publisher); + } + + @Test + public void onContextRefreshedEvent() { + ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class); + RouteRefreshListener listener = new RouteRefreshListener(publisher); + + listener.onApplicationEvent( + new ContextRefreshedEvent(mock(ApplicationContext.class))); + + verify(publisher).publishEvent(any(RefreshRoutesEvent.class)); + } + @Test public void onInstanceRegisteredEvent() { ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class);