diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java index 0892c94c..a148f35c 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/actuate/GatewayControllerEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2017 the original author or authors. + * 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. @@ -27,7 +27,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory; -import org.springframework.cloud.gateway.route.RefreshRoutesEvent; +import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.route.RouteDefinition; import org.springframework.cloud.gateway.route.RouteDefinitionLocator; @@ -79,7 +79,6 @@ public class GatewayControllerEndpoint implements ApplicationEventPublisherAware // TODO: Add uncommited or new but not active routes endpoint - //TODO: this should really be a listener that responds to a RefreshEvent @PostMapping("/refresh") public Mono refresh() { this.publisher.publishEvent(new RefreshRoutesEvent(this)); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java index 3ac494af..b0be738b 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/config/GatewayAutoConfiguration.java @@ -90,7 +90,9 @@ import org.springframework.cloud.gateway.route.RouteDefinitionRepository; import org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator; import org.springframework.cloud.gateway.route.RouteDefinitionWriter; import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.cloud.gateway.route.RouteRefreshListener; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -244,10 +246,16 @@ public class GatewayAutoConfiguration { @Bean @Primary + //TODO: property to disable composite? public RouteLocator cachedCompositeRouteLocator(List routeLocators) { return new CachingRouteLocator(new CompositeRouteLocator(Flux.fromIterable(routeLocators))); } + @Bean + public RouteRefreshListener routeRefreshListener(ApplicationEventPublisher publisher) { + return new RouteRefreshListener(publisher); + } + @Bean public FilteringWebHandler filteringWebHandler(List globalFilters) { return new FilteringWebHandler(globalFilters); diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RefreshRoutesEvent.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/RefreshRoutesEvent.java similarity index 90% rename from spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RefreshRoutesEvent.java rename to spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/RefreshRoutesEvent.java index 1827f284..c23e6072 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RefreshRoutesEvent.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/event/RefreshRoutesEvent.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2017 the original author or authors. + * 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. @@ -15,7 +15,7 @@ * */ -package org.springframework.cloud.gateway.route; +package org.springframework.cloud.gateway.event; import org.springframework.context.ApplicationEvent; diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteDefinitionLocator.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteDefinitionLocator.java index dcbfcb27..5b067cf1 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteDefinitionLocator.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteDefinitionLocator.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2017 the original author or authors. + * 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. @@ -20,9 +20,11 @@ package org.springframework.cloud.gateway.route; import java.util.List; import java.util.concurrent.atomic.AtomicReference; -import org.springframework.context.event.EventListener; import reactor.core.publisher.Flux; +import org.springframework.cloud.gateway.event.RefreshRoutesEvent; +import org.springframework.context.event.EventListener; + /** * @author Spencer Gibb */ diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteLocator.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteLocator.java index 1d69cb25..36888b1f 100644 --- a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteLocator.java +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/CachingRouteLocator.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2017 the original author or authors. + * 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. @@ -20,11 +20,12 @@ package org.springframework.cloud.gateway.route; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.gateway.event.RefreshRoutesEvent; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import reactor.core.publisher.Flux; - /** * @author Spencer Gibb */ diff --git a/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteRefreshListener.java b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteRefreshListener.java new file mode 100644 index 00000000..ea331db3 --- /dev/null +++ b/spring-cloud-gateway-core/src/main/java/org/springframework/cloud/gateway/route/RouteRefreshListener.java @@ -0,0 +1,68 @@ +/* + * 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.route; + +import org.springframework.cloud.client.discovery.event.HeartbeatEvent; +import org.springframework.cloud.client.discovery.event.HeartbeatMonitor; +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.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationListener; +import org.springframework.util.Assert; + +// see ZuulDiscoveryRefreshListener +// TODO: make abstract class in commons? +public class RouteRefreshListener + implements ApplicationListener { + + private HeartbeatMonitor monitor = new HeartbeatMonitor(); + private final ApplicationEventPublisher publisher; + + public RouteRefreshListener(ApplicationEventPublisher publisher) { + Assert.notNull(publisher, "publisher may not be null"); + this.publisher = publisher; + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof InstanceRegisteredEvent) { + reset(); + } + else if (event instanceof ParentHeartbeatEvent) { + ParentHeartbeatEvent e = (ParentHeartbeatEvent) event; + resetIfNeeded(e.getValue()); + } + else if (event instanceof HeartbeatEvent) { + HeartbeatEvent e = (HeartbeatEvent) event; + resetIfNeeded(e.getValue()); + } + } + + private void resetIfNeeded(Object value) { + if (this.monitor.update(value)) { + reset(); + } + } + + private void reset() { + this.publisher.publishEvent(new RefreshRoutesEvent(this)); + } + +} diff --git a/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteRefreshListenerTests.java b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteRefreshListenerTests.java new file mode 100644 index 00000000..33fe6426 --- /dev/null +++ b/spring-cloud-gateway-core/src/test/java/org/springframework/cloud/gateway/route/RouteRefreshListenerTests.java @@ -0,0 +1,68 @@ +/* + * 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.route; + +import org.junit.Test; + +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.ApplicationEventPublisher; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class RouteRefreshListenerTests { + + @Test + public void onInstanceRegisteredEvent() { + ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class); + RouteRefreshListener listener = new RouteRefreshListener(publisher); + + listener.onApplicationEvent(new InstanceRegisteredEvent<>(this, new Object())); + + verify(publisher).publishEvent(any(RefreshRoutesEvent.class)); + } + + @Test + public void onHeartbeatEvent() { + ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class); + RouteRefreshListener listener = new RouteRefreshListener(publisher); + + listener.onApplicationEvent(new HeartbeatEvent(this, 1L)); + listener.onApplicationEvent(new HeartbeatEvent(this, 1L)); + listener.onApplicationEvent(new HeartbeatEvent(this, 2L)); + + verify(publisher, times(2)).publishEvent(any(RefreshRoutesEvent.class)); + } + + @Test + public void onParentHeartbeatEvent() { + ApplicationEventPublisher publisher = mock(ApplicationEventPublisher.class); + RouteRefreshListener listener = new RouteRefreshListener(publisher); + + listener.onApplicationEvent(new ParentHeartbeatEvent(this, 1L)); + listener.onApplicationEvent(new ParentHeartbeatEvent(this, 1L)); + listener.onApplicationEvent(new ParentHeartbeatEvent(this, 2L)); + + verify(publisher, times(2)).publishEvent(any(RefreshRoutesEvent.class)); + } +} diff --git a/spring-cloud-starter-gateway/pom.xml b/spring-cloud-starter-gateway/pom.xml index 41dad491..6602a286 100644 --- a/spring-cloud-starter-gateway/pom.xml +++ b/spring-cloud-starter-gateway/pom.xml @@ -20,6 +20,10 @@ ${basedir}/../.. + + org.springframework.cloud + spring-cloud-starter + org.springframework.cloud spring-cloud-gateway-core