Commit 75addf04 by Spencer Gibb

change zuul implementation to be controller/handler mapping based.

This allows mappings to be at the root and not have to be prefixed. It also allows mappings to fall through to other handler mappings. Patterns are now Ant-style via AntPathMatcher. fixes gh-72
parent 7c4e3100
...@@ -500,33 +500,15 @@ Zuul's rule engine allows rules and filters to be written in essentially any JVM ...@@ -500,33 +500,15 @@ Zuul's rule engine allows rules and filters to be written in essentially any JVM
[[netflix-zuul-reverse-proxy]] [[netflix-zuul-reverse-proxy]]
=== Embedded 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 `/proxy/*` to the appropriate service. The proxy uses Ribbon to locate an instance to forward to via Eureka. Forwarding to the service is protected by a Hystrix circuit breaker. Rules are 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`. 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:
zuul.proxy.route.users: /users zuul.route.users: /myusers/**
This means that http calls to /proxy/users 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 change the mapping, set `zuul.proxy.mapping` to a different value, such as `/api`. By default, the proxy mapping gets stripped from the request before forwarding. To keep the proxy mapping on the forwarded call, set `zuul.proxy.stripMapping = false`. 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`.
To have the `X-Forwarded-Host` header added to the forwarded requests, set `zuul.proxy.addProxyHeaders = true`. The Zuul proxy supports Ant-style patterns.
[[netflix-zuul-server]] The `X-Forwarded-Host` header added to the forwarded requests by default. To turn it off set `zuul.proxy.add-proxy-headers = false`.
=== Standalone Zuul Server
Spring Cloud has created a standalone Zuul server. To enable it, annotate a Spring Boot main class with `@EnableZuulServer`. This routes all calls to the appropriate service. The server uses Ribbon to locate an instance to forward to via Eureka. Forwarding to the service is protected by a Hystrix circuit breaker. Rules are configured via the Spring environment. The Config Server is an ideal place for the Zuul configuration. Zuul Server configuration rules look like the following: 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.
zuul.server.route.users: /users
This means that http calls to /users get forwarded to the users service.
Since zuul, by default, intercepts all requests (`/*`), to enable actuator, you should set the `management.port`.
To use the Spring Boot error facilities while using the standalone Zuul server, please set the following properties
----
# moves the Spring Dispatch Servlet to a path below `/`
server:
servletPath: /app
# sets the error path to use the Dispatch Servlet to resolve the error view
error:
path: ${server.servletPath}/error
----
...@@ -30,7 +30,6 @@ ...@@ -30,7 +30,6 @@
<module>spring-cloud-netflix-hystrix-dashboard</module> <module>spring-cloud-netflix-hystrix-dashboard</module>
<module>spring-cloud-netflix-eureka-server</module> <module>spring-cloud-netflix-eureka-server</module>
<module>spring-cloud-netflix-turbine</module> <module>spring-cloud-netflix-turbine</module>
<module>spring-cloud-netflix-zuul-server</module>
<module>docs</module> <module>docs</module>
</modules> </modules>
...@@ -254,6 +253,20 @@ ...@@ -254,6 +253,20 @@
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
<properties> <properties>
<archaius.version>0.6.3</archaius.version> <archaius.version>0.6.3</archaius.version>
<eureka.version>1.1.145</eureka.version> <eureka.version>1.1.145</eureka.version>
......
...@@ -43,12 +43,6 @@ ...@@ -43,12 +43,6 @@
<groupId>com.netflix.eureka</groupId> <groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId> <artifactId>eureka-client</artifactId>
<optional>true</optional> <optional>true</optional>
<exclusions>
<exclusion>
<artifactId>netflix-eventbus</artifactId>
<groupId>com.netflix.netflix-commons</groupId>
</exclusion>
</exclusions>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.netflix.eureka</groupId> <groupId>com.netflix.eureka</groupId>
......
...@@ -2,6 +2,8 @@ package org.springframework.cloud.client.discovery; ...@@ -2,6 +2,8 @@ package org.springframework.cloud.client.discovery;
import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.ServiceInstance;
import java.util.List;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
*/ */
...@@ -10,4 +12,6 @@ public interface DiscoveryClient { ...@@ -10,4 +12,6 @@ public interface DiscoveryClient {
* @return ServiceInstance with information used to register the local service * @return ServiceInstance with information used to register the local service
*/ */
public ServiceInstance getLocalServiceInstance(); public ServiceInstance getLocalServiceInstance();
public List<String> getServices();
} }
package org.springframework.cloud.netflix.eureka; package org.springframework.cloud.netflix.eureka;
import com.google.common.base.Function;
import com.google.common.collect.Lists;
import com.netflix.appinfo.InstanceInfo;
import com.netflix.discovery.shared.Application;
import com.netflix.discovery.shared.Applications;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.DiscoveryClient;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import static com.google.common.collect.Iterables.*;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
*/ */
...@@ -12,6 +23,9 @@ public class EurekaDiscoveryClient implements DiscoveryClient { ...@@ -12,6 +23,9 @@ public class EurekaDiscoveryClient implements DiscoveryClient {
@Autowired @Autowired
private EurekaInstanceConfigBean config; private EurekaInstanceConfigBean config;
@Autowired
private com.netflix.discovery.DiscoveryClient discovery;
@Override @Override
public ServiceInstance getLocalServiceInstance() { public ServiceInstance getLocalServiceInstance() {
return new ServiceInstance() { return new ServiceInstance() {
...@@ -31,4 +45,19 @@ public class EurekaDiscoveryClient implements DiscoveryClient { ...@@ -31,4 +45,19 @@ public class EurekaDiscoveryClient implements DiscoveryClient {
} }
}; };
} }
@Override
public List<String> getServices() {
Applications applications = discovery.getApplications();
if (applications == null) {
return Collections.emptyList();
}
return Lists.newArrayList(transform(applications.getRegisteredApplications(), new Function<Application, String>() {
@Nullable
@Override
public String apply(@Nullable Application app) {
return app.getName().toLowerCase();
}
}));
}
} }
...@@ -13,7 +13,6 @@ import org.springframework.cloud.netflix.hystrix.EnableHystrix; ...@@ -13,7 +13,6 @@ import org.springframework.cloud.netflix.hystrix.EnableHystrix;
@EnableEurekaClient @EnableEurekaClient
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Documented @Import(ZuulConfiguration.class)
@Import(ZuulProxyConfiguration.class)
public @interface EnableZuulProxy { public @interface EnableZuulProxy {
} }
...@@ -8,12 +8,13 @@ import java.lang.annotation.*; ...@@ -8,12 +8,13 @@ import java.lang.annotation.*;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
* @deprecated @see org.springframework.cloud.netflix.zuul.EnableZuulProxy
*/ */
@EnableHystrix @EnableHystrix
@EnableEurekaClient @EnableEurekaClient
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@Documented @Documented
@Import(ZuulServerConfiguration.class) @Import(ZuulConfiguration.class)
public @interface EnableZuulServer { public @interface EnableZuulServer {
} }
package org.springframework.cloud.netflix.zuul; package org.springframework.cloud.netflix.zuul;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.bind.PropertySourceUtils; import org.springframework.boot.bind.PropertySourceUtils;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.context.environment.EnvironmentChangeEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.*; import org.springframework.core.env.*;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.LinkedHashMap; import java.util.*;
import java.util.Map; import java.util.concurrent.atomic.AtomicReference;
import java.util.Set;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
*/ */
public class Routes { @Slf4j
private static final Logger logger = LoggerFactory.getLogger(Routes.class); public class RouteLocator implements ApplicationListener<EnvironmentChangeEvent> {
public static final String DEFAULT_ROUTE = "/"; public static final String DEFAULT_ROUTE = "/";
@Autowired @Autowired
ConfigurableEnvironment env; protected ConfigurableEnvironment env;
@Autowired
protected DiscoveryClient discovery;
@Autowired
protected ZuulProperties properties;
private Field propertySourcesField; private Field propertySourcesField;
private String keyPrefix; private AtomicReference<LinkedHashMap<String, String>> routes = new AtomicReference<>();
public Routes(String keyPrefix) { public RouteLocator() {
this.keyPrefix = keyPrefix;
initField(); initField();
} }
...@@ -35,26 +42,52 @@ public class Routes { ...@@ -35,26 +42,52 @@ public class Routes {
propertySourcesField.setAccessible(true); propertySourcesField.setAccessible(true);
} }
//TODO: cache routes or respond to environment event and refresh all routes @Override
public LinkedHashMap<String, String> getRoutes() { public void onApplicationEvent(EnvironmentChangeEvent event) {
LinkedHashMap<String, String> routes = new LinkedHashMap<>(); for (String key : event.getKeys()) {
if (key.startsWith(properties.getRoutePrefix())) {
routes.set(locateRoutes());
return;
}
}
}
//TODO: respond to changes in eureka
public Map<String, String> getRoutes() {
if (routes.get() == null) {
routes.set(locateRoutes());
}
return routes.get();
}
protected LinkedHashMap<String, String> locateRoutes() {
LinkedHashMap<String, String> routesMap = new LinkedHashMap<>();
//Add routes for discovery services by default
List<String> services = discovery.getServices();
for (String serviceId : services) {
//Ignore specified services
if (!properties.getIgnoredServices().contains(serviceId))
routesMap.put("/" + serviceId + "/**", serviceId);
}
MutablePropertySources propertySources = env.getPropertySources(); MutablePropertySources propertySources = env.getPropertySources();
for (PropertySource<?> propertySource : propertySources) { for (PropertySource<?> propertySource : propertySources) {
getRoutes(propertySource, routes); getRoutes(propertySource, routesMap);
} }
String defaultServiceId = routes.get(DEFAULT_ROUTE); String defaultServiceId = routesMap.get(DEFAULT_ROUTE);
if (defaultServiceId != null) { if (defaultServiceId != null) {
//move the defaultServiceId to the end //move the defaultServiceId to the end
routes.remove(DEFAULT_ROUTE); routesMap.remove(DEFAULT_ROUTE);
routes.put(DEFAULT_ROUTE, defaultServiceId); routesMap.put(DEFAULT_ROUTE, defaultServiceId);
} }
return routesMap;
return routes;
} }
public void getRoutes(PropertySource<?> propertySource, LinkedHashMap<String, String> routes) { protected void getRoutes(PropertySource<?> propertySource, Map<String, String> routes) {
if (propertySource instanceof CompositePropertySource) { if (propertySource instanceof CompositePropertySource) {
try { try {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
...@@ -69,13 +102,13 @@ public class Routes { ...@@ -69,13 +102,13 @@ public class Routes {
//EnumerablePropertySource enumerable = (EnumerablePropertySource) propertySource; //EnumerablePropertySource enumerable = (EnumerablePropertySource) propertySource;
MutablePropertySources propertySources = new MutablePropertySources(); MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addLast(propertySource); propertySources.addLast(propertySource);
Map<String, Object> routeEntries = PropertySourceUtils.getSubProperties(propertySources, keyPrefix); Map<String, Object> routeEntries = PropertySourceUtils.getSubProperties(propertySources, properties.getRoutePrefix());
for (Map.Entry<String, Object> entry : routeEntries.entrySet()) { for (Map.Entry<String, Object> entry : routeEntries.entrySet()) {
String serviceId = entry.getKey(); String serviceId = entry.getKey();
String route = entry.getValue().toString(); String route = entry.getValue().toString();
if (routes.containsKey(route)) { if (routes.containsKey(route)) {
logger.warn("Overwriting route {}: already defined by {}", route, routes.get(route)); log.warn("Overwriting route {}: already defined by {}", route, routes.get(route));
} }
routes.put(route, serviceId); routes.put(route, serviceId);
} }
......
package org.springframework.cloud.netflix.zuul; package org.springframework.cloud.netflix.zuul;
import com.netflix.zuul.context.ContextLifecycleFilter;
import com.netflix.zuul.http.ZuulServlet; import com.netflix.zuul.http.ZuulServlet;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.trace.TraceRepository; import org.springframework.boot.actuate.trace.TraceRepository;
import org.springframework.boot.context.embedded.FilterRegistrationBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.embedded.ServletRegistrationBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter; import org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter;
import org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter; import org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter;
import org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter; import org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter;
...@@ -13,39 +13,38 @@ import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter; ...@@ -13,39 +13,38 @@ import org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter;
import org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter; import org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter;
import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter; import org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.Collection;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
*/ */
public abstract class AbstractZuulConfiguration { @Configuration
@EnableConfigurationProperties()
@ConditionalOnClass(ZuulServlet.class)
@ConditionalOnExpression("${zuul.enabled:true}")
public class ZuulConfiguration {
@Autowired(required=false) @Autowired(required=false)
private TraceRepository traces; private TraceRepository traces;
protected abstract ZuulProperties getProperties();
@Bean @Bean
public Routes routes() { public ZuulProperties zuulProperties() {
return new Routes(getProperties().getRoutePrefix()); return new ZuulProperties();
} }
@Bean @Bean
public ServletRegistrationBean zuulServlet() { public RouteLocator routes(){
return new ServletRegistrationBean(new ZuulServlet(), getProperties().getMapping()+"/*"); return new RouteLocator();
} }
@Bean @Bean
public FilterRegistrationBean contextLifecycleFilter() { public ZuulController zuulController() {
Collection<String> urlPatterns = new ArrayList<>(); return new ZuulController();
urlPatterns.add(getProperties().getMapping()+"/*"); }
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new ContextLifecycleFilter());
filterRegistrationBean.setUrlPatterns(urlPatterns);
return filterRegistrationBean; @Bean
public ZuulHandlerMapping zuulHandlerMapping() {
return new ZuulHandlerMapping();
} }
@Bean @Bean
......
package org.springframework.cloud.netflix.zuul;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.http.ZuulServlet;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.ServletWrappingController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author Spencer Gibb
*/
public class ZuulController extends ServletWrappingController {
public ZuulController() {
setServletClass(ZuulServlet.class);
setServletName("zuul");
}
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
return super.handleRequestInternal(request, response);
} finally {
// @see com.netflix.zuul.context.ContextLifecycleFilter.doFilter
RequestContext.getCurrentContext().unset();
}
}
}
package org.springframework.cloud.netflix.zuul;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
import javax.annotation.PostConstruct;
import java.util.Map;
/**
* @author Spencer Gibb
*/
@Slf4j
public class ZuulHandlerMapping extends AbstractUrlHandlerMapping implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
protected RouteLocator routeLocator;
@Autowired
protected ZuulController zuul;
@Autowired
protected ZuulProperties properties;
public ZuulHandlerMapping() {
setOrder(-200);
}
@PostConstruct
public void init() {
}
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
registerHandlers(routeLocator.getRoutes());
}
private void registerHandlers(Map<String, String> routes) {
if (routes.isEmpty()) {
logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping");
}
else {
for (Map.Entry<String, String> entry : routes.entrySet()) {
String url = entry.getKey();
// Prepend with slash if not already present.
if (!url.startsWith("/")) {
url = "/" + url;
}
if (StringUtils.hasText(properties.getRoutePrefix())) {
url = properties.getMapping()+url;
if (!url.startsWith("/")) {
url = "/" + url;
}
}
registerHandler(url, zuul);
}
}
}
}
package org.springframework.cloud.netflix.zuul; package org.springframework.cloud.netflix.zuul;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Collections;
import java.util.List;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
*/ */
public interface ZuulProperties { @Data
public String getMapping(); @ConfigurationProperties("zuul")
public boolean isStripMapping(); public class ZuulProperties {
public String getRoutePrefix(); private String mapping = "";
public boolean isAddProxyHeaders(); private boolean stripMapping = false;
private String routePrefix = "zuul.route.";
private boolean addProxyHeaders = true;
private List<String> ignoredServices = Collections.emptyList();
} }
package org.springframework.cloud.netflix.zuul;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.netflix.zuul.http.ZuulServlet;
/**
* @author Spencer Gibb
*/
@Configuration
@EnableConfigurationProperties(ZuulProxyProperties.class)
@ConditionalOnClass(ZuulServlet.class)
@ConditionalOnExpression("${zuul.proxy.enabled:true}")
public class ZuulProxyConfiguration extends AbstractZuulConfiguration {
@Bean
public ZuulProxyProperties zuulProxyProperties() {
return new ZuulProxyProperties();
}
@Bean
//ZuulProxyProperties doesn't implement ZuulProperties so there are not 2 implementations (see ZuulServerConfiguration
public ZuulProperties zuulProperties() {
return new ZuulProperties() {
@Override
public String getMapping() {
return zuulProxyProperties().getMapping();
}
@Override
public boolean isStripMapping() {
return zuulProxyProperties().isStripMapping();
}
@Override
public String getRoutePrefix() {
return zuulProxyProperties().getRoutePrefix();
}
@Override
public boolean isAddProxyHeaders() {
return zuulProxyProperties().isAddProxyHeaders();
}
};
}
@Override
protected ZuulProperties getProperties() {
return zuulProperties();
}
}
package org.springframework.cloud.netflix.zuul;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Spencer Gibb
*/
@Data
@ConfigurationProperties("zuul.proxy")
public class ZuulProxyProperties {
private String mapping = "/proxy";
private boolean stripMapping = true; // this is currently the default behaviour
private String routePrefix = "zuul.proxy.route.";
private boolean addProxyHeaders = false; // TODO: current CF demo's rely on this
}
...@@ -2,28 +2,33 @@ package org.springframework.cloud.netflix.zuul.filters.pre; ...@@ -2,28 +2,33 @@ package org.springframework.cloud.netflix.zuul.filters.pre;
import com.google.common.base.Optional; import com.google.common.base.Optional;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.Routes; import org.springframework.cloud.netflix.zuul.RouteLocator;
import org.springframework.cloud.netflix.zuul.ZuulProperties; import org.springframework.cloud.netflix.zuul.ZuulProperties;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.LinkedHashMap; import java.util.Map;
import static com.google.common.collect.Iterables.*;
public class PreDecorationFilter extends ZuulFilter { public class PreDecorationFilter extends ZuulFilter {
private static Logger LOG = LoggerFactory.getLogger(PreDecorationFilter.class); private static Logger LOG = LoggerFactory.getLogger(PreDecorationFilter.class);
@Autowired @Autowired
private Routes routes; private RouteLocator routeLocator;
@Autowired @Autowired
private ZuulProperties properties; private ZuulProperties properties;
private PathMatcher pathMatcher = new AntPathMatcher();
@Override @Override
public int filterOrder() { public int filterOrder() {
return 5; return 5;
...@@ -55,12 +60,12 @@ public class PreDecorationFilter extends ZuulFilter { ...@@ -55,12 +60,12 @@ public class PreDecorationFilter extends ZuulFilter {
} }
ctx.put("requestURI", uriPart); ctx.put("requestURI", uriPart);
LinkedHashMap<String, String> routesMap = routes.getRoutes(); Map<String, String> routesMap = routeLocator.getRoutes();
Optional<String> route = Iterables.tryFind(routesMap.keySet(), new Predicate<String>() { Optional<String> route = tryFind(routesMap.keySet(), new Predicate<String>() {
@Override @Override
public boolean apply(@Nullable String path) { public boolean apply(@Nullable String path) {
return uriPart.startsWith(path); return pathMatcher.match(path, uriPart);
} }
}); });
......
package org.springframework.cloud.netflix.zuul;
import com.google.common.collect.Lists;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.mock.env.MockPropertySource;
import java.util.Map;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.initMocks;
/**
* @author Spencer Gibb
*/
public class RouteLocatorTests {
public static final String IGNOREDSERVICE = "ignoredservice";
public static final String ASERVICE = "aservice";
public static final String MYSERVICE = "myservice";
@Mock
ConfigurableEnvironment env;
@Mock
DiscoveryClient discovery;
@Before
public void init() {
initMocks(this);
}
@Test
public void testGetRoutes() {
RouteLocator routeLocator = new RouteLocator();
routeLocator.properties = new ZuulProperties();
routeLocator.properties.setIgnoredServices(Lists.newArrayList(IGNOREDSERVICE));
routeLocator.discovery = this.discovery;
routeLocator.env = this.env;
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("zuul.route."+ ASERVICE, getMapping(ASERVICE)));
when(env.getPropertySources()).thenReturn(propertySources);
when(discovery.getServices()).thenReturn(Lists.newArrayList(MYSERVICE, IGNOREDSERVICE));
Map<String, String> routesMap = routeLocator.getRoutes();
assertNotNull("routesMap was null", routesMap);
assertFalse("routesMap was empty", routesMap.isEmpty());
assertMapping(routesMap, MYSERVICE);
assertMapping(routesMap, ASERVICE);
String serviceId = routesMap.get(getMapping(IGNOREDSERVICE));
assertNull("routes did not ignore "+IGNOREDSERVICE, serviceId);
}
protected void assertMapping(Map<String, String> routesMap, String expectedServiceId) {
String mapping = getMapping(expectedServiceId);
String serviceId = routesMap.get(mapping);
assertEquals("routesMap had wrong value for "+mapping, expectedServiceId, serviceId);
}
private String getMapping(String serviceId) {
return "/"+ serviceId +"/**";
}
}
package org.springframework.cloud.netflix.zuul.sample; package org.springframework.cloud.netflix.zuul.sample;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.SpringApplication;
import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan; import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Configuration;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy; import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@Configuration @SpringBootApplication
@ComponentScan
@EnableAutoConfiguration
@RestController @RestController
@EnableZuulProxy @EnableZuulProxy
@EnableEurekaClient
public class ZuulProxyApplication { public class ZuulProxyApplication {
@RequestMapping("/testing123") @RequestMapping("/testing123")
...@@ -26,7 +24,7 @@ public class ZuulProxyApplication { ...@@ -26,7 +24,7 @@ public class ZuulProxyApplication {
} }
public static void main(String[] args) { public static void main(String[] args) {
new SpringApplicationBuilder(ZuulProxyApplication.class).web(true).run(args); SpringApplication.run(ZuulProxyApplication.class, args);
} }
} }
...@@ -6,13 +6,16 @@ spring: ...@@ -6,13 +6,16 @@ spring:
eureka: eureka:
server: server:
enabled: false enabled: false
error: #error:
path: /myerror # path: /myerror
management:
context-path: /admin
endpoints:
health:
sensitive: false
zuul: zuul:
proxy: #mapping: /api
mapping: /api #strip-mapping: true
stripMapping: true route:
route: testclient: /testing123
testclient: /testing123 #stores: /
stores: /stores
customers: /customers
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-netflix-zuul-server</artifactId>
<packaging>jar</packaging>
<name>Spring Cloud Netflix Zuul Server</name>
<url>http://projects.spring.io/spring-cloud/</url>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>
<build>
<plugins>
</plugins>
</build>
<properties>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.eureka</groupId>
<artifactId>eureka-client</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-metrics-event-stream</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-javanica</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-core</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-eureka</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.ribbon</groupId>
<artifactId>ribbon-httpclient</artifactId>
</dependency>
<dependency>
<groupId>com.netflix.zuul</groupId>
<artifactId>zuul-core</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- Only needed at compile time -->
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
package org.springframework.cloud.netflix.zuul;
import com.netflix.zuul.http.ZuulServlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author Spencer Gibb
*/
@Configuration
@ConditionalOnClass(ZuulServlet.class)
@EnableConfigurationProperties(ZuulServerProperties.class)
@ConditionalOnExpression("${zuul.server.enabled:true}")
public class ZuulServerConfiguration extends AbstractZuulConfiguration {
@Bean
public ZuulServerProperties zuulServerProperties() {
return new ZuulServerProperties();
}
@Bean
//ZuulServerProperties doesn't implement ZuulProperties so there are not 2 implementations (see ZuulProxyConfiguration
public ZuulProperties zuulProperties() {
return new ZuulProperties() {
@Override
public String getMapping() {
return zuulServerProperties().getMapping();
}
@Override
public boolean isStripMapping() {
return zuulServerProperties().isStripMapping();
}
@Override
public String getRoutePrefix() {
return zuulServerProperties().getRoutePrefix();
}
@Override
public boolean isAddProxyHeaders() {
return zuulServerProperties().isAddProxyHeaders();
}
};
}
@Override
protected ZuulProperties getProperties() {
return zuulProperties();
}
}
package org.springframework.cloud.netflix.zuul;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @author Spencer Gibb
*/
@Data
@ConfigurationProperties("zuul.server")
public class ZuulServerProperties {
private String mapping = "";
private boolean stripMapping = false;
private String routePrefix = "zuul.server.route.";
private boolean addProxyHeaders = true;
}
package org.springframework.cloud.netflix.zuul;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAutoConfiguration
@EnableZuulServer
public class ZuulServerApplication {
public static void main(String[] args) {
new SpringApplicationBuilder(ZuulServerApplication.class).web(true).run(args);
}
}
package org.springframework.cloud.netflix.zuul;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = ZuulServerApplication.class)
@WebAppConfiguration
@IntegrationTest("server.port=0")
public class ZuulServerApplicationTests {
@Test
public void contextLoads() {
}
}
server:
port: 9876
servletPath: /app
management:
port: 9877
spring:
application:
name: testzuulserver
error:
path: ${server.servletPath}/error
zuul:
server:
route:
testclient: /testing123
stores: /stores
customers: /customers
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment