diff --git a/docs/src/main/asciidoc/spring-cloud-netflix.adoc b/docs/src/main/asciidoc/spring-cloud-netflix.adoc index 6ab5d541..80651d07 100644 --- a/docs/src/main/asciidoc/spring-cloud-netflix.adoc +++ b/docs/src/main/asciidoc/spring-cloud-netflix.adoc @@ -500,15 +500,60 @@ Zuul's rule engine allows rules and filters to be written in essentially any JVM [[netflix-zuul-reverse-proxy]] === Embedded Zuul Reverse Proxy -Spring Cloud has created an embedded Zuul proxy to ease the development of a very common use case where a UI application wants to proxy calls to one or more back end services. To enable it, annotate a Spring Boot main class with `@EnableZuulProxy`. This forwards local calls to the appropriate service. By convention, a service with the `spring.application.name` of `users`, will receive requests from the proxy located at `/users`. The proxy uses Ribbon to locate an instance to forward to via Eureka. To skip having a service automatically added from eureka, set `zuul.ignored-services = service1`. Forwarding to the service is protected by a Hystrix circuit breaker. Additional rules can be configured via the Spring environment. The Config Server is an ideal place for the Zuul configuration. Zuul Embedded Proxy configuration rules look like the following: +Spring Cloud has created an embedded Zuul proxy to ease the development of a very common use case where a UI application wants to proxy calls to one or more back end services. To enable it, annotate a Spring Boot main class with `@EnableZuulProxy`, and this forwards local calls to the appropriate service. By convention, a service with the Eureka ID "users", will receive requests from the proxy located at `/users`. The proxy uses Ribbon to locate an instance to forward to via Eureka. To skip having a service automatically added from eureka, set `zuul.ignored-services = service1`. - zuul.route.users: /myusers/** +To augment or change the proxy routes, you can add external +configuration like the following: -This means that http calls to /myusers get forwarded to the users service. This proxy configuration is useful for services that host a user interface to proxy to the backend services it requires. To add a prefix to the mapping, set `zuul.proxy.mapping` to a value, such as `/api`. To strip the proxy mapping from the request before forwarding set `zuul.proxy.strip-mapping = true`. +.application.yml +[source,yaml] +---- + zuul: + routes: + users: /myusers/** +---- -The Zuul proxy supports Ant-style patterns. +This means that http calls to "/myusers" get forwarded to the "users" +service. This configuration is useful for a user interface to proxy to +the backend services it requires (avoiding the need to manage CORS and +authentication concerns independently for all the backends). -The `X-Forwarded-Host` header added to the forwarded requests by default. To turn it off set `zuul.proxy.add-proxy-headers = false`. +To get more fine-grained control over a route you can specify the path +and the serviceId independently: -An application with the `@EnableZuulProxy` could act as a standalone server if you set a default route, for example `zuul.route.home: /` would route `/` to the home service. +.application.yml +[source,yaml] +---- + zuul: + routes: + users: + path: /myusers/** + serviceId: users_service +---- + +This means that http calls to "/myusers" get forwarded to the +"users_service" service. The route has to have a "path" which can be +specified as an ant-style pattern, so "/myusers/*" only matches one +level, but "/myusers/**" matches hierarchically. + +The location of the backend can be specified as either a "serviceId" +(for a Eureka service) or a "url" (for a physical location), e.g. + +.application.yml +[source,yaml] +---- + zuul: + routes: + users: + path: /myusers/** + url: http://example.com/users_service +---- + +Forwarding to the service is protected by a Hystrix circuit breaker so if a service is down the client will see an error, but once the circuit is open the proxy will not try to contact the service. + +To add a prefix to the mapping, set `zuul.prefix` to a value, such as `/api`. To strip the proxy prefix from the request before the request is forwarded set `zuul.stripPrefix = true`. + +The `X-Forwarded-Host` header added to the forwarded requests by default. To turn it off set `zuul.addProxyHeaders = false`. + +An application with the `@EnableZuulProxy` could act as a standalone server if you set a default route ("/"), for example `zuul.route.home: /` would route all traffic (i.e. "/**") to the "home" service. 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 da974490..666b6480 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 @@ -40,8 +40,8 @@ public class ZuulConfiguration { private ZuulProperties zuulProperties; @Bean - public RouteLocator routes() { - return new RouteLocator(discovery, zuulProperties); + public ZuulRouteLocator routes() { + return new ZuulRouteLocator(discovery, zuulProperties); } @Bean diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulHandlerMapping.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulHandlerMapping.java index 33995340..b050cd00 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulHandlerMapping.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulHandlerMapping.java @@ -1,5 +1,6 @@ package org.springframework.cloud.netflix.zuul; +import java.util.Collection; import java.util.Map; import org.springframework.beans.factory.annotation.Autowired; @@ -26,14 +27,14 @@ import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping; public class ZuulHandlerMapping extends AbstractUrlHandlerMapping implements ApplicationListener, MvcEndpoint { - private RouteLocator routeLocator; + private ZuulRouteLocator routeLocator; private ZuulController zuul; private ZuulProperties properties; @Autowired - public ZuulHandlerMapping(RouteLocator routeLocator, ZuulController zuul, + public ZuulHandlerMapping(ZuulRouteLocator routeLocator, ZuulController zuul, ZuulProperties properties) { this.routeLocator = routeLocator; this.zuul = zuul; @@ -43,16 +44,15 @@ public class ZuulHandlerMapping extends AbstractUrlHandlerMapping implements @Override public void onApplicationEvent(InstanceRegisteredEvent event) { - registerHandlers(routeLocator.getRoutes()); + registerHandlers(routeLocator.getRoutes().keySet()); } - protected void registerHandlers(Map routes) { + protected void registerHandlers(Collection routes) { if (routes.isEmpty()) { logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping"); } else { - for (Map.Entry entry : routes.entrySet()) { - String url = entry.getKey(); + for (String url : routes) { // Prepend with slash if not already present. if (!url.startsWith("/")) { url = "/" + url; @@ -74,9 +74,9 @@ public class ZuulHandlerMapping extends AbstractUrlHandlerMapping implements @ResponseBody @ManagedOperation public Map reset() { - Map routes = routeLocator.resetRoutes(); - registerHandlers(routes); - return routes; + routeLocator.resetRoutes(); + registerHandlers(routeLocator.getRoutes().keySet()); + return getRoutes(); } @RequestMapping(method = RequestMethod.GET) diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProperties.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProperties.java index ad2d6a48..d4f90b04 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProperties.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulProperties.java @@ -1,23 +1,87 @@ package org.springframework.cloud.netflix.zuul; -import java.util.Collections; -import java.util.HashMap; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; /** * @author Spencer Gibb + * @author Dave Syer */ @Data @ConfigurationProperties("zuul") public class ZuulProperties { - private String prefix = ""; - private boolean stripPrefix = false; - private Map routes = new HashMap(); - private boolean addProxyHeaders = true; - private List ignoredServices = Collections.emptyList(); + private String prefix = ""; + private boolean stripPrefix = false; + private Map routes = new LinkedHashMap(); + private boolean addProxyHeaders = true; + private List ignoredServices = new ArrayList(); + + public Map getRoutesWithDefaultServiceIds() { + for (Entry entry : this.routes.entrySet()) { + ZuulRoute value = entry.getValue(); + if (!StringUtils.hasText(value.getLocation())) { + value.serviceId = entry.getKey(); + } + } + return this.routes; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class ZuulRoute { + private String path; + private String serviceId; + private String url; + private boolean stripPath = false; + + public ZuulRoute(String text) { + String location = null; + String path = text; + if (text.contains("=")) { + String[] values = StringUtils.trimArrayElements(StringUtils.split(text, + "=")); + location = values[1]; + path = values[0]; + } + if (!path.startsWith("/")) { + path = "/" + path; + } + setLocation(location); + this.path = path; + } + + public ZuulRoute(String path, String location) { + this.path = path; + setLocation(location); + } + + public String getLocation() { + if (StringUtils.hasText(url)) { + return url; + } + return serviceId; + } + + public void setLocation(String location) { + if (location != null + && (location.startsWith("http:") || location.startsWith("https:"))) { + url = location; + } + else { + serviceId = location; + } + } + } + } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RouteLocator.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulRouteLocator.java similarity index 72% rename from spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RouteLocator.java rename to spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulRouteLocator.java index 9ec31213..a8cc70da 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/RouteLocator.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/ZuulRouteLocator.java @@ -1,6 +1,7 @@ package org.springframework.cloud.netflix.zuul; import java.lang.reflect.Field; +import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -10,6 +11,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.cloud.netflix.zuul.ZuulProperties.ZuulRoute; import org.springframework.context.ApplicationListener; import org.springframework.core.env.CompositePropertySource; import org.springframework.util.ReflectionUtils; @@ -18,7 +20,7 @@ import org.springframework.util.ReflectionUtils; * @author Spencer Gibb */ @Slf4j -public class RouteLocator implements ApplicationListener { +public class ZuulRouteLocator implements ApplicationListener { public static final String DEFAULT_ROUTE = "/"; @@ -29,7 +31,7 @@ public class RouteLocator implements ApplicationListener private Field propertySourcesField; private AtomicReference> routes = new AtomicReference<>(); - public RouteLocator(DiscoveryClient discovery, ZuulProperties properties) { + public ZuulRouteLocator(DiscoveryClient discovery, ZuulProperties properties) { this.discovery = discovery; this.properties = properties; initField(); @@ -51,36 +53,41 @@ public class RouteLocator implements ApplicationListener } } + public Collection getRoutePaths() { + return getRoutes().keySet(); + } + public Map getRoutes() { if (routes.get() == null) { - return resetRoutes(); + resetRoutes(); } return routes.get(); } //access so ZuulHandlerMapping actuator can reset it's mappings - /*package*/ Map resetRoutes() { + /*package*/ void resetRoutes() { LinkedHashMap newValue = locateRoutes(); routes.set(newValue); - return newValue; } protected LinkedHashMap locateRoutes() { LinkedHashMap routesMap = new LinkedHashMap<>(); - // Add routes for discovery services by default - List services = discovery.getServices(); - for (String serviceId : services) { - // Ignore specified services - if (!properties.getIgnoredServices().contains(serviceId)) - routesMap.put("/" + serviceId + "/**", serviceId); - } - addConfiguredRoutes(routesMap); String defaultServiceId = routesMap.get(DEFAULT_ROUTE); + // Add routes for discovery services by default + List services = discovery.getServices(); + for (String serviceId : services) { + // Ignore specifically ignored services and those that were manually configured + String key = "/" + serviceId + "/**"; + if (!properties.getIgnoredServices().contains(serviceId) && !routesMap.containsKey(key)) { + routesMap.put(key, serviceId); + } + } + if (defaultServiceId != null) { // move the defaultServiceId to the end routesMap.remove(DEFAULT_ROUTE); @@ -90,16 +97,17 @@ public class RouteLocator implements ApplicationListener } protected void addConfiguredRoutes(Map routes) { - Map routeEntries = properties.getRoutes(); - for (Map.Entry entry : routeEntries.entrySet()) { - String serviceId = entry.getKey(); - String route = entry.getValue() ; + Map routeEntries = properties.getRoutesWithDefaultServiceIds(); + for (ZuulRoute entry : routeEntries.values()) { + String location = entry.getLocation(); + String route = entry.getPath(); if (routes.containsKey(route)) { log.warn("Overwriting route {}: already defined by {}", route, routes.get(route)); } - routes.put(route, serviceId); + routes.put(route, location); } } + } diff --git a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java index 5f6d3d0b..dae4692c 100644 --- a/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java +++ b/spring-cloud-netflix-core/src/main/java/org/springframework/cloud/netflix/zuul/filters/pre/PreDecorationFilter.java @@ -11,7 +11,7 @@ import javax.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.cloud.netflix.zuul.RouteLocator; +import org.springframework.cloud.netflix.zuul.ZuulRouteLocator; import org.springframework.cloud.netflix.zuul.ZuulProperties; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; @@ -25,13 +25,13 @@ import com.netflix.zuul.context.RequestContext; public class PreDecorationFilter extends ZuulFilter { private static Logger LOG = LoggerFactory.getLogger(PreDecorationFilter.class); - private RouteLocator routeLocator; + private ZuulRouteLocator routeLocator; private ZuulProperties properties; private PathMatcher pathMatcher = new AntPathMatcher(); - public PreDecorationFilter(RouteLocator routeLocator, ZuulProperties properties) { + public PreDecorationFilter(ZuulRouteLocator routeLocator, ZuulProperties properties) { this.routeLocator = routeLocator; this.properties = properties; } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplicationTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplicationTests.java index 46c4a82e..6da894ed 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplicationTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/SampleZuulProxyApplicationTests.java @@ -19,24 +19,37 @@ import org.springframework.test.context.web.WebAppConfiguration; @RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = SampleZuulProxyApplication.class) @WebAppConfiguration -@IntegrationTest("server.port=0") +@IntegrationTest({ "server.port: 0", + "zuul.routes.other: /test/**=http://localhost:7777/local", + "zuul.routes.simple: /simple/**" }) public class SampleZuulProxyApplicationTests { @Value("${local.server.port}") private int port; @Autowired - private RouteLocator routes; + private ZuulRouteLocator routes; @Autowired private ZuulHandlerMapping mapping; + @Test + public void bindRouteUsingPropertyEditor() { + assertEquals("http://localhost:7777/local", routes.getRoutes().get("/test/**")); + } + + @Test + public void bindRouteUsingOnlyPath() { + assertEquals("simple", routes.getRoutes().get("/simple/**")); + } + @Test public void deleteOnSelfViaSimpleHostRoutingFilter() { routes.getRoutes().put("/self/**", "http://localhost:" + port + "/local"); - mapping.registerHandlers(routes.getRoutes()); - ResponseEntity result = new TestRestTemplate().exchange("http://localhost:" + port + "/self/1", - HttpMethod.DELETE, new HttpEntity((Void) null), String.class); + mapping.registerHandlers(routes.getRoutes().keySet()); + ResponseEntity result = new TestRestTemplate().exchange( + "http://localhost:" + port + "/self/1", HttpMethod.DELETE, + new HttpEntity((Void) null), String.class); assertEquals(HttpStatus.OK, result.getStatusCode()); assertEquals("Deleted!", result.getBody()); } diff --git a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RouteLocatorTests.java b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulRouteLocatorTests.java similarity index 69% rename from spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RouteLocatorTests.java rename to spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulRouteLocatorTests.java index 1e4d8add..56eb3f9f 100644 --- a/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/RouteLocatorTests.java +++ b/spring-cloud-netflix-core/src/test/java/org/springframework/cloud/netflix/zuul/ZuulRouteLocatorTests.java @@ -13,14 +13,16 @@ import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.netflix.zuul.ZuulProperties.ZuulRoute; import org.springframework.core.env.ConfigurableEnvironment; import com.google.common.collect.Lists; /** * @author Spencer Gibb + * @author Dave Syer */ -public class RouteLocatorTests { +public class ZuulRouteLocatorTests { public static final String IGNOREDSERVICE = "ignoredservice"; public static final String ASERVICE = "aservice"; @@ -40,8 +42,8 @@ public class RouteLocatorTests { @Test public void testGetRoutes() { ZuulProperties properties = new ZuulProperties(); - RouteLocator routeLocator = new RouteLocator(this.discovery, properties); - properties.getRoutes().put(ASERVICE, "/"+ASERVICE + "/**"); + ZuulRouteLocator routeLocator = new ZuulRouteLocator(this.discovery, properties); + properties.getRoutes().put(ASERVICE, new ZuulRoute("/"+ASERVICE + "/**")); Map routesMap = routeLocator.getRoutes(); @@ -53,8 +55,8 @@ public class RouteLocatorTests { @Test public void testGetRoutesWithMapping() { ZuulProperties properties = new ZuulProperties(); - RouteLocator routeLocator = new RouteLocator(this.discovery, properties); - properties.getRoutes().put(ASERVICE, "/"+ASERVICE + "/**"); + ZuulRouteLocator routeLocator = new ZuulRouteLocator(this.discovery, properties); + properties.getRoutes().put(ASERVICE, new ZuulRoute("/"+ASERVICE + "/**", ASERVICE)); // Prefix doesn't have any impact on the routes (it's used in the filter) properties.setPrefix("/foo"); @@ -68,8 +70,8 @@ public class RouteLocatorTests { @Test public void testGetPhysicalRoutes() { ZuulProperties properties = new ZuulProperties(); - RouteLocator routeLocator = new RouteLocator(this.discovery, properties); - properties.getRoutes().put("http://" + ASERVICE, "/"+ASERVICE + "/**"); + ZuulRouteLocator routeLocator = new ZuulRouteLocator(this.discovery, properties); + properties.getRoutes().put(ASERVICE, new ZuulRoute("/"+ASERVICE + "/**", "http://" + ASERVICE)); Map routesMap = routeLocator.getRoutes(); @@ -81,7 +83,7 @@ public class RouteLocatorTests { @Test public void testIgnoreRoutes() { ZuulProperties properties = new ZuulProperties(); - RouteLocator routeLocator = new RouteLocator(this.discovery, properties); + ZuulRouteLocator routeLocator = new ZuulRouteLocator(this.discovery, properties); properties.setIgnoredServices(Lists.newArrayList(IGNOREDSERVICE)); when(discovery.getServices()).thenReturn( @@ -95,7 +97,7 @@ public class RouteLocatorTests { @Test public void testAutoRoutes() { ZuulProperties properties = new ZuulProperties(); - RouteLocator routeLocator = new RouteLocator(this.discovery, properties); + ZuulRouteLocator routeLocator = new ZuulRouteLocator(this.discovery, properties); when(discovery.getServices()).thenReturn( Lists.newArrayList(MYSERVICE)); @@ -107,6 +109,22 @@ public class RouteLocatorTests { assertMapping(routesMap, MYSERVICE); } + @Test + public void testAutoRoutesCanBeOverridden() { + ZuulProperties properties = new ZuulProperties(); + properties.getRoutes().put(MYSERVICE, new ZuulRoute("/"+MYSERVICE + "/**", "http://example.com/" + MYSERVICE)); + ZuulRouteLocator routeLocator = new ZuulRouteLocator(this.discovery, properties); + + when(discovery.getServices()).thenReturn( + Lists.newArrayList(MYSERVICE)); + + Map routesMap = routeLocator.getRoutes(); + + assertNotNull("routesMap was null", routesMap); + assertFalse("routesMap was empty", routesMap.isEmpty()); + assertMapping(routesMap, "http://example.com/" + MYSERVICE, MYSERVICE); + } + protected void assertMapping(Map routesMap, String serviceId) { assertMapping(routesMap, serviceId, serviceId); } diff --git a/spring-cloud-netflix-core/src/test/resources/application.yml b/spring-cloud-netflix-core/src/test/resources/application.yml index ec5e90c4..a8aa1aea 100644 --- a/spring-cloud-netflix-core/src/test/resources/application.yml +++ b/spring-cloud-netflix-core/src/test/resources/application.yml @@ -20,5 +20,9 @@ zuul: #prefix: /api #strip-prefix: true routes: - testclient: /testing123/** - http://localhost:8081: /stores/** + test: + serviceId: testclient + path: /testing123/** + stores: + url: http://localhost:8081 + path: /stores/**