diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulConfiguration.java index 81f46576..a352043c 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.cloud.netflix.zuul; +import java.util.Collection; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; @@ -30,6 +31,7 @@ import org.springframework.cloud.client.actuator.HasFeatures; import org.springframework.cloud.client.discovery.event.HeartbeatEvent; import org.springframework.cloud.client.discovery.event.HeartbeatMonitor; import org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent; +import org.springframework.cloud.netflix.zuul.filters.CompositeRouteLocator; import org.springframework.cloud.netflix.zuul.filters.RouteLocator; import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; @@ -47,6 +49,7 @@ import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Primary; import org.springframework.context.event.ContextRefreshedEvent; import com.netflix.zuul.ZuulFilter; @@ -78,8 +81,15 @@ public class ZuulConfiguration { } @Bean - @ConditionalOnMissingBean(RouteLocator.class) - public RouteLocator routeLocator() { + @Primary + public CompositeRouteLocator primaryRouteLocator( + Collection routeLocators) { + return new CompositeRouteLocator(routeLocators); + } + + @Bean + @ConditionalOnMissingBean(SimpleRouteLocator.class) + public SimpleRouteLocator simpleRouteLocator() { return new SimpleRouteLocator(this.server.getServletPrefix(), this.zuulProperties); } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java index 40ea7a9c..b9af3438 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProxyConfiguration.java @@ -76,9 +76,8 @@ public class ZuulProxyConfiguration extends ZuulConfiguration { } @Bean - @Override - @ConditionalOnMissingBean(RouteLocator.class) - public DiscoveryClientRouteLocator routeLocator() { + @ConditionalOnMissingBean(DiscoveryClientRouteLocator.class) + public DiscoveryClientRouteLocator discoveryRouteLocator() { return new DiscoveryClientRouteLocator(this.server.getServletPrefix(), this.discovery, this.zuulProperties, this.serviceRouteMapper); } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/CompositeRouteLocator.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/CompositeRouteLocator.java new file mode 100644 index 00000000..a106d97a --- /dev/null +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/CompositeRouteLocator.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013-2014 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.netflix.zuul.filters; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.util.Assert; + +/** + * RouteLocator that composes multiple RouteLocators. + * + * @author Johannes Edmeier + * + */ +public class CompositeRouteLocator implements RefreshableRouteLocator { + private final Collection routeLocators; + private ArrayList rl; + + public CompositeRouteLocator(Collection routeLocators) { + Assert.notNull(routeLocators, "'routeLocators' must not be null"); + rl = new ArrayList<>(routeLocators); + AnnotationAwareOrderComparator.sort(rl); + this.routeLocators = rl; + } + + @Override + public Collection getIgnoredPaths() { + List ignoredPaths = new ArrayList<>(); + for (RouteLocator locator : routeLocators) { + ignoredPaths.addAll(locator.getIgnoredPaths()); + } + return ignoredPaths; + } + + @Override + public List getRoutes() { + List route = new ArrayList<>(); + for (RouteLocator locator : routeLocators) { + route.addAll(locator.getRoutes()); + } + return route; + } + + @Override + public Route getMatchingRoute(String path) { + for (RouteLocator locator : routeLocators) { + Route route = locator.getMatchingRoute(path); + if (route != null) { + return route; + } + } + return null; + } + + @Override + public void refresh() { + for (RouteLocator locator : routeLocators) { + if (locator instanceof RefreshableRouteLocator) { + ((RefreshableRouteLocator) locator).refresh(); + } + } + } +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java index 2a6d9c22..ee88073a 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/SimpleRouteLocator.java @@ -26,6 +26,7 @@ import java.util.concurrent.atomic.AtomicReference; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.ZuulRoute; import org.springframework.cloud.netflix.zuul.util.RequestUtils; +import org.springframework.core.Ordered; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; @@ -38,7 +39,8 @@ import lombok.extern.apachecommons.CommonsLog; * @author Dave Syer */ @CommonsLog -public class SimpleRouteLocator implements RouteLocator { +public class SimpleRouteLocator implements RouteLocator, Ordered { + private static final int DEFAULT_ORDER = 0; private ZuulProperties properties; @@ -48,6 +50,7 @@ public class SimpleRouteLocator implements RouteLocator { private String zuulServletPath; private AtomicReference> routes = new AtomicReference<>(); + private int order = DEFAULT_ORDER; public SimpleRouteLocator(String servletPath, ZuulProperties properties) { this.properties = properties; @@ -200,4 +203,13 @@ public class SimpleRouteLocator implements RouteLocator { return adjustedPath; } + @Override + public int getOrder() { + return order; + } + + public void setOrder(int order) { + this.order = order; + } + } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/CompositeRouteLocatorTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/CompositeRouteLocatorTests.java new file mode 100644 index 00000000..abe5b3f4 --- /dev/null +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/CompositeRouteLocatorTests.java @@ -0,0 +1,97 @@ +package org.springframework.cloud.netflix.zuul.filters; + +import static java.util.Arrays.asList; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +/** + * @author Johannes Edmeier + */ +public class CompositeRouteLocatorTests { + private CompositeRouteLocator locator; + + public CompositeRouteLocatorTests() { + List locators = new ArrayList<>(); + locators.add(new TestRouteLocator(asList("ign1"), + asList(createRoute("1", "/pathA")))); + locators.add( + new TestRouteLocator(asList("ign1", "ign2"), + asList(createRoute("2", "/pathA"), createRoute("2", "/pathB")))); + this.locator = new CompositeRouteLocator(locators); + } + + @Test + public void test_getIgnoredPaths() { + assertThat(locator.getIgnoredPaths(), hasItems("ign1", "ign2")); + + } + + @Test + public void test_getRoutes() { + assertThat(locator.getRoutes(), + hasItems(createRoute("1", "/pathA"), createRoute("2", "/pathB"))); + } + + @Test + public void test_getMatchingRoute() { + assertThat(locator.getMatchingRoute("/pathA"), notNullValue()); + assertThat(locator.getMatchingRoute("/pathA").getId(), is("1")); + assertThat("Locator 1 should take precedence", locator.getMatchingRoute("/pathB").getId(), + is("2")); + assertThat(locator.getMatchingRoute("/pathNot"), nullValue()); + } + + @Test + public void test_refresh() { + RefreshableRouteLocator mock = mock(RefreshableRouteLocator.class); + new CompositeRouteLocator(asList(mock)).refresh(); + verify(mock).refresh(); + } + + private Route createRoute(String id, String path) { + return new Route(id, path, null, null, false, Collections.emptySet()); + } + + private static class TestRouteLocator implements RouteLocator { + private Collection ignoredPaths; + private List routes; + + public TestRouteLocator(Collection ignoredPaths, List routes) { + this.ignoredPaths = ignoredPaths; + this.routes = routes; + } + + @Override + public Collection getIgnoredPaths() { + return this.ignoredPaths; + } + + @Override + public List getRoutes() { + return this.routes; + } + + @Override + public Route getMatchingRoute(String path) { + for (Route route : routes) { + if (path.startsWith(route.getPath())) { + return route; + } + } + return null; + } + + } +} \ No newline at end of file diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandIntegrationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandIntegrationTests.java index 790cab60..75b6a013 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandIntegrationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/filters/route/restclient/RestClientRibbonCommandIntegrationTests.java @@ -32,8 +32,6 @@ import java.util.UUID; import javax.servlet.http.HttpServletRequest; -import lombok.SneakyThrows; - import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -49,7 +47,6 @@ import org.springframework.cloud.netflix.ribbon.RibbonClients; import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.cloud.netflix.ribbon.StaticServerList; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; -import org.springframework.cloud.netflix.zuul.filters.RouteLocator; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; import org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientRouteLocator; import org.springframework.cloud.netflix.zuul.filters.route.RestClientRibbonCommand; @@ -85,6 +82,8 @@ import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.ServerList; import com.netflix.niws.client.http.RestClient; +import lombok.SneakyThrows; + @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = RestClientRibbonCommandIntegrationTests.TestConfig.class, webEnvironment = WebEnvironment.RANDOM_PORT, value = { "zuul.routes.other: /test/**=http://localhost:7777/local", @@ -96,7 +95,7 @@ import com.netflix.niws.client.http.RestClient; public class RestClientRibbonCommandIntegrationTests extends ZuulProxyTestBase { @Autowired - RouteLocator routeLocator; + DiscoveryClientRouteLocator routeLocator; @Override protected boolean supportsPatch() { @@ -252,17 +251,17 @@ public class RestClientRibbonCommandIntegrationTests extends ZuulProxyTestBase { assertTrue("ribbonCommandFactory not a MyRibbonCommandFactory", this.ribbonCommandFactory instanceof TestConfig.MyRibbonCommandFactory); } - + @Override @SuppressWarnings("deprecation") @Test public void javascriptEncodedFormParams() { TestRestTemplate testRestTemplate = new TestRestTemplate(); ArrayList> converters = new ArrayList<>(); - converters.addAll(Arrays.asList(new StringHttpMessageConverter(), + converters.addAll(Arrays.asList(new StringHttpMessageConverter(), new NoEncodingFormHttpMessageConverter())); testRestTemplate.getRestTemplate().setMessageConverters(converters); - + MultiValueMap map = new LinkedMultiValueMap<>(); map.add("foo", "(bar)"); ResponseEntity result = testRestTemplate.postForEntity( @@ -306,7 +305,7 @@ public class RestClientRibbonCommandIntegrationTests extends ZuulProxyTestBase { public ResponseEntity addHeader(HttpServletRequest request) { HttpHeaders headers = new HttpHeaders(); headers.set("X-Header", "FOO"); - ResponseEntity result = new ResponseEntity( + ResponseEntity result = new ResponseEntity<>( request.getRequestURI(), headers, HttpStatus.OK); return result; } @@ -337,7 +336,8 @@ public class RestClientRibbonCommandIntegrationTests extends ZuulProxyTestBase { } @Bean - public RouteLocator routeLocator(DiscoveryClient discoveryClient, + public DiscoveryClientRouteLocator discoveryRouteLocator( + DiscoveryClient discoveryClient, ZuulProperties zuulProperties) { return new MyRouteLocator("/", discoveryClient, zuulProperties); }