简单研究下Zuul简单使用以及原理.
1. 使用
0. pom如下:
<?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"> <parent> <artifactId>cloud</artifactId> <groupId>cn.qz.cloud</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>cloud-gateway-zuul9526</artifactId> <dependencies> <!--zuul--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <!--eureka-client--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--引入自己抽取的工具包--> <dependency> <groupId>cn.qz.cloud</groupId> <artifactId>cloud-api-commons</artifactId> <version>${project.version}</version> </dependency> <!--一般基础配置类--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
最终zuul-core 是1.3.1 版本。
1. 新增filter
PreFilter 前置处理器
package cn.qz.cloud.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.Enumeration; @Component public class PreFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.PRE_TYPE; } @Override public int filterOrder() { return -1; } @Override public boolean shouldFilter() { return true; } /** * 返回值好像没什么实际意义, @see com.netflix.zuul.FilterProcessor#runFilters(java.lang.String) * * @return 可以返回任意的对象,当前实现忽略。 * @throws ZuulException */ @Override public Object run() throws ZuulException { // RequestContext 继承自ConcurrentHashMap, 用于维护当前请求的一些上下文参数, 内部用threadLocal 维护当前请求的当前RequestContext。 HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String key = headerNames.nextElement(); String header = request.getHeader(key); System.out.println("key: " + key + "\tvalue: " + header); } return null; } }
PostFilter 后置处理器
package cn.qz.cloud.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants; import org.springframework.stereotype.Component; @Component public class PostFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.POST_TYPE; } @Override public int filterOrder() { return -1; } @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); return ctx.sendZuulResponse(); } @Override public Object run() throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); System.out.println("========cn.qz.cloud.filter.PostFilter.run"); System.out.println(ctx); return null; } }
2. 主启动类
package cn.qz.cloud; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @EnableZuulProxy @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
3. 配置文件 application.yml
server: port: 9526 spring: application: name: cloud-zuul servlet: multipart: max-file-size: 2048MB max-request-size: 2048MB eureka: instance: hostname: cloud-gateway-service client: #服务提供者provider注册进eureka服务列表内 service-url: register-with-eureka: true fetch-registry: true defaultZone: http://localhost:7001/eureka zuul: routes: # 第一种配置方式,这种方式会创建两个 ZuulRoute 对象。 一个是: /api/v1/payment/** 路由转发到服务 cloud-payment-service; 第二个是 /cloud-payment-service/** 转发到服务 cloud-payment-service cloud-payment-service: path: /api/v1/payment/** sensitive-headers: Cookie,Set-Cookie service-id: CLOUD-PAYMENT-SERVICE # 下面这种方式等价于 /orders/** 开头的所有请求都交给orders 服务处理 orders: /orders/** # URL 跳转的方式,不走ribbon 路由服务。访问: http://localhost:9526/guonei guonei: path: /guonei/** url: http://news.baidu.com/guonei retryable: false ribbon: eager-load: enabled: false servlet-path: /cloud-zuul ribbon: ReadTimeout: 600000 SocketTimeout: 600000 ConnectTimeout: 80000
4. 接下来就可以通过zuul 实现请求转发,测试如下:
xxx /e/ideaspace/test (dev) $ curl http://localhost:9526/api/v1/payment/pay/getServerPort % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 52 0 52 0 0 1040 0 --:--:-- --:--:-- --:--:-- 1061{"success":true,"code":"200","msg":"","data":"8081"} xxx /e/ideaspace/test (dev) $ curl http://localhost:9526/cloud-zuul/api/v1/payment/pay/getServerPort % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 52 0 52 0 0 928 0 --:--:-- --:--:-- --:--:-- 962{"success":true,"code":"200","msg":"","data":"8081"} xxx /e/ideaspace/test (dev) $ curl http://localhost:9526/cloud-payment-service/pay/getServerPort % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 52 0 52 0 0 269 0 --:--:-- --:--:-- --:--:-- 270{"success":true,"code":"200","msg":"","data":"8081"} xxx /e/ideaspace/test (dev) $ curl http://localhost:9526/cloud-zuul/cloud-payment-service/pay/getServerPort % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 52 0 52 0 0 1209 0 --:--:-- --:--:-- --:--:-- 1268{"success":true,"code":"200","msg":"","data":"8081"}
浏览器访问: http://localhost:9526/guonei 可以看到调整到百度新闻首页。
/api/v1/payment/pay/getServerPort 和 /cloud-payment-service/pay/getServerPort 应该走的是一套机制; /cloud-zuul/api/v1/payment/pay/getServerPort 和/cloud-zuul/cloud-payment-service/pay/getServerPort 是一套机制。下面研究两套流程逻辑。
2. 原理
1. 前置
基于SpringMVC的DispatcherServlet + zuul 的自己的Servlet + Filter 机制来实现。
有两套机制: 第一套走SpringMVC(基于DispatcherServlet自定义HandlerMapping + Handler实现逻辑), 第二套不走SpingMVC机制(相当于直接向容器注册Servlet)。
2. 整合原理
org.springframework.cloud.netflix.zuul.EnableZuulProxy 注解开启自动整合zuul。引org.springframework.cloud.netflix.zuul.ZuulProxyMarkerConfiguration配置类,实际上是注入一个Marker类,这也是Spring starter经常采用的一种方式。
@EnableCircuitBreaker @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(ZuulProxyMarkerConfiguration.class) public @interface EnableZuulProxy { } ------------ @Configuration(proxyBeanMethods = false) public class ZuulProxyMarkerConfiguration { @Bean public Marker zuulProxyMarkerBean() { return new Marker(); } class Marker { } }
引入自动配置类:org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration
@Configuration(proxyBeanMethods = false) @Import({ RibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class, RibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class, RibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class, HttpClientConfiguration.class }) @ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class) public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration { @SuppressWarnings("rawtypes") @Autowired(required = false) private List<RibbonRequestCustomizer> requestCustomizers = Collections.emptyList(); @Autowired(required = false) private Registration registration; @Autowired private DiscoveryClient discovery; @Autowired private ServiceRouteMapper serviceRouteMapper; @Override public HasFeatures zuulFeature() { return HasFeatures.namedFeature("Zuul (Discovery)", ZuulProxyAutoConfiguration.class); } @Bean @ConditionalOnMissingBean(DiscoveryClientRouteLocator.class) public DiscoveryClientRouteLocator discoveryRouteLocator() { return new DiscoveryClientRouteLocator(this.server.getServlet().getContextPath(), this.discovery, this.zuulProperties, this.serviceRouteMapper, this.registration); } // pre filters @Bean @ConditionalOnMissingBean(PreDecorationFilter.class) public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator, ProxyRequestHelper proxyRequestHelper) { return new PreDecorationFilter(routeLocator, this.server.getServlet().getContextPath(), this.zuulProperties, proxyRequestHelper); } // route filters @Bean @ConditionalOnMissingBean(RibbonRoutingFilter.class) public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper, RibbonCommandFactory<?> ribbonCommandFactory) { RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, ribbonCommandFactory, this.requestCustomizers); return filter; } @Bean @ConditionalOnMissingBean({ SimpleHostRoutingFilter.class, CloseableHttpClient.class }) public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties zuulProperties, ApacheHttpClientConnectionManagerFactory connectionManagerFactory, ApacheHttpClientFactory httpClientFactory) { return new SimpleHostRoutingFilter(helper, zuulProperties, connectionManagerFactory, httpClientFactory); } @Bean @ConditionalOnMissingBean({ SimpleHostRoutingFilter.class }) public SimpleHostRoutingFilter simpleHostRoutingFilter2(ProxyRequestHelper helper, ZuulProperties zuulProperties, CloseableHttpClient httpClient) { return new SimpleHostRoutingFilter(helper, zuulProperties, httpClient); } @Bean @ConditionalOnMissingBean(ServiceRouteMapper.class) public ServiceRouteMapper serviceRouteMapper() { return new SimpleServiceRouteMapper(); } @Configuration(proxyBeanMethods = false) @ConditionalOnMissingClass("org.springframework.boot.actuate.health.Health") protected static class NoActuatorConfiguration { @Bean public ProxyRequestHelper proxyRequestHelper(ZuulProperties zuulProperties) { ProxyRequestHelper helper = new ProxyRequestHelper(zuulProperties); return helper; } } @Configuration(proxyBeanMethods = false) @ConditionalOnClass(Health.class) protected static class EndpointConfiguration { @Autowired(required = false) private HttpTraceRepository traces; @Bean @ConditionalOnEnabledEndpoint public RoutesEndpoint routesEndpoint(RouteLocator routeLocator) { return new RoutesEndpoint(routeLocator); } @ConditionalOnEnabledEndpoint @Bean public FiltersEndpoint filtersEndpoint() { FilterRegistry filterRegistry = FilterRegistry.instance(); return new FiltersEndpoint(filterRegistry); } @Bean public ProxyRequestHelper proxyRequestHelper(ZuulProperties zuulProperties) { TraceProxyRequestHelper helper = new TraceProxyRequestHelper(zuulProperties); if (this.traces != null) { helper.setTraces(this.traces); } return helper; } } }
父类org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration:
@Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ ZuulProperties.class }) @ConditionalOnClass({ ZuulServlet.class, ZuulServletFilter.class }) @ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class) // Make sure to get the ServerProperties from the same place as a normal web app would // FIXME @Import(ServerPropertiesAutoConfiguration.class) public class ZuulServerAutoConfiguration { @Autowired protected ZuulProperties zuulProperties; @Autowired protected ServerProperties server; @Autowired(required = false) private ErrorController errorController; private Map<String, CorsConfiguration> corsConfigurations; @Autowired(required = false) private List<WebMvcConfigurer> configurers = emptyList(); @Bean public HasFeatures zuulFeature() { return HasFeatures.namedFeature("Zuul (Simple)", ZuulServerAutoConfiguration.class); } @Bean @Primary public CompositeRouteLocator primaryRouteLocator( Collection<RouteLocator> routeLocators) { return new CompositeRouteLocator(routeLocators); } @Bean @ConditionalOnMissingBean(SimpleRouteLocator.class) public SimpleRouteLocator simpleRouteLocator() { return new SimpleRouteLocator(this.server.getServlet().getContextPath(), this.zuulProperties); } @Bean public ZuulController zuulController() { return new ZuulController(); } @Bean public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes, ZuulController zuulController) { ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController); mapping.setErrorController(this.errorController); mapping.setCorsConfigurations(getCorsConfigurations()); return mapping; } protected final Map<String, CorsConfiguration> getCorsConfigurations() { if (this.corsConfigurations == null) { ZuulCorsRegistry registry = new ZuulCorsRegistry(); this.configurers.forEach(configurer -> configurer.addCorsMappings(registry)); this.corsConfigurations = registry.getCorsConfigurations(); } return this.corsConfigurations; } @Bean public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() { return new ZuulRefreshListener(); } @Bean @ConditionalOnMissingBean(name = "zuulServlet") @ConditionalOnProperty(name = "zuul.use-filter", havingValue = "false", matchIfMissing = true) public ServletRegistrationBean zuulServlet() { ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean<>( new ZuulServlet(), this.zuulProperties.getServletPattern()); // The whole point of exposing this servlet is to provide a route that doesn't // buffer requests. servlet.addInitParameter("buffer-requests", "false"); return servlet; } @Bean @ConditionalOnMissingBean(name = "zuulServletFilter") @ConditionalOnProperty(name = "zuul.use-filter", havingValue = "true", matchIfMissing = false) public FilterRegistrationBean zuulServletFilter() { final FilterRegistrationBean<ZuulServletFilter> filterRegistration = new FilterRegistrationBean<>(); filterRegistration.setUrlPatterns( Collections.singleton(this.zuulProperties.getServletPattern())); filterRegistration.setFilter(new ZuulServletFilter()); filterRegistration.setOrder(Ordered.LOWEST_PRECEDENCE); // The whole point of exposing this servlet is to provide a route that doesn't // buffer requests. filterRegistration.addInitParameter("buffer-requests", "false"); return filterRegistration; } // pre filters @Bean public ServletDetectionFilter servletDetectionFilter() { return new ServletDetectionFilter(); } @Bean @ConditionalOnMissingBean public FormBodyWrapperFilter formBodyWrapperFilter() { return new FormBodyWrapperFilter(); } @Bean @ConditionalOnMissingBean public DebugFilter debugFilter() { return new DebugFilter(); } @Bean @ConditionalOnMissingBean public Servlet30WrapperFilter servlet30WrapperFilter() { return new Servlet30WrapperFilter(); } // post filters @Bean public SendResponseFilter sendResponseFilter(ZuulProperties properties) { return new SendResponseFilter(zuulProperties); } @Bean public SendErrorFilter sendErrorFilter() { return new SendErrorFilter(); } @Bean public SendForwardFilter sendForwardFilter() { return new SendForwardFilter(); } @Bean @ConditionalOnProperty("zuul.ribbon.eager-load.enabled") public ZuulRouteApplicationContextInitializer zuulRoutesApplicationContextInitiazer( SpringClientFactory springClientFactory) { return new ZuulRouteApplicationContextInitializer(springClientFactory, zuulProperties); } @Configuration(proxyBeanMethods = false) protected static class ZuulFilterConfiguration { @Autowired private Map<String, ZuulFilter> filters; @Bean public ZuulFilterInitializer zuulFilterInitializer(CounterFactory counterFactory, TracerFactory tracerFactory) { FilterLoader filterLoader = FilterLoader.getInstance(); FilterRegistry filterRegistry = FilterRegistry.instance(); return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry); } } @Configuration(proxyBeanMethods = false) @ConditionalOnClass(MeterRegistry.class) protected static class ZuulCounterFactoryConfiguration { @Bean @ConditionalOnBean(MeterRegistry.class) @ConditionalOnMissingBean(CounterFactory.class) public CounterFactory counterFactory(MeterRegistry meterRegistry) { return new DefaultCounterFactory(meterRegistry); } } @Configuration(proxyBeanMethods = false) protected static class ZuulMetricsConfiguration { @Bean @ConditionalOnMissingClass("io.micrometer.core.instrument.MeterRegistry") @ConditionalOnMissingBean(CounterFactory.class) public CounterFactory counterFactory() { return new EmptyCounterFactory(); } @ConditionalOnMissingBean(TracerFactory.class) @Bean public TracerFactory tracerFactory() { return new EmptyTracerFactory(); } } private static class ZuulRefreshListener implements ApplicationListener<ApplicationEvent> { @Autowired private ZuulHandlerMapping zuulHandlerMapping; private HeartbeatMonitor heartbeatMonitor = new HeartbeatMonitor(); @Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ContextRefreshedEvent || event instanceof RefreshScopeRefreshedEvent || event instanceof RoutesRefreshedEvent || 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.heartbeatMonitor.update(value)) { reset(); } } private void reset() { this.zuulHandlerMapping.setDirty(true); } } private static class ZuulCorsRegistry extends CorsRegistry { @Override protected Map<String, CorsConfiguration> getCorsConfigurations() { return super.getCorsConfigurations(); } } }
可以看到上面配置,ZuulProxyAutoConfiguration 相比 ZuulServerAutoConfiguration 增加了一些Ribbon相关的代理,也就是支持从注册中心拿服务信息。这也是EnableZuulProxy注解和EnableZuulServer 的区别,两个分别引入下面配置类:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(ZuulServerMarkerConfiguration.class) public @interface EnableZuulServer { } -------- @EnableCircuitBreaker @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(ZuulProxyMarkerConfiguration.class) public @interface EnableZuulProxy { }
官方解释如下:
Spring Cloud Netflix会根据使用哪个注释来启用Zuul安装多个过滤器。`@EnableZuulProxy`是`@EnableZuulServer`的超集。换句话说,`@EnableZuulProxy`包含`@EnableZuulServer`安装的所有过滤器。“代理”中的其他过滤器启用路由功能。如果你想要一个“空白”Zuul,你应该使用`@EnableZuulServer`。
org.springframework.cloud.netflix.zuul.filters.ZuulProperties: 属性类,解析配置文件的配置,可以看到将配置信息维护到Map<String, ZuulRoute> routes对象中, 并且org.springframework.cloud.netflix.zuul.filters.ZuulProperties#init 用@PostConstruct 在初始化之前进行路径的规范化。
@ConfigurationProperties("zuul") public class ZuulProperties { /** * Headers that are generally expected to be added by Spring Security, and hence often * duplicated if the proxy and the backend are secured with Spring. By default they * are added to the ignored headers if Spring Security is present and * ignoreSecurityHeaders = true. */ public static final List<String> SECURITY_HEADERS = Arrays.asList("Pragma", "Cache-Control", "X-Frame-Options", "X-Content-Type-Options", "X-XSS-Protection", "Expires"); /** * A common prefix for all routes. */ private String prefix = ""; /** * Flag saying whether to strip the prefix from the path before forwarding. */ private boolean stripPrefix = true; /** * Flag for whether retry is supported by default (assuming the routes themselves * support it). */ private Boolean retryable = false; /** * Map of route names to properties. */ private Map<String, ZuulRoute> routes = new LinkedHashMap<>(); /** * Flag to determine whether the proxy adds X-Forwarded-* headers. */ private boolean addProxyHeaders = true; /** * Flag to determine whether the proxy forwards the Host header. */ private boolean addHostHeader = false; /** * Set of service names not to consider for proxying automatically. By default all * services in the discovery client will be proxied. */ private Set<String> ignoredServices = new LinkedHashSet<>(); private Set<String> ignoredPatterns = new LinkedHashSet<>(); /** * Names of HTTP headers to ignore completely (i.e. leave them out of downstream * requests and drop them from downstream responses). */ private Set<String> ignoredHeaders = new LinkedHashSet<>(); /** * Flag to say that SECURITY_HEADERS are added to ignored headers if spring security * is on the classpath. By setting ignoreSecurityHeaders to false we can switch off * this default behaviour. This should be used together with disabling the default * spring security headers see * https://docs.spring.io/spring-security/site/docs/current/reference/html/headers.html#default-security-headers */ private boolean ignoreSecurityHeaders = true; /** * Flag to force the original query string encoding when building the backend URI in * SimpleHostRoutingFilter. When activated, query string will be built using * HttpServletRequest getQueryString() method instead of UriTemplate. Note that this * flag is not used in RibbonRoutingFilter with services found via DiscoveryClient * (like Eureka). */ private boolean forceOriginalQueryStringEncoding = false; /** * Path to install Zuul as a servlet (not part of Spring MVC). The servlet is more * memory efficient for requests with large bodies, e.g. file uploads. */ private String servletPath = "/zuul"; private boolean ignoreLocalService = true; /** * Host properties controlling default connection pool properties. */ private Host host = new Host(); /** * Flag to say that request bodies can be traced. */ private boolean traceRequestBody = false; /** * Flag to say that path elements past the first semicolon can be dropped. */ private boolean removeSemicolonContent = true; /** * Flag to indicate whether to decode the matched URL or use it as is. */ private boolean decodeUrl = true; /** * List of sensitive headers that are not passed to downstream requests. Defaults to a * "safe" set of headers that commonly contain user credentials. It's OK to remove * those from the list if the downstream service is part of the same system as the * proxy, so they are sharing authentication data. If using a physical URL outside * your own domain, then generally it would be a bad idea to leak user credentials. */ private Set<String> sensitiveHeaders = new LinkedHashSet<>( Arrays.asList("Cookie", "Set-Cookie", "Authorization")); /** * Flag to say whether the hostname for ssl connections should be verified or not. * Default is true. This should only be used in test setups! */ private boolean sslHostnameValidationEnabled = true; private ExecutionIsolationStrategy ribbonIsolationStrategy = SEMAPHORE; private HystrixSemaphore semaphore = new HystrixSemaphore(); private HystrixThreadPool threadPool = new HystrixThreadPool(); /** * Setting for SendResponseFilter to conditionally set Content-Length header. */ private boolean setContentLength = false; /** * Setting for SendResponseFilter to conditionally include X-Zuul-Debug-Header header. */ private boolean includeDebugHeader = false; /** * Setting for SendResponseFilter for the initial stream buffer size. */ private int initialStreamBufferSize = 8192; public Set<String> getIgnoredHeaders() { Set<String> ignoredHeaders = new LinkedHashSet<>(this.ignoredHeaders); if (ClassUtils.isPresent( "org.springframework.security.config.annotation.web.WebSecurityConfigurer", null) && Collections.disjoint(ignoredHeaders, SECURITY_HEADERS) && ignoreSecurityHeaders) { // Allow Spring Security in the gateway to control these headers ignoredHeaders.addAll(SECURITY_HEADERS); } return ignoredHeaders; } public void setIgnoredHeaders(Set<String> ignoredHeaders) { this.ignoredHeaders.addAll(ignoredHeaders); } @PostConstruct public void init() { for (Entry<String, ZuulRoute> entry : this.routes.entrySet()) { ZuulRoute value = entry.getValue(); if (!StringUtils.hasText(value.getLocation())) { value.serviceId = entry.getKey(); } if (!StringUtils.hasText(value.getId())) { value.id = entry.getKey(); } if (!StringUtils.hasText(value.getPath())) { value.path = "/" + entry.getKey() + "/**"; } } } public String getServletPattern() { String path = this.servletPath; if (!path.startsWith("/")) { path = "/" + path; } if (!path.contains("*")) { path = path.endsWith("/") ? (path + "*") : (path + "/*"); } return path; } public String getPrefix() { return prefix; } public void setPrefix(String prefix) { this.prefix = prefix; } public boolean isStripPrefix() { return stripPrefix; } public void setStripPrefix(boolean stripPrefix) { this.stripPrefix = stripPrefix; } public Boolean getRetryable() { return retryable; } public void setRetryable(Boolean retryable) { this.retryable = retryable; } public Map<String, ZuulRoute> getRoutes() { return routes; } public void setRoutes(Map<String, ZuulRoute> routes) { this.routes = routes; } public boolean isAddProxyHeaders() { return addProxyHeaders; } public void setAddProxyHeaders(boolean addProxyHeaders) { this.addProxyHeaders = addProxyHeaders; } public boolean isAddHostHeader() { return addHostHeader; } public void setAddHostHeader(boolean addHostHeader) { this.addHostHeader = addHostHeader; } public Set<String> getIgnoredServices() { return ignoredServices; } public void setIgnoredServices(Set<String> ignoredServices) { this.ignoredServices = ignoredServices; } public Set<String> getIgnoredPatterns() { return ignoredPatterns; } public void setIgnoredPatterns(Set<String> ignoredPatterns) { this.ignoredPatterns = ignoredPatterns; } public boolean isIgnoreSecurityHeaders() { return ignoreSecurityHeaders; } public void setIgnoreSecurityHeaders(boolean ignoreSecurityHeaders) { this.ignoreSecurityHeaders = ignoreSecurityHeaders; } public boolean isForceOriginalQueryStringEncoding() { return forceOriginalQueryStringEncoding; } public void setForceOriginalQueryStringEncoding( boolean forceOriginalQueryStringEncoding) { this.forceOriginalQueryStringEncoding = forceOriginalQueryStringEncoding; } public String getServletPath() { return servletPath; } public void setServletPath(String servletPath) { this.servletPath = servletPath; } public boolean isIgnoreLocalService() { return ignoreLocalService; } public void setIgnoreLocalService(boolean ignoreLocalService) { this.ignoreLocalService = ignoreLocalService; } public Host getHost() { return host; } public void setHost(Host host) { this.host = host; } public boolean isTraceRequestBody() { return traceRequestBody; } public void setTraceRequestBody(boolean traceRequestBody) { this.traceRequestBody = traceRequestBody; } public boolean isRemoveSemicolonContent() { return removeSemicolonContent; } public void setRemoveSemicolonContent(boolean removeSemicolonContent) { this.removeSemicolonContent = removeSemicolonContent; } public boolean isDecodeUrl() { return decodeUrl; } public void setDecodeUrl(boolean decodeUrl) { this.decodeUrl = decodeUrl; } public Set<String> getSensitiveHeaders() { return sensitiveHeaders; } public void setSensitiveHeaders(Set<String> sensitiveHeaders) { this.sensitiveHeaders = sensitiveHeaders; } public boolean isSslHostnameValidationEnabled() { return sslHostnameValidationEnabled; } public void setSslHostnameValidationEnabled(boolean sslHostnameValidationEnabled) { this.sslHostnameValidationEnabled = sslHostnameValidationEnabled; } public ExecutionIsolationStrategy getRibbonIsolationStrategy() { return ribbonIsolationStrategy; } public void setRibbonIsolationStrategy( ExecutionIsolationStrategy ribbonIsolationStrategy) { this.ribbonIsolationStrategy = ribbonIsolationStrategy; } public HystrixSemaphore getSemaphore() { return semaphore; } public void setSemaphore(HystrixSemaphore semaphore) { this.semaphore = semaphore; } public HystrixThreadPool getThreadPool() { return threadPool; } public void setThreadPool(HystrixThreadPool threadPool) { this.threadPool = threadPool; } public boolean isSetContentLength() { return setContentLength; } public void setSetContentLength(boolean setContentLength) { this.setContentLength = setContentLength; } public boolean isIncludeDebugHeader() { return includeDebugHeader; } public void setIncludeDebugHeader(boolean includeDebugHeader) { this.includeDebugHeader = includeDebugHeader; } public int getInitialStreamBufferSize() { return initialStreamBufferSize; } public void setInitialStreamBufferSize(int initialStreamBufferSize) { this.initialStreamBufferSize = initialStreamBufferSize; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ZuulProperties that = (ZuulProperties) o; return addHostHeader == that.addHostHeader && addProxyHeaders == that.addProxyHeaders && forceOriginalQueryStringEncoding == that.forceOriginalQueryStringEncoding && Objects.equals(host, that.host) && Objects.equals(ignoredHeaders, that.ignoredHeaders) && Objects.equals(ignoredPatterns, that.ignoredPatterns) && Objects.equals(ignoredServices, that.ignoredServices) && ignoreLocalService == that.ignoreLocalService && ignoreSecurityHeaders == that.ignoreSecurityHeaders && Objects.equals(prefix, that.prefix) && removeSemicolonContent == that.removeSemicolonContent && Objects.equals(retryable, that.retryable) && Objects.equals(ribbonIsolationStrategy, that.ribbonIsolationStrategy) && Objects.equals(routes, that.routes) && Objects.equals(semaphore, that.semaphore) && Objects.equals(sensitiveHeaders, that.sensitiveHeaders) && Objects.equals(servletPath, that.servletPath) && sslHostnameValidationEnabled == that.sslHostnameValidationEnabled && stripPrefix == that.stripPrefix && setContentLength == that.setContentLength && includeDebugHeader == that.includeDebugHeader && initialStreamBufferSize == that.initialStreamBufferSize && Objects.equals(threadPool, that.threadPool) && traceRequestBody == that.traceRequestBody; } @Override public int hashCode() { return Objects.hash(addHostHeader, addProxyHeaders, forceOriginalQueryStringEncoding, host, ignoredHeaders, ignoredPatterns, ignoredServices, ignoreLocalService, ignoreSecurityHeaders, prefix, removeSemicolonContent, retryable, ribbonIsolationStrategy, routes, semaphore, sensitiveHeaders, servletPath, sslHostnameValidationEnabled, stripPrefix, threadPool, traceRequestBody, setContentLength, includeDebugHeader, initialStreamBufferSize); } @Override public String toString() { return new StringBuilder("ZuulProperties{").append("prefix='").append(prefix) .append("', ").append("stripPrefix=").append(stripPrefix).append(", ") .append("retryable=").append(retryable).append(", ").append("routes=") .append(routes).append(", ").append("addProxyHeaders=") .append(addProxyHeaders).append(", ").append("addHostHeader=") .append(addHostHeader).append(", ").append("ignoredServices=") .append(ignoredServices).append(", ").append("ignoredPatterns=") .append(ignoredPatterns).append(", ").append("ignoredHeaders=") .append(ignoredHeaders).append(", ").append("ignoreSecurityHeaders=") .append(ignoreSecurityHeaders).append(", ") .append("forceOriginalQueryStringEncoding=") .append(forceOriginalQueryStringEncoding).append(", ") .append("servletPath='").append(servletPath).append("', ") .append("ignoreLocalService=").append(ignoreLocalService).append(", ") .append("host=").append(host).append(", ").append("traceRequestBody=") .append(traceRequestBody).append(", ").append("removeSemicolonContent=") .append(removeSemicolonContent).append(", ").append("sensitiveHeaders=") .append(sensitiveHeaders).append(", ") .append("sslHostnameValidationEnabled=") .append(sslHostnameValidationEnabled).append(", ") .append("ribbonIsolationStrategy=").append(ribbonIsolationStrategy) .append(", ").append("semaphore=").append(semaphore).append(", ") .append("threadPool=").append(threadPool).append(", ") .append("setContentLength=").append(setContentLength).append(", ") .append("includeDebugHeader=").append(includeDebugHeader).append(", ") .append("initialStreamBufferSize=").append(initialStreamBufferSize) .append(", ").append("}").toString(); } /** * Represents a Zuul route. */ public static class ZuulRoute { /** * The ID of the route (the same as its map key by default). */ private String id; /** * The path (pattern) for the route, e.g. /foo/**. */ private String path; /** * The service ID (if any) to map to this route. You can specify a physical URL or * a service, but not both. */ private String serviceId; /** * A full physical URL to map to the route. An alternative is to use a service ID * and service discovery to find the physical address. */ private String url; /** * Flag to determine whether the prefix for this route (the path, minus pattern * patcher) should be stripped before forwarding. */ private boolean stripPrefix = true; /** * Flag to indicate that this route should be retryable (if supported). Generally * retry requires a service ID and ribbon. */ private Boolean retryable; /** * List of sensitive headers that are not passed to downstream requests. Defaults * to a "safe" set of headers that commonly contain user credentials. It's OK to * remove those from the list if the downstream service is part of the same system * as the proxy, so they are sharing authentication data. If using a physical URL * outside your own domain, then generally it would be a bad idea to leak user * credentials. */ private Set<String> sensitiveHeaders = new LinkedHashSet<>(); private boolean customSensitiveHeaders = false; public ZuulRoute() { } public ZuulRoute(String id, String path, String serviceId, String url, boolean stripPrefix, Boolean retryable, Set<String> sensitiveHeaders) { this.id = id; this.path = path; this.serviceId = serviceId; this.url = url; this.stripPrefix = stripPrefix; this.retryable = retryable; this.sensitiveHeaders = sensitiveHeaders; this.customSensitiveHeaders = sensitiveHeaders != null; } 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]; } this.id = extractId(path); if (!path.startsWith("/")) { path = "/" + path; } setLocation(location); this.path = path; } public ZuulRoute(String path, String location) { this.id = extractId(path); this.path = path; setLocation(location); } public String getLocation() { if (StringUtils.hasText(this.url)) { return this.url; } return this.serviceId; } public void setLocation(String location) { if (location != null && (location.startsWith("http:") || location.startsWith("https:"))) { this.url = location; } else { this.serviceId = location; } } private String extractId(String path) { path = path.startsWith("/") ? path.substring(1) : path; path = path.replace("/*", "").replace("*", ""); return path; } public Route getRoute(String prefix) { return new Route(this.id, this.path, getLocation(), prefix, this.retryable, isCustomSensitiveHeaders() ? this.sensitiveHeaders : null, this.stripPrefix); } public boolean isCustomSensitiveHeaders() { return this.customSensitiveHeaders; } public void setCustomSensitiveHeaders(boolean customSensitiveHeaders) { this.customSensitiveHeaders = customSensitiveHeaders; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getServiceId() { return serviceId; } public void setServiceId(String serviceId) { this.serviceId = serviceId; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public boolean isStripPrefix() { return stripPrefix; } public void setStripPrefix(boolean stripPrefix) { this.stripPrefix = stripPrefix; } public Boolean getRetryable() { return retryable; } public void setRetryable(Boolean retryable) { this.retryable = retryable; } public Set<String> getSensitiveHeaders() { return sensitiveHeaders; } public void setSensitiveHeaders(Set<String> headers) { this.customSensitiveHeaders = true; this.sensitiveHeaders = new LinkedHashSet<>(headers); } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } ZuulRoute that = (ZuulRoute) o; return customSensitiveHeaders == that.customSensitiveHeaders && Objects.equals(id, that.id) && Objects.equals(path, that.path) && Objects.equals(retryable, that.retryable) && Objects.equals(sensitiveHeaders, that.sensitiveHeaders) && Objects.equals(serviceId, that.serviceId) && stripPrefix == that.stripPrefix && Objects.equals(url, that.url); } @Override public int hashCode() { return Objects.hash(customSensitiveHeaders, id, path, retryable, sensitiveHeaders, serviceId, stripPrefix, url); } @Override public String toString() { return new StringBuilder("ZuulRoute{").append("id='").append(id).append("', ") .append("path='").append(path).append("', ").append("serviceId='") .append(serviceId).append("', ").append("url='").append(url) .append("', ").append("stripPrefix=").append(stripPrefix).append(", ") .append("retryable=").append(retryable).append(", ") .append("sensitiveHeaders=").append(sensitiveHeaders).append(", ") .append("customSensitiveHeaders=").append(customSensitiveHeaders) .append(", ").append("}").toString(); } } /** * Represents a host. */ public static class Host { /** * The maximum number of total connections the proxy can hold open to backends. */ private int maxTotalConnections = 200; /** * The maximum number of connections that can be used by a single route. */ private int maxPerRouteConnections = 20; /** * The socket timeout in millis. Defaults to 10000. */ private int socketTimeoutMillis = 10000; /** * The connection timeout in millis. Defaults to 2000. */ private int connectTimeoutMillis = 2000; /** * The timeout in milliseconds used when requesting a connection from the * connection manager. Defaults to -1, undefined use the system default. */ private int connectionRequestTimeoutMillis = -1; /** * The lifetime for the connection pool. */ private long timeToLive = -1; /** * The time unit for timeToLive. */ private TimeUnit timeUnit = TimeUnit.MILLISECONDS; public Host() { } public Host(int maxTotalConnections, int maxPerRouteConnections, int socketTimeoutMillis, int connectTimeoutMillis, long timeToLive, TimeUnit timeUnit) { this.maxTotalConnections = maxTotalConnections; this.maxPerRouteConnections = maxPerRouteConnections; this.socketTimeoutMillis = socketTimeoutMillis; this.connectTimeoutMillis = connectTimeoutMillis; this.timeToLive = timeToLive; this.timeUnit = timeUnit; } public int getMaxTotalConnections() { return maxTotalConnections; } public void setMaxTotalConnections(int maxTotalConnections) { this.maxTotalConnections = maxTotalConnections; } public int getMaxPerRouteConnections() { return maxPerRouteConnections; } public void setMaxPerRouteConnections(int maxPerRouteConnections) { this.maxPerRouteConnections = maxPerRouteConnections; } public int getSocketTimeoutMillis() { return socketTimeoutMillis; } public void setSocketTimeoutMillis(int socketTimeoutMillis) { this.socketTimeoutMillis = socketTimeoutMillis; } public int getConnectTimeoutMillis() { return connectTimeoutMillis; } public void setConnectTimeoutMillis(int connectTimeoutMillis) { this.connectTimeoutMillis = connectTimeoutMillis; } public int getConnectionRequestTimeoutMillis() { return connectionRequestTimeoutMillis; } public void setConnectionRequestTimeoutMillis( int connectionRequestTimeoutMillis) { this.connectionRequestTimeoutMillis = connectionRequestTimeoutMillis; } public long getTimeToLive() { return timeToLive; } public void setTimeToLive(long timeToLive) { this.timeToLive = timeToLive; } public TimeUnit getTimeUnit() { return timeUnit; } public void setTimeUnit(TimeUnit timeUnit) { this.timeUnit = timeUnit; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Host host = (Host) o; return maxTotalConnections == host.maxTotalConnections && maxPerRouteConnections == host.maxPerRouteConnections && socketTimeoutMillis == host.socketTimeoutMillis && connectTimeoutMillis == host.connectTimeoutMillis && connectionRequestTimeoutMillis == host.connectionRequestTimeoutMillis && timeToLive == host.timeToLive && timeUnit == host.timeUnit; } @Override public int hashCode() { return Objects.hash(maxTotalConnections, maxPerRouteConnections, socketTimeoutMillis, connectTimeoutMillis, connectionRequestTimeoutMillis, timeToLive, timeUnit); } @Override public String toString() { return new ToStringCreator(this) .append("maxTotalConnections", maxTotalConnections) .append("maxPerRouteConnections", maxPerRouteConnections) .append("socketTimeoutMillis", socketTimeoutMillis) .append("connectTimeoutMillis", connectTimeoutMillis) .append("connectionRequestTimeoutMillis", connectionRequestTimeoutMillis) .append("timeToLive", timeToLive).append("timeUnit", timeUnit) .toString(); } } /** * Represents Hystrix Sempahores. */ public static class HystrixSemaphore { /** * The maximum number of total semaphores for Hystrix. */ private int maxSemaphores = 100; public HystrixSemaphore() { } public HystrixSemaphore(int maxSemaphores) { this.maxSemaphores = maxSemaphores; } public int getMaxSemaphores() { return maxSemaphores; } public void setMaxSemaphores(int maxSemaphores) { this.maxSemaphores = maxSemaphores; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } HystrixSemaphore that = (HystrixSemaphore) o; return maxSemaphores == that.maxSemaphores; } @Override public int hashCode() { return Objects.hash(maxSemaphores); } @Override public String toString() { final StringBuilder sb = new StringBuilder("HystrixSemaphore{"); sb.append("maxSemaphores=").append(maxSemaphores); sb.append('}'); return sb.toString(); } } /** * Represents Hystrix ThreadPool. */ public static class HystrixThreadPool { /** * Flag to determine whether RibbonCommands should use separate thread pools for * hystrix. By setting to true, RibbonCommands will be executed in a hystrix's * thread pool that it is associated with. Each RibbonCommand will be associated * with a thread pool according to its commandKey (serviceId). As default, all * commands will be executed in a single thread pool whose threadPoolKey is * "RibbonCommand". This property is only applicable when using THREAD as * ribbonIsolationStrategy */ private boolean useSeparateThreadPools = false; /** * A prefix for HystrixThreadPoolKey of hystrix's thread pool that is allocated to * each service Id. This property is only applicable when using THREAD as * ribbonIsolationStrategy and useSeparateThreadPools = true */ private String threadPoolKeyPrefix = ""; public boolean isUseSeparateThreadPools() { return useSeparateThreadPools; } public void setUseSeparateThreadPools(boolean useSeparateThreadPools) { this.useSeparateThreadPools = useSeparateThreadPools; } public String getThreadPoolKeyPrefix() { return threadPoolKeyPrefix; } public void setThreadPoolKeyPrefix(String threadPoolKeyPrefix) { this.threadPoolKeyPrefix = threadPoolKeyPrefix; } } }
这里只关注和Zuul流程相关的几个重要的Bean:
1. 程序入口
1. ZuulHandlerMapping, 相当于自定义SpringMVC的HandlerMapping
2. ZuulController,相当于自定义Controller,继承自org.springframework.web.servlet.mvc.Controller。内部将处理逻辑委托给ZuulServlet,这种方式走的是SpringMVC的处理机制,只不过内部将出炉逻辑委托给ZuulServlet。
3. ZuulServlet,处理代码的Servlet,继承自HttpServlet。
上面1、2、3 方式注入到Spring之后,相当于增加了另一套经过SpringMVC的访问机制,和我们自定义的@GetMapping 是平级的一套机制。
4. ServletRegistrationBean 注册的方式,将ZuulServlet 到tomcat的servlet列表。相当于和SpringMVC的DispatcherServlet 平级的一个Servlet, 调用的时候先过该Servlet,路由匹配失败后走SpringMVC的默认的DispatcherServlet 。
上面1,2,3,4 是zuul 拦截程序的入口。一种依赖于SpringMVC,一种相当于走Zuul的自己的路径,默认是/zuul, 可以自己通过zuul.servletPath 进行配置。参考:org.springframework.cloud.netflix.zuul.filters.ZuulProperties
2. RouteLocator 路由查询器
可以认为这部分是进行路由匹配的一系列Locator。包括一系列根据服务名替换路由等规则是走的这里的配置。 其实根本是读的org.springframework.cloud.netflix.zuul.filters.ZuulProperties#routes 维护的信息。
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<? extends RouteLocator> routeLocators; private ArrayList<RouteLocator> rl; public CompositeRouteLocator(Collection<? extends RouteLocator> routeLocators) { Assert.notNull(routeLocators, "'routeLocators' must not be null"); rl = new ArrayList<>(routeLocators); AnnotationAwareOrderComparator.sort(rl); this.routeLocators = rl; } @Override public Collection<String> getIgnoredPaths() { List<String> ignoredPaths = new ArrayList<>(); for (RouteLocator locator : routeLocators) { ignoredPaths.addAll(locator.getIgnoredPaths()); } return ignoredPaths; } @Override public List<Route> getRoutes() { List<Route> 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(); } } } }
在org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping#setDirty 会调用上面的refresh 方法。
public void setDirty(boolean dirty) { this.dirty = dirty; if (this.routeLocator instanceof RefreshableRouteLocator) { ((RefreshableRouteLocator) this.routeLocator).refresh(); } }
refresh 调用到: org.springframework.cloud.netflix.zuul.filters.discovery.DiscoveryClientRouteLocator#locateRoutes
@Override protected LinkedHashMap<String, ZuulRoute> locateRoutes() { LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<>(); routesMap.putAll(super.locateRoutes()); if (this.discovery != null) { Map<String, ZuulRoute> staticServices = new LinkedHashMap<>(); for (ZuulRoute route : routesMap.values()) { String serviceId = route.getServiceId(); if (serviceId == null) { serviceId = route.getId(); } if (serviceId != null) { staticServices.put(serviceId, route); } } // Add routes for discovery services by default List<String> services = this.discovery.getServices(); String[] ignored = this.properties.getIgnoredServices() .toArray(new String[0]); for (String serviceId : services) { // Ignore specifically ignored services and those that were manually // configured String key = "/" + mapRouteToService(serviceId) + "/**"; if (staticServices.containsKey(serviceId) && staticServices.get(serviceId).getUrl() == null) { // Explicitly configured with no URL, cannot be ignored // all static routes are already in routesMap // Update location using serviceId if location is null ZuulRoute staticRoute = staticServices.get(serviceId); if (!StringUtils.hasText(staticRoute.getLocation())) { staticRoute.setLocation(serviceId); } } if (!PatternMatchUtils.simpleMatch(ignored, serviceId) && !routesMap.containsKey(key)) { // Not ignored routesMap.put(key, new ZuulRoute(key, serviceId)); } } } if (routesMap.get(DEFAULT_ROUTE) != null) { ZuulRoute defaultRoute = routesMap.get(DEFAULT_ROUTE); // Move the defaultServiceId to the end routesMap.remove(DEFAULT_ROUTE); routesMap.put(DEFAULT_ROUTE, defaultRoute); } LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>(); for (Entry<String, ZuulRoute> entry : routesMap.entrySet()) { String path = entry.getKey(); // Prepend with slash if not already present. if (!path.startsWith("/")) { path = "/" + path; } if (StringUtils.hasText(this.properties.getPrefix())) { path = this.properties.getPrefix() + path; if (!path.startsWith("/")) { path = "/" + path; } } values.put(path, entry.getValue()); } return values; }
3. filter 用于执行处理流程的一系列过滤器。
1. FormBodyWrapperFilter、ServletDetectionFilter、SendResponseFilter 一系列的Filter, com.netflix.zuul.ZuulFilter#filterType 方法标记其处理的类型。从源码org.springframework.cloud.netflix.zuul.filters.support.FilterConstants 也可以看出其包含的filter 类型包括:
/** * {@link ZuulFilter#filterType()} error type. */ public static final String ERROR_TYPE = "error"; /** * {@link ZuulFilter#filterType()} post type. */ public static final String POST_TYPE = "post"; /** * {@link ZuulFilter#filterType()} pre type. */ public static final String PRE_TYPE = "pre"; /** * {@link ZuulFilter#filterType()} route type. */ public static final String ROUTE_TYPE = "route";
2. 注入的重要的filter
前置过滤器
- `ServletDetectionFilter`:检测请求是否通过Spring调度程序。使用键`FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY`设置布尔值。
- `FormBodyWrapperFilter`:解析表单数据,并为下游请求重新编码它。
- `DebugFilter`:如果设置`debug`请求参数,则此过滤器将`RequestContext.setDebugRouting()`和`RequestContext.setDebugRequest()`设置为true。
路由过滤器
- `SendForwardFilter`:此过滤器使用Servlet `RequestDispatcher`转发请求。转发位置存储在`RequestContext`属性`FilterConstants.FORWARD_TO_KEY`中。这对于转发到当前应用程序中的端点很有用。
过滤器:
- `SendResponseFilter`:将代理请求的响应写入当前响应。
错误过滤器:
- `SendErrorFilter`:如果`RequestContext.getThrowable()`不为null,则转发到/错误(默认情况下)。可以通过设置`error.path`属性来更改默认转发路径(`/error`)。
3. org.springframework.cloud.netflix.zuul.ZuulFilterInitializer 是维护ZuulFilter的一个重要类,自动注入容器中的ZuulFilter 对象,然后进行分类后维护到com.netflix.zuul.FilterLoader 对象内,相关源码:
(1) ZuulFilterInitializer
public class ZuulFilterInitializer { private static final Log log = LogFactory.getLog(ZuulFilterInitializer.class); private final Map<String, ZuulFilter> filters; private final CounterFactory counterFactory; private final TracerFactory tracerFactory; private final FilterLoader filterLoader; private final FilterRegistry filterRegistry; public ZuulFilterInitializer(Map<String, ZuulFilter> filters, CounterFactory counterFactory, TracerFactory tracerFactory, FilterLoader filterLoader, FilterRegistry filterRegistry) { this.filters = filters; this.counterFactory = counterFactory; this.tracerFactory = tracerFactory; this.filterLoader = filterLoader; this.filterRegistry = filterRegistry; } @PostConstruct public void contextInitialized() { log.info("Starting filter initializer"); TracerFactory.initialize(tracerFactory); CounterFactory.initialize(counterFactory); for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) { filterRegistry.put(entry.getKey(), entry.getValue()); } } ...
(2) com.netflix.zuul.FilterLoader
public class FilterLoader { final static FilterLoader INSTANCE = new FilterLoader(); private static final Logger LOG = LoggerFactory.getLogger(FilterLoader.class); private final ConcurrentHashMap<String, Long> filterClassLastModified = new ConcurrentHashMap<String, Long>(); private final ConcurrentHashMap<String, String> filterClassCode = new ConcurrentHashMap<String, String>(); private final ConcurrentHashMap<String, String> filterCheck = new ConcurrentHashMap<String, String>(); private final ConcurrentHashMap<String, List<ZuulFilter>> hashFiltersByType = new ConcurrentHashMap<String, List<ZuulFilter>>(); ...
最后维护到hashFiltersByType 的filters 如下:
3. 调用原理
1. 前置
filter 是zuul 处理流程的重要组件。ZuulServlet 是程序入口;ZuulFilter 是程序处理的逻辑,以链条的设计模式进行调用;其中RouteLocator 在Filter 中发挥作用,用于匹配解析相关的路由,然后存到RequestContext, RequestContext 继承自ConcurrentHashMap, 用于维护当前请求的一些上下文参数, 内部用threadLocal 维护当前请求的当前RequestContext。整个ZullFilter 链条 用RequestContext 进行数据传递,且key 在常量类中定义。
参考: com.netflix.zuul.context.RequestContext 和 org.springframework.cloud.netflix.zuul.filters.support.FilterConstants 类。
不管哪种方式的调用最终都会经过ZuulServlet。ZuulServlet 代码如下:
public class ZuulServlet extends HttpServlet { private static final long serialVersionUID = -3374242278843351500L; private ZuulRunner zuulRunner; @Override public void init(ServletConfig config) throws ServletException { super.init(config); String bufferReqsStr = config.getInitParameter("buffer-requests"); boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true") ? true : false; zuulRunner = new ZuulRunner(bufferReqs); } @Override public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException { try { init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); // Marks this request as having passed through the "Zuul engine", as opposed to servlets // explicitly bound in web.xml, for which requests will not have the same data attached RequestContext context = RequestContext.getCurrentContext(); context.setZuulEngineRan(); try { preRoute(); } catch (ZuulException e) { error(e); postRoute(); return; } try { route(); } catch (ZuulException e) { error(e); postRoute(); return; } try { postRoute(); } catch (ZuulException e) { error(e); return; } } catch (Throwable e) { error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName())); } finally { RequestContext.getCurrentContext().unset(); } } /** * executes "post" ZuulFilters * * @throws ZuulException */ void postRoute() throws ZuulException { zuulRunner.postRoute(); } /** * executes "route" filters * * @throws ZuulException */ void route() throws ZuulException { zuulRunner.route(); } /** * executes "pre" filters * * @throws ZuulException */ void preRoute() throws ZuulException { zuulRunner.preRoute(); } /** * initializes request * * @param servletRequest * @param servletResponse */ void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) { zuulRunner.init(servletRequest, servletResponse); } /** * sets error context info and executes "error" filters * * @param e */ void error(ZuulException e) { RequestContext.getCurrentContext().setThrowable(e); zuulRunner.error(); } @RunWith(MockitoJUnitRunner.class) public static class UnitTest { @Mock HttpServletRequest servletRequest; @Mock HttpServletResponseWrapper servletResponse; @Mock FilterProcessor processor; @Mock PrintWriter writer; @Before public void before() { MockitoAnnotations.initMocks(this); } @Test public void testProcessZuulFilter() { ZuulServlet zuulServlet = new ZuulServlet(); zuulServlet = spy(zuulServlet); RequestContext context = spy(RequestContext.getCurrentContext()); try { FilterProcessor.setProcessor(processor); RequestContext.testSetCurrentContext(context); when(servletResponse.getWriter()).thenReturn(writer); zuulServlet.init(servletRequest, servletResponse); verify(zuulServlet, times(1)).init(servletRequest, servletResponse); assertTrue(RequestContext.getCurrentContext().getRequest() instanceof HttpServletRequestWrapper); assertTrue(RequestContext.getCurrentContext().getResponse() instanceof HttpServletResponseWrapper); zuulServlet.preRoute(); verify(processor, times(1)).preRoute(); zuulServlet.postRoute(); verify(processor, times(1)).postRoute(); // verify(context, times(1)).unset(); zuulServlet.route(); verify(processor, times(1)).route(); RequestContext.testSetCurrentContext(null); } catch (Exception e) { e.printStackTrace(); } } } }
2.
这种配置方式经过SpringMVC 路由,由SpringMVC的DispatcherServlet 进行转发。调用链如下:
接下来研究其调用过程:
从 com.netflix.zuul.http.ZuulServlet#service 代码看出,其过程分为路由前、路由、路由后, 如果发生错误就走error类型的filter。其中RequestContext 用于整个流程中的数据共享。各种filter的运行会走到ZuulRunner。
/** * sets HttpServlet request and HttpResponse * * @param servletRequest * @param servletResponse */ public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) { RequestContext ctx = RequestContext.getCurrentContext(); if (bufferRequests) { ctx.setRequest(new HttpServletRequestWrapper(servletRequest)); } else { ctx.setRequest(servletRequest); } ctx.setResponse(new HttpServletResponseWrapper(servletResponse)); } /** * executes "post" filterType ZuulFilters * * @throws ZuulException */ public void postRoute() throws ZuulException { FilterProcessor.getInstance().postRoute(); } /** * executes "route" filterType ZuulFilters * * @throws ZuulException */ public void route() throws ZuulException { FilterProcessor.getInstance().route(); } /** * executes "pre" filterType ZuulFilters * * @throws ZuulException */ public void preRoute() throws ZuulException { FilterProcessor.getInstance().preRoute(); } /** * executes "error" filterType ZuulFilters */ public void error() { FilterProcessor.getInstance().error(); }
/** * runs "post" filters which are called after "route" filters. ZuulExceptions from ZuulFilters are thrown. * Any other Throwables are caught and a ZuulException is thrown out with a 500 status code * * @throws ZuulException */ public void postRoute() throws ZuulException { try { runFilters("post"); } catch (ZuulException e) { throw e; } catch (Throwable e) { throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_POST_FILTER_" + e.getClass().getName()); } } /** * runs all "error" filters. These are called only if an exception occurs. Exceptions from this are swallowed and logged so as not to bubble up. */ public void error() { try { runFilters("error"); } catch (Throwable e) { logger.error(e.getMessage(), e); } } /** * Runs all "route" filters. These filters route calls to an origin. * * @throws ZuulException if an exception occurs. */ public void route() throws ZuulException { try { runFilters("route"); } catch (ZuulException e) { throw e; } catch (Throwable e) { throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName()); } } /** * runs all "pre" filters. These filters are run before routing to the orgin. * * @throws ZuulException */ public void preRoute() throws ZuulException { try { runFilters("pre"); } catch (ZuulException e) { throw e; } catch (Throwable e) { throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName()); } } /** * runs all filters of the filterType sType/ Use this method within filters to run custom filters by type * * @param sType the filterType. * @return * @throws Throwable throws up an arbitrary exception */ public Object runFilters(String sType) throws Throwable { if (RequestContext.getCurrentContext().debugRouting()) { Debug.addRoutingDebug("Invoking {" + sType + "} type filters"); } boolean bResult = false; List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType); if (list != null) { for (int i = 0; i < list.size(); i++) { ZuulFilter zuulFilter = list.get(i); Object result = processZuulFilter(zuulFilter); if (result != null && result instanceof Boolean) { bResult |= ((Boolean) result); } } } return bResult; } /** * Processes an individual ZuulFilter. This method adds Debug information. Any uncaught Thowables are caught by this method and converted to a ZuulException with a 500 status code. * * @param filter * @return the return value for that filter * @throws ZuulException */ public Object processZuulFilter(ZuulFilter filter) throws ZuulException { RequestContext ctx = RequestContext.getCurrentContext(); boolean bDebug = ctx.debugRouting(); final String metricPrefix = "zuul.filter-"; long execTime = 0; String filterName = ""; try { long ltime = System.currentTimeMillis(); filterName = filter.getClass().getSimpleName(); RequestContext copy = null; Object o = null; Throwable t = null; if (bDebug) { Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName); copy = ctx.copy(); } ZuulFilterResult result = filter.runFilter(); ExecutionStatus s = result.getStatus(); execTime = System.currentTimeMillis() - ltime; switch (s) { case FAILED: t = result.getException(); ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime); break; case SUCCESS: o = result.getResult(); ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime); if (bDebug) { Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms"); Debug.compareContextState(filterName, copy); } break; default: break; } if (t != null) throw t; usageNotifier.notify(filter, s); return o; } catch (Throwable e) { if (bDebug) { Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage()); } usageNotifier.notify(filter, ExecutionStatus.FAILED); if (e instanceof ZuulException) { throw (ZuulException) e; } else { ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName); ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime); throw ex; } } }
1. preFilter
包含6个前置处理器。
public class ServletDetectionFilter extends ZuulFilter { public ServletDetectionFilter() { } @Override public String filterType() { return PRE_TYPE; } /** * Must run before other filters that rely on the difference between DispatcherServlet * and ZuulServlet. */ @Override public int filterOrder() { return SERVLET_DETECTION_FILTER_ORDER; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); if (!(request instanceof HttpServletRequestWrapper) && isDispatcherServletRequest(request)) { ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, true); } else { ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, false); } return null; } private boolean isDispatcherServletRequest(HttpServletRequest request) { return request.getAttribute( DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null; } }
这个filter 判断请求是来自SpringMVC的Dispatcher还是直接来自ZuulServlet,也就是如果以/cloud-zuul/ 开头的路由这个是false,否则放的状态位为true。
// Make framework objects available to handlers and view objects. request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext()); request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
2. org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter
这个ilter 根据request 请求来源选择性包装,如果是来自SpringMVC 就包装,否则就不包装。
public class Servlet30WrapperFilter extends ZuulFilter { private Field requestField = null; public Servlet30WrapperFilter() { this.requestField = ReflectionUtils.findField(HttpServletRequestWrapper.class, "req", HttpServletRequest.class); Assert.notNull(this.requestField, "HttpServletRequestWrapper.req field not found"); this.requestField.setAccessible(true); } protected Field getRequestField() { return this.requestField; } @Override public String filterType() { return PRE_TYPE; } @Override public int filterOrder() { return SERVLET_30_WRAPPER_FILTER_ORDER; } @Override public boolean shouldFilter() { return true; // TODO: only if in servlet 3.0 env } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); if (request instanceof HttpServletRequestWrapper) { request = (HttpServletRequest) ReflectionUtils.getField(this.requestField, request); ctx.setRequest(new Servlet30RequestWrapper(request)); } else if (RequestUtils.isDispatcherServletRequest()) { // If it's going through the dispatcher we need to buffer the body ctx.setRequest(new Servlet30RequestWrapper(request)); } return null; } }
3. cn.qz.cloud.filter.PreFilter
这是自己的前置过滤器,打印一些日志。
4. org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter
public class FormBodyWrapperFilter extends ZuulFilter { private FormHttpMessageConverter formHttpMessageConverter; private Field requestField; private Field servletRequestField; public FormBodyWrapperFilter() { this(new AllEncompassingFormHttpMessageConverter()); } public FormBodyWrapperFilter(FormHttpMessageConverter formHttpMessageConverter) { this.formHttpMessageConverter = formHttpMessageConverter; this.requestField = ReflectionUtils.findField(HttpServletRequestWrapper.class, "req", HttpServletRequest.class); this.servletRequestField = ReflectionUtils.findField(ServletRequestWrapper.class, "request", ServletRequest.class); Assert.notNull(this.requestField, "HttpServletRequestWrapper.req field not found"); Assert.notNull(this.servletRequestField, "ServletRequestWrapper.request field not found"); this.requestField.setAccessible(true); this.servletRequestField.setAccessible(true); } @Override public String filterType() { return PRE_TYPE; } @Override public int filterOrder() { return FORM_BODY_WRAPPER_FILTER_ORDER; } @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String contentType = request.getContentType(); // Don't use this filter on GET method if (contentType == null) { return false; } // Only use this filter for form data and only for multipart data in a // DispatcherServlet handler try { MediaType mediaType = MediaType.valueOf(contentType); return MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType) || (isDispatcherServletRequest(request) && MediaType.MULTIPART_FORM_DATA.includes(mediaType)); } catch (InvalidMediaTypeException ex) { return false; } } private boolean isDispatcherServletRequest(HttpServletRequest request) { return request.getAttribute( DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); FormBodyRequestWrapper wrapper = null; if (request instanceof HttpServletRequestWrapper) { HttpServletRequest wrapped = (HttpServletRequest) ReflectionUtils .getField(this.requestField, request); wrapper = new FormBodyRequestWrapper(wrapped); ReflectionUtils.setField(this.requestField, request, wrapper); if (request instanceof ServletRequestWrapper) { ReflectionUtils.setField(this.servletRequestField, request, wrapper); } } else { wrapper = new FormBodyRequestWrapper(request); ctx.setRequest(wrapper); } if (wrapper != null) { ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType()); } return null; } private class FormBodyRequestWrapper extends Servlet30RequestWrapper { private HttpServletRequest request; private volatile byte[] contentData; private MediaType contentType; private int contentLength; FormBodyRequestWrapper(HttpServletRequest request) { super(request); this.request = request; } @Override public String getContentType() { if (this.contentData == null) { buildContentData(); } return this.contentType.toString(); } @Override public int getContentLength() { if (super.getContentLength() <= 0) { return super.getContentLength(); } if (this.contentData == null) { buildContentData(); } return this.contentLength; } public long getContentLengthLong() { return getContentLength(); } @Override public ServletInputStream getInputStream() throws IOException { if (this.contentData == null) { buildContentData(); } return new ServletInputStreamWrapper(this.contentData); } private synchronized void buildContentData() { if (this.contentData != null) { return; } try { MultiValueMap<String, Object> builder = RequestContentDataExtractor .extract(this.request); FormHttpOutputMessage data = new FormHttpOutputMessage(); this.contentType = MediaType.valueOf(this.request.getContentType()); data.getHeaders().setContentType(this.contentType); FormBodyWrapperFilter.this.formHttpMessageConverter.write(builder, this.contentType, data); // copy new content type including multipart boundary this.contentType = data.getHeaders().getContentType(); byte[] input = data.getInput(); this.contentLength = input.length; this.contentData = input; } catch (Exception e) { throw new IllegalStateException("Cannot convert form data", e); } } private class FormHttpOutputMessage implements HttpOutputMessage { private HttpHeaders headers = new HttpHeaders(); private ByteArrayOutputStream output = new ByteArrayOutputStream(); @Override public HttpHeaders getHeaders() { return this.headers; } @Override public OutputStream getBody() throws IOException { return this.output; } public byte[] getInput() throws IOException { this.output.flush(); return this.output.toByteArray(); } } } }
5. org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter
public class DebugFilter extends ZuulFilter { private static final DynamicBooleanProperty ROUTING_DEBUG = DynamicPropertyFactory .getInstance().getBooleanProperty(ZuulConstants.ZUUL_DEBUG_REQUEST, false); private static final DynamicStringProperty DEBUG_PARAMETER = DynamicPropertyFactory .getInstance().getStringProperty(ZuulConstants.ZUUL_DEBUG_PARAMETER, "debug"); @Override public String filterType() { return PRE_TYPE; } @Override public int filterOrder() { return DEBUG_FILTER_ORDER; } @Override public boolean shouldFilter() { HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) { return true; } return ROUTING_DEBUG.get(); } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); ctx.setDebugRouting(true); ctx.setDebugRequest(true); return null; } }
debug 设置debug参数。默认是false, 不走该filter。
6. org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter
public class PreDecorationFilter extends ZuulFilter { private static final Log log = LogFactory.getLog(PreDecorationFilter.class); /** * @deprecated use {@link FilterConstants#PRE_DECORATION_FILTER_ORDER} */ @Deprecated public static final int FILTER_ORDER = PRE_DECORATION_FILTER_ORDER; /** * A double slash pattern. */ public static final Pattern DOUBLE_SLASH = Pattern.compile("//"); private RouteLocator routeLocator; private String dispatcherServletPath; private ZuulProperties properties; private UrlPathHelper urlPathHelper = new UrlPathHelper(); private ProxyRequestHelper proxyRequestHelper; public PreDecorationFilter(RouteLocator routeLocator, String dispatcherServletPath, ZuulProperties properties, ProxyRequestHelper proxyRequestHelper) { this.routeLocator = routeLocator; this.properties = properties; this.urlPathHelper .setRemoveSemicolonContent(properties.isRemoveSemicolonContent()); this.urlPathHelper.setUrlDecode(properties.isDecodeUrl()); this.dispatcherServletPath = dispatcherServletPath; this.proxyRequestHelper = proxyRequestHelper; } @Override public int filterOrder() { return PRE_DECORATION_FILTER_ORDER; } @Override public String filterType() { return PRE_TYPE; } @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded && !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined // serviceId } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); final String requestURI = this.urlPathHelper .getPathWithinApplication(ctx.getRequest()); Route route = this.routeLocator.getMatchingRoute(requestURI); if (route != null) { String location = route.getLocation(); if (location != null) { ctx.put(REQUEST_URI_KEY, route.getPath()); ctx.put(PROXY_KEY, route.getId()); if (!route.isCustomSensitiveHeaders()) { this.proxyRequestHelper.addIgnoredHeaders( this.properties.getSensitiveHeaders().toArray(new String[0])); } else { this.proxyRequestHelper.addIgnoredHeaders( route.getSensitiveHeaders().toArray(new String[0])); } if (route.getRetryable() != null) { ctx.put(RETRYABLE_KEY, route.getRetryable()); } if (location.startsWith(HTTP_SCHEME + ":") || location.startsWith(HTTPS_SCHEME + ":")) { ctx.setRouteHost(getUrl(location)); ctx.addOriginResponseHeader(SERVICE_HEADER, location); } else if (location.startsWith(FORWARD_LOCATION_PREFIX)) { ctx.set(FORWARD_TO_KEY, StringUtils.cleanPath( location.substring(FORWARD_LOCATION_PREFIX.length()) + route.getPath())); ctx.setRouteHost(null); return null; } else { // set serviceId for use in filters.route.RibbonRequest ctx.set(SERVICE_ID_KEY, location); ctx.setRouteHost(null); ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location); } if (this.properties.isAddProxyHeaders()) { addProxyHeaders(ctx, route); String xforwardedfor = ctx.getRequest() .getHeader(X_FORWARDED_FOR_HEADER); String remoteAddr = ctx.getRequest().getRemoteAddr(); if (xforwardedfor == null) { xforwardedfor = remoteAddr; } else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates xforwardedfor += ", " + remoteAddr; } ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor); } if (this.properties.isAddHostHeader()) { ctx.addZuulRequestHeader(HttpHeaders.HOST, toHostHeader(ctx.getRequest())); } } } else { log.warn("No route found for uri: " + requestURI); String forwardURI = getForwardUri(requestURI); ctx.set(FORWARD_TO_KEY, forwardURI); } return null; } /* for testing */ String getForwardUri(String requestURI) { // default fallback servlet is DispatcherServlet String fallbackPrefix = this.dispatcherServletPath; String fallBackUri = requestURI; if (RequestUtils.isZuulServletRequest()) { // remove the Zuul servletPath from the requestUri log.debug("zuulServletPath=" + this.properties.getServletPath()); fallBackUri = fallBackUri.replaceFirst(this.properties.getServletPath(), ""); log.debug("Replaced Zuul servlet path:" + fallBackUri); } else if (this.dispatcherServletPath != null) { // remove the DispatcherServlet servletPath from the requestUri log.debug("dispatcherServletPath=" + this.dispatcherServletPath); fallBackUri = fallBackUri.replaceFirst(this.dispatcherServletPath, ""); log.debug("Replaced DispatcherServlet servlet path:" + fallBackUri); } if (!fallBackUri.startsWith("/")) { fallBackUri = "/" + fallBackUri; } String forwardURI = (fallbackPrefix == null) ? fallBackUri : fallbackPrefix + fallBackUri; forwardURI = DOUBLE_SLASH.matcher(forwardURI).replaceAll("/"); return forwardURI; } private void addProxyHeaders(RequestContext ctx, Route route) { HttpServletRequest request = ctx.getRequest(); String host = toHostHeader(request); String port = String.valueOf(request.getServerPort()); String proto = request.getScheme(); if (hasHeader(request, X_FORWARDED_HOST_HEADER)) { host = request.getHeader(X_FORWARDED_HOST_HEADER) + "," + host; } if (!hasHeader(request, X_FORWARDED_PORT_HEADER)) { if (hasHeader(request, X_FORWARDED_PROTO_HEADER)) { StringBuilder builder = new StringBuilder(); for (String previous : StringUtils.commaDelimitedListToStringArray( request.getHeader(X_FORWARDED_PROTO_HEADER))) { if (builder.length() > 0) { builder.append(","); } builder.append( HTTPS_SCHEME.equals(previous) ? HTTPS_PORT : HTTP_PORT); } builder.append(",").append(port); port = builder.toString(); } } else { port = request.getHeader(X_FORWARDED_PORT_HEADER) + "," + port; } if (hasHeader(request, X_FORWARDED_PROTO_HEADER)) { proto = request.getHeader(X_FORWARDED_PROTO_HEADER) + "," + proto; } ctx.addZuulRequestHeader(X_FORWARDED_HOST_HEADER, host); ctx.addZuulRequestHeader(X_FORWARDED_PORT_HEADER, port); ctx.addZuulRequestHeader(X_FORWARDED_PROTO_HEADER, proto); addProxyPrefix(ctx, route); } private boolean hasHeader(HttpServletRequest request, String name) { return StringUtils.hasLength(request.getHeader(name)); } private void addProxyPrefix(RequestContext ctx, Route route) { String forwardedPrefix = ctx.getRequest().getHeader(X_FORWARDED_PREFIX_HEADER); String contextPath = ctx.getRequest().getContextPath(); String prefix = StringUtils.hasLength(forwardedPrefix) ? forwardedPrefix : (StringUtils.hasLength(contextPath) ? contextPath : null); if (StringUtils.hasText(route.getPrefix())) { StringBuilder newPrefixBuilder = new StringBuilder(); if (prefix != null) { if (prefix.endsWith("/") && route.getPrefix().startsWith("/")) { newPrefixBuilder.append(prefix, 0, prefix.length() - 1); } else { newPrefixBuilder.append(prefix); } } newPrefixBuilder.append(route.getPrefix()); prefix = newPrefixBuilder.toString(); } if (prefix != null) { ctx.addZuulRequestHeader(X_FORWARDED_PREFIX_HEADER, prefix); } } private String toHostHeader(HttpServletRequest request) { int port = request.getServerPort(); if ((port == HTTP_PORT && HTTP_SCHEME.equals(request.getScheme())) || (port == HTTPS_PORT && HTTPS_SCHEME.equals(request.getScheme()))) { return request.getServerName(); } else { return request.getServerName() + ":" + port; } } private URL getUrl(String target) { try { return new URL(target); } catch (MalformedURLException ex) { throw new IllegalStateException("Target URL is malformed", ex); } } }
2. routeFilter 路由处理器
public class RibbonRoutingFilter extends ZuulFilter { private static final Log log = LogFactory.getLog(RibbonRoutingFilter.class); protected ProxyRequestHelper helper; protected RibbonCommandFactory<?> ribbonCommandFactory; protected List<RibbonRequestCustomizer> requestCustomizers; private boolean useServlet31 = true; public RibbonRoutingFilter(ProxyRequestHelper helper, RibbonCommandFactory<?> ribbonCommandFactory, List<RibbonRequestCustomizer> requestCustomizers) { this.helper = helper; this.ribbonCommandFactory = ribbonCommandFactory; this.requestCustomizers = requestCustomizers; // To support Servlet API 3.1 we need to check if getContentLengthLong exists // Spring 5 minimum support is 3.0, so this stays try { HttpServletRequest.class.getMethod("getContentLengthLong"); } catch (NoSuchMethodException e) { useServlet31 = false; } } @Deprecated // TODO Remove in 2.1.x public RibbonRoutingFilter(RibbonCommandFactory<?> ribbonCommandFactory) { this(new ProxyRequestHelper(), ribbonCommandFactory, null); } /* for testing */ boolean isUseServlet31() { return useServlet31; } @Override public String filterType() { return ROUTE_TYPE; } @Override public int filterOrder() { return RIBBON_ROUTING_FILTER_ORDER; } @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null && ctx.sendZuulResponse()); } @Override public Object run() { RequestContext context = RequestContext.getCurrentContext(); this.helper.addIgnoredHeaders(); try { RibbonCommandContext commandContext = buildCommandContext(context); ClientHttpResponse response = forward(commandContext); setResponse(response); return response; } catch (ZuulException ex) { throw new ZuulRuntimeException(ex); } catch (Exception ex) { throw new ZuulRuntimeException(ex); } } protected RibbonCommandContext buildCommandContext(RequestContext context) { HttpServletRequest request = context.getRequest(); MultiValueMap<String, String> headers = this.helper .buildZuulRequestHeaders(request); MultiValueMap<String, String> params = this.helper .buildZuulRequestQueryParams(request); String verb = getVerb(request); InputStream requestEntity = getRequestBody(request); if (request.getContentLength() < 0 && !verb.equalsIgnoreCase("GET")) { context.setChunkedRequestBody(); } String serviceId = (String) context.get(SERVICE_ID_KEY); Boolean retryable = (Boolean) context.get(RETRYABLE_KEY); Object loadBalancerKey = context.get(LOAD_BALANCER_KEY); String uri = this.helper.buildZuulRequestURI(request); // remove double slashes uri = uri.replace("//", "/"); long contentLength = useServlet31 ? request.getContentLengthLong() : request.getContentLength(); return new RibbonCommandContext(serviceId, verb, uri, retryable, headers, params, requestEntity, this.requestCustomizers, contentLength, loadBalancerKey); } protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception { Map<String, Object> info = this.helper.debug(context.getMethod(), context.getUri(), context.getHeaders(), context.getParams(), context.getRequestEntity()); RibbonCommand command = this.ribbonCommandFactory.create(context); try { ClientHttpResponse response = command.execute(); this.helper.appendDebug(info, response.getRawStatusCode(), response.getHeaders()); return response; } catch (HystrixRuntimeException ex) { return handleException(info, ex); } } protected ClientHttpResponse handleException(Map<String, Object> info, HystrixRuntimeException ex) throws ZuulException { int statusCode = HttpStatus.INTERNAL_SERVER_ERROR.value(); Throwable cause = ex; String message = ex.getFailureType().toString(); ClientException clientException = findClientException(ex); if (clientException == null) { clientException = findClientException(ex.getFallbackException()); } if (clientException != null) { if (clientException .getErrorType() == ClientException.ErrorType.SERVER_THROTTLED) { statusCode = HttpStatus.SERVICE_UNAVAILABLE.value(); } cause = clientException; message = clientException.getErrorType().toString(); } info.put("status", String.valueOf(statusCode)); throw new ZuulException(cause, "Forwarding error", statusCode, message); } protected ClientException findClientException(Throwable t) { if (t == null) { return null; } if (t instanceof ClientException) { return (ClientException) t; } return findClientException(t.getCause()); } protected InputStream getRequestBody(HttpServletRequest request) { InputStream requestEntity = null; try { requestEntity = (InputStream) RequestContext.getCurrentContext() .get(REQUEST_ENTITY_KEY); if (requestEntity == null) { requestEntity = request.getInputStream(); } } catch (IOException ex) { log.error("Error during getRequestBody", ex); } return requestEntity; } protected String getVerb(HttpServletRequest request) { String method = request.getMethod(); if (method == null) { return "GET"; } return method; } protected void setResponse(ClientHttpResponse resp) throws ClientException, IOException { RequestContext.getCurrentContext().set("zuulResponse", resp); this.helper.setResponse(resp.getRawStatusCode(), resp.getBody() == null ? null : resp.getBody(), resp.getHeaders()); } }
@Override protected ClientHttpResponse run() throws Exception { final RequestContext context = RequestContext.getCurrentContext(); RQ request = createRequest(); RS response; boolean retryableClient = this.client instanceof AbstractLoadBalancingClient && ((AbstractLoadBalancingClient) this.client) .isClientRetryable((ContextAwareRequest) request); if (retryableClient) { response = this.client.execute(request, config); } else { response = this.client.executeWithLoadBalancer(request, config); } context.set("ribbonResponse", response); // Explicitly close the HttpResponse if the Hystrix command timed out to // release the underlying HTTP connection held by the response. // if (this.isResponseTimedOut()) { if (response != null) { response.close(); } } return new RibbonHttpResponse(response); }
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException { LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig); try { return command.submit( new ServerOperation<T>() { @Override public Observable<T> call(Server server) { URI finalUri = reconstructURIWithServer(server, request.getUri()); S requestForServer = (S) request.replaceUri(finalUri); try { return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig)); } catch (Exception e) { return Observable.error(e); } } }) .toBlocking() .single(); } catch (Exception e) { Throwable t = e.getCause(); if (t instanceof ClientException) { throw (ClientException) t; } else { throw new ClientException(e); } } }
(1) finalUri 是生成最后的uri, 替换掉服务名称的uri
public RibbonApacheHttpResponse execute(RibbonApacheHttpRequest request, final IClientConfig configOverride) throws Exception { IClientConfig config = configOverride != null ? configOverride : this.config; RibbonProperties ribbon = RibbonProperties.from(config); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(ribbon.connectTimeout(this.connectTimeout)) .setSocketTimeout(ribbon.readTimeout(this.readTimeout)) .setRedirectsEnabled(ribbon.isFollowRedirects(this.followRedirects)) .setContentCompressionEnabled(ribbon.isGZipPayload(this.gzipPayload)) .build(); request = getSecureRequest(request, configOverride); final HttpUriRequest httpUriRequest = request.toRequest(requestConfig); final HttpResponse httpResponse = this.delegate.execute(httpUriRequest); return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI()); }
这里实际就是委托给相应的delegate 客户端去发送请求,默认走的是org.apache.http.impl.client.CloseableHttpClient#execute(org.apache.http.client.methods.HttpUriRequest) -》 位于HttpClient 包的客户端。
2.
package org.springframework.cloud.netflix.zuul.filters.route; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import java.util.regex.Pattern; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.servlet.http.HttpServletRequest; import com.netflix.client.ClientException; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.http.Header; import org.apache.http.HttpHost; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.entity.ContentType; import org.apache.http.entity.InputStreamEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicHttpEntityEnclosingRequest; import org.apache.http.message.BasicHttpRequest; import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory; import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory; import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties; import org.springframework.cloud.netflix.zuul.filters.ZuulProperties.Host; import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException; import org.springframework.context.ApplicationListener; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.REQUEST_ENTITY_KEY; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ROUTE_TYPE; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SIMPLE_HOST_ROUTING_FILTER_ORDER; /** * Route {@link ZuulFilter} that sends requests to predetermined URLs via apache * {@link HttpClient}. URLs are found in {@link RequestContext#getRouteHost()}. * * @author Spencer Gibb * @author Dave Syer * @author Bilal Alp * @author Gang Li * @author Denys Ivano */ public class SimpleHostRoutingFilter extends ZuulFilter implements ApplicationListener<EnvironmentChangeEvent> { private static final Log log = LogFactory.getLog(SimpleHostRoutingFilter.class); private static final Pattern MULTIPLE_SLASH_PATTERN = Pattern.compile("/{2,}"); private final Timer connectionManagerTimer = new Timer( "SimpleHostRoutingFilter.connectionManagerTimer", true); private boolean sslHostnameValidationEnabled; private boolean forceOriginalQueryStringEncoding; private ProxyRequestHelper helper; private Host hostProperties; private ApacheHttpClientConnectionManagerFactory connectionManagerFactory; private ApacheHttpClientFactory httpClientFactory; private HttpClientConnectionManager connectionManager; private CloseableHttpClient httpClient; private boolean customHttpClient = false; private boolean useServlet31 = true; @Override @SuppressWarnings("Deprecation") public void onApplicationEvent(EnvironmentChangeEvent event) { onPropertyChange(event); } @Deprecated public void onPropertyChange(EnvironmentChangeEvent event) { if (!customHttpClient) { boolean createNewClient = false; for (String key : event.getKeys()) { if (key.startsWith("zuul.host.")) { createNewClient = true; break; } } if (createNewClient) { try { this.httpClient.close(); } catch (IOException ex) { log.error("error closing client", ex); } // Re-create connection manager (may be shut down on HTTP client close) try { this.connectionManager.shutdown(); } catch (RuntimeException ex) { log.error("error shutting down connection manager", ex); } this.connectionManager = newConnectionManager(); this.httpClient = newClient(); } } } public SimpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties properties, ApacheHttpClientConnectionManagerFactory connectionManagerFactory, ApacheHttpClientFactory httpClientFactory) { this.helper = helper; this.hostProperties = properties.getHost(); this.sslHostnameValidationEnabled = properties.isSslHostnameValidationEnabled(); this.forceOriginalQueryStringEncoding = properties .isForceOriginalQueryStringEncoding(); this.connectionManagerFactory = connectionManagerFactory; this.httpClientFactory = httpClientFactory; checkServletVersion(); } public SimpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties properties, CloseableHttpClient httpClient) { this.helper = helper; this.hostProperties = properties.getHost(); this.sslHostnameValidationEnabled = properties.isSslHostnameValidationEnabled(); this.forceOriginalQueryStringEncoding = properties .isForceOriginalQueryStringEncoding(); this.httpClient = httpClient; this.customHttpClient = true; checkServletVersion(); } @PostConstruct private void initialize() { if (!customHttpClient) { this.connectionManager = newConnectionManager(); this.httpClient = newClient(); this.connectionManagerTimer.schedule(new TimerTask() { @Override public void run() { if (SimpleHostRoutingFilter.this.connectionManager == null) { return; } SimpleHostRoutingFilter.this.connectionManager .closeExpiredConnections(); } }, 30000, 5000); } } @PreDestroy public void stop() { this.connectionManagerTimer.cancel(); } @Override public String filterType() { return ROUTE_TYPE; } @Override public int filterOrder() { return SIMPLE_HOST_ROUTING_FILTER_ORDER; } @Override public boolean shouldFilter() { return RequestContext.getCurrentContext().getRouteHost() != null && RequestContext.getCurrentContext().sendZuulResponse(); } @Override public Object run() { RequestContext context = RequestContext.getCurrentContext(); HttpServletRequest request = context.getRequest(); MultiValueMap<String, String> headers = this.helper .buildZuulRequestHeaders(request); MultiValueMap<String, String> params = this.helper .buildZuulRequestQueryParams(request); String verb = getVerb(request); InputStream requestEntity = getRequestBody(request); if (getContentLength(request) < 0) { context.setChunkedRequestBody(); } String uri = this.helper.buildZuulRequestURI(request); this.helper.addIgnoredHeaders(); try { CloseableHttpResponse response = forward(this.httpClient, verb, uri, request, headers, params, requestEntity); setResponse(response); } catch (Exception ex) { throw new ZuulRuntimeException(handleException(ex)); } return null; } protected ZuulException handleException(Exception ex) { int statusCode = HttpStatus.INTERNAL_SERVER_ERROR.value(); Throwable cause = ex; String message = ex.getMessage(); ClientException clientException = findClientException(ex); if (clientException != null) { if (clientException .getErrorType() == ClientException.ErrorType.SERVER_THROTTLED) { statusCode = HttpStatus.SERVICE_UNAVAILABLE.value(); } cause = clientException; message = clientException.getErrorType().toString(); } return new ZuulException(cause, "Forwarding error", statusCode, message); } protected ClientException findClientException(Throwable t) { if (t == null) { return null; } if (t instanceof ClientException) { return (ClientException) t; } return findClientException(t.getCause()); } protected void checkServletVersion() { // To support Servlet API 3.1 we need to check if getContentLengthLong exists // Spring 5 minimum support is 3.0, so this stays try { HttpServletRequest.class.getMethod("getContentLengthLong"); useServlet31 = true; } catch (NoSuchMethodException e) { useServlet31 = false; } } protected void setUseServlet31(boolean useServlet31) { this.useServlet31 = useServlet31; } protected HttpClientConnectionManager getConnectionManager() { return connectionManager; } protected HttpClientConnectionManager newConnectionManager() { return connectionManagerFactory.newConnectionManager( !this.sslHostnameValidationEnabled, this.hostProperties.getMaxTotalConnections(), this.hostProperties.getMaxPerRouteConnections(), this.hostProperties.getTimeToLive(), this.hostProperties.getTimeUnit(), null); } protected CloseableHttpClient newClient() { final RequestConfig requestConfig = RequestConfig.custom() .setConnectionRequestTimeout( this.hostProperties.getConnectionRequestTimeoutMillis()) .setSocketTimeout(this.hostProperties.getSocketTimeoutMillis()) .setConnectTimeout(this.hostProperties.getConnectTimeoutMillis()) .setCookieSpec(CookieSpecs.IGNORE_COOKIES).build(); return httpClientFactory.createBuilder().setDefaultRequestConfig(requestConfig) .setConnectionManager(this.connectionManager).disableRedirectHandling() .build(); } private CloseableHttpResponse forward(CloseableHttpClient httpclient, String verb, String uri, HttpServletRequest request, MultiValueMap<String, String> headers, MultiValueMap<String, String> params, InputStream requestEntity) throws Exception { Map<String, Object> info = this.helper.debug(verb, uri, headers, params, requestEntity); URL host = RequestContext.getCurrentContext().getRouteHost(); HttpHost httpHost = getHttpHost(host); uri = StringUtils.cleanPath( MULTIPLE_SLASH_PATTERN.matcher(host.getPath() + uri).replaceAll("/")); long contentLength = getContentLength(request); ContentType contentType = null; if (request.getContentType() != null) { contentType = ContentType.parse(request.getContentType()); } InputStreamEntity entity = new InputStreamEntity(requestEntity, contentLength, contentType); HttpRequest httpRequest = buildHttpRequest(verb, uri, entity, headers, params, request); try { log.debug(httpHost.getHostName() + " " + httpHost.getPort() + " " + httpHost.getSchemeName()); CloseableHttpResponse zuulResponse = forwardRequest(httpclient, httpHost, httpRequest); this.helper.appendDebug(info, zuulResponse.getStatusLine().getStatusCode(), revertHeaders(zuulResponse.getAllHeaders())); return zuulResponse; } finally { // When HttpClient instance is no longer needed, // shut down the connection manager to ensure // immediate deallocation of all system resources // httpclient.getConnectionManager().shutdown(); } } protected HttpRequest buildHttpRequest(String verb, String uri, InputStreamEntity entity, MultiValueMap<String, String> headers, MultiValueMap<String, String> params, HttpServletRequest request) { HttpRequest httpRequest; String uriWithQueryString = uri + (this.forceOriginalQueryStringEncoding ? getEncodedQueryString(request) : this.helper.getQueryString(params)); switch (verb.toUpperCase()) { case "POST": HttpPost httpPost = new HttpPost(uriWithQueryString); httpRequest = httpPost; httpPost.setEntity(entity); break; case "PUT": HttpPut httpPut = new HttpPut(uriWithQueryString); httpRequest = httpPut; httpPut.setEntity(entity); break; case "PATCH": HttpPatch httpPatch = new HttpPatch(uriWithQueryString); httpRequest = httpPatch; httpPatch.setEntity(entity); break; case "DELETE": BasicHttpEntityEnclosingRequest entityRequest = new BasicHttpEntityEnclosingRequest( verb, uriWithQueryString); httpRequest = entityRequest; entityRequest.setEntity(entity); break; default: httpRequest = new BasicHttpRequest(verb, uriWithQueryString); log.debug(uriWithQueryString); } httpRequest.setHeaders(convertHeaders(headers)); return httpRequest; } private String getEncodedQueryString(HttpServletRequest request) { String query = request.getQueryString(); return (query != null) ? "?" + query : ""; } private MultiValueMap<String, String> revertHeaders(Header[] headers) { MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); for (Header header : headers) { String name = header.getName(); if (!map.containsKey(name)) { map.put(name, new ArrayList<String>()); } map.get(name).add(header.getValue()); } return map; } private Header[] convertHeaders(MultiValueMap<String, String> headers) { List<Header> list = new ArrayList<>(); for (String name : headers.keySet()) { for (String value : headers.get(name)) { list.add(new BasicHeader(name, value)); } } return list.toArray(new BasicHeader[0]); } private CloseableHttpResponse forwardRequest(CloseableHttpClient httpclient, HttpHost httpHost, HttpRequest httpRequest) throws IOException { return httpclient.execute(httpHost, httpRequest); } private HttpHost getHttpHost(URL host) { HttpHost httpHost = new HttpHost(host.getHost(), host.getPort(), host.getProtocol()); return httpHost; } protected InputStream getRequestBody(HttpServletRequest request) { InputStream requestEntity = null; try { requestEntity = (InputStream) RequestContext.getCurrentContext() .get(REQUEST_ENTITY_KEY); if (requestEntity == null) { requestEntity = request.getInputStream(); } } catch (IOException ex) { log.error("error during getRequestBody", ex); } return requestEntity; } private String getVerb(HttpServletRequest request) { String sMethod = request.getMethod(); return sMethod.toUpperCase(); } private void setResponse(HttpResponse response) throws IOException { RequestContext.getCurrentContext().set("zuulResponse", response); this.helper.setResponse(response.getStatusLine().getStatusCode(), response.getEntity() == null ? null : response.getEntity().getContent(), revertHeaders(response.getAllHeaders())); } /** * Add header names to exclude from proxied response in the current request. * @param names names of headers to exclude */ protected void addIgnoredHeaders(String... names) { this.helper.addIgnoredHeaders(names); } /** * Determines whether the filter enables the validation for ssl hostnames. * @return true if enabled */ boolean isSslHostnameValidationEnabled() { return this.sslHostnameValidationEnabled; } // Get the header value as a long in order to more correctly proxy very large requests protected long getContentLength(HttpServletRequest request) { if (useServlet31) { return request.getContentLengthLong(); } String contentLengthHeader = request.getHeader(HttpHeaders.CONTENT_LENGTH); if (contentLengthHeader != null) { try { return Long.parseLong(contentLengthHeader); } catch (NumberFormatException e) { } } return request.getContentLength(); } }
其创建如下: org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration#simpleHostRoutingFilter2
@Bean @ConditionalOnMissingBean({ SimpleHostRoutingFilter.class }) public SimpleHostRoutingFilter simpleHostRoutingFilter2(ProxyRequestHelper helper, ZuulProperties zuulProperties, CloseableHttpClient httpClient) { return new SimpleHostRoutingFilter(helper, zuulProperties, httpClient); }
可以看出其构造方法直接指定了httpClient 客户端,也就是直接使用httpCLient 进行转发请求。
3. org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter
public class SendForwardFilter extends ZuulFilter { protected static final String SEND_FORWARD_FILTER_RAN = "sendForwardFilter.ran"; @Override public String filterType() { return ROUTE_TYPE; } @Override public int filterOrder() { return SEND_FORWARD_FILTER_ORDER; } @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); return ctx.containsKey(FORWARD_TO_KEY) && !ctx.getBoolean(SEND_FORWARD_FILTER_RAN, false); } @Override public Object run() { try { RequestContext ctx = RequestContext.getCurrentContext(); String path = (String) ctx.get(FORWARD_TO_KEY); RequestDispatcher dispatcher = ctx.getRequest().getRequestDispatcher(path); if (dispatcher != null) { ctx.set(SEND_FORWARD_FILTER_RAN, true); if (!ctx.getResponse().isCommitted()) { dispatcher.forward(ctx.getRequest(), ctx.getResponse()); ctx.getResponse().flushBuffer(); } } } catch (Exception ex) { ReflectionUtils.rethrowRuntimeException(ex); } return null; } }
3. postFilter
1. cn.qz.cloud.filter.PostFilter
这个是我们自己的测试的后置过滤器。 到这一步 RequestContext 信息如下:(可以看到包含的一些信息,包括request 等信息)
2. org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
public class SendResponseFilter extends ZuulFilter { private static final Log log = LogFactory.getLog(SendResponseFilter.class); private boolean useServlet31 = true; private ZuulProperties zuulProperties; private ThreadLocal<byte[]> buffers; @Deprecated public SendResponseFilter() { this(new ZuulProperties()); } public SendResponseFilter(ZuulProperties zuulProperties) { this.zuulProperties = zuulProperties; // To support Servlet API 3.1 we need to check if setContentLengthLong exists // minimum support in Spring 5 is 3.0 so we need to keep tihs try { HttpServletResponse.class.getMethod("setContentLengthLong", long.class); } catch (NoSuchMethodException e) { useServlet31 = false; } buffers = ThreadLocal .withInitial(() -> new byte[zuulProperties.getInitialStreamBufferSize()]); } /* for testing */ boolean isUseServlet31() { return useServlet31; } @Override public String filterType() { return POST_TYPE; } @Override public int filterOrder() { return SEND_RESPONSE_FILTER_ORDER; } @Override public boolean shouldFilter() { RequestContext context = RequestContext.getCurrentContext(); return context.getThrowable() == null && (!context.getZuulResponseHeaders().isEmpty() || context.getResponseDataStream() != null || context.getResponseBody() != null); } @Override public Object run() { try { addResponseHeaders(); writeResponse(); } catch (Exception ex) { ReflectionUtils.rethrowRuntimeException(ex); } return null; } private void writeResponse() throws Exception { RequestContext context = RequestContext.getCurrentContext(); // there is no body to send if (context.getResponseBody() == null && context.getResponseDataStream() == null) { return; } HttpServletResponse servletResponse = context.getResponse(); if (servletResponse.getCharacterEncoding() == null) { // only set if not set servletResponse.setCharacterEncoding("UTF-8"); } String servletResponseContentEncoding = getResponseContentEncoding(context); OutputStream outStream = servletResponse.getOutputStream(); InputStream is = null; try { if (context.getResponseBody() != null) { String body = context.getResponseBody(); is = new ByteArrayInputStream( body.getBytes(servletResponse.getCharacterEncoding())); } else { is = context.getResponseDataStream(); if (is != null && context.getResponseGZipped()) { // if origin response is gzipped, and client has not requested gzip, // decompress stream before sending to client // else, stream gzip directly to client if (isGzipRequested(context)) { servletResponseContentEncoding = "gzip"; } else { servletResponseContentEncoding = null; is = handleGzipStream(is); } } } if (servletResponseContentEncoding != null) { servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, servletResponseContentEncoding); } if (is != null) { writeResponse(is, outStream); } } finally { /** * We must ensure that the InputStream provided by our upstream pooling * mechanism is ALWAYS closed even in the case of wrapped streams, which are * supplied by pooled sources such as Apache's * PoolingHttpClientConnectionManager. In that particular case, the underlying * HTTP connection will be returned back to the connection pool iif either * close() is explicitly called, a read error occurs, or the end of the * underlying stream is reached. If, however a write error occurs, we will end * up leaking a connection from the pool without an explicit close() * * @author Johannes Edmeier */ if (is != null) { try { is.close(); } catch (Exception ex) { log.warn("Error while closing upstream input stream", ex); } } // cleanup ThreadLocal when we are all done if (buffers != null) { buffers.remove(); } try { Object zuulResponse = context.get("zuulResponse"); if (zuulResponse instanceof Closeable) { ((Closeable) zuulResponse).close(); } outStream.flush(); // The container will close the stream for us } catch (IOException ex) { log.warn("Error while sending response to client: " + ex.getMessage()); } } } protected InputStream handleGzipStream(InputStream in) throws Exception { // Record bytes read during GZip initialization to allow to rewind the stream if // needed // RecordingInputStream stream = new RecordingInputStream(in); try { return new GZIPInputStream(stream); } catch (java.util.zip.ZipException | java.io.EOFException ex) { if (stream.getBytesRead() == 0) { // stream was empty, return the original "empty" stream return in; } else { // reset the stream and assume an unencoded response log.warn( "gzip response expected but failed to read gzip headers, assuming unencoded response for request " + RequestContext.getCurrentContext().getRequest() .getRequestURL().toString()); stream.reset(); return stream; } } finally { stream.stopRecording(); } } protected boolean isGzipRequested(RequestContext context) { final String requestEncoding = context.getRequest() .getHeader(ZuulHeaders.ACCEPT_ENCODING); return requestEncoding != null && HTTPRequestUtils.getInstance().isGzipped(requestEncoding); } private String getResponseContentEncoding(RequestContext context) { List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders(); if (zuulResponseHeaders != null) { for (Pair<String, String> it : zuulResponseHeaders) { if (ZuulHeaders.CONTENT_ENCODING.equalsIgnoreCase(it.first())) { return it.second(); } } } return null; } private void writeResponse(InputStream zin, OutputStream out) throws Exception { byte[] bytes = buffers.get(); int bytesRead = -1; while ((bytesRead = zin.read(bytes)) != -1) { out.write(bytes, 0, bytesRead); } } private void addResponseHeaders() { RequestContext context = RequestContext.getCurrentContext(); HttpServletResponse servletResponse = context.getResponse(); if (this.zuulProperties.isIncludeDebugHeader()) { @SuppressWarnings("unchecked") List<String> rd = (List<String>) context.get(ROUTING_DEBUG_KEY); if (rd != null) { StringBuilder debugHeader = new StringBuilder(); for (String it : rd) { debugHeader.append("[[[" + it + "]]]"); } servletResponse.addHeader(X_ZUUL_DEBUG_HEADER, debugHeader.toString()); } } List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders(); if (zuulResponseHeaders != null) { for (Pair<String, String> it : zuulResponseHeaders) { if (!ZuulHeaders.CONTENT_ENCODING.equalsIgnoreCase(it.first())) { servletResponse.addHeader(it.first(), it.second()); } } } if (includeContentLengthHeader(context)) { Long contentLength = context.getOriginContentLength(); if (useServlet31) { servletResponse.setContentLengthLong(contentLength); } else { // Try and set some kind of content length if we can safely convert the // Long to an int if (isLongSafe(contentLength)) { servletResponse.setContentLength(contentLength.intValue()); } } } } private boolean isLongSafe(long value) { return value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE; } protected boolean includeContentLengthHeader(RequestContext context) { // Not configured to forward the header if (!this.zuulProperties.isSetContentLength()) { return false; } // Only if Content-Length is provided if (context.getOriginContentLength() == null) { return false; } // If response is compressed, include header only if we are not about to // decompress it if (context.getResponseGZipped()) { return context.isGzipRequested(); } // Forward it in all other cases return true; } /** * InputStream recording bytes read to allow for a reset() until recording is stopped. */ private static class RecordingInputStream extends InputStream { private InputStream delegate; private ByteArrayOutputStream buffer = new ByteArrayOutputStream(); RecordingInputStream(InputStream delegate) { super(); this.delegate = Objects.requireNonNull(delegate); } @Override public int read() throws IOException { int read = delegate.read(); if (buffer != null && read != -1) { buffer.write(read); } return read; } @Override public int read(byte[] b, int off, int len) throws IOException { int read = delegate.read(b, off, len); if (buffer != null && read != -1) { buffer.write(b, off, read); } return read; } public void reset() { if (buffer == null) { throw new IllegalStateException("Stream is not recording"); } this.delegate = new SequenceInputStream( new ByteArrayInputStream(buffer.toByteArray()), delegate); this.buffer = new ByteArrayOutputStream(); } public int getBytesRead() { return (buffer == null) ? -1 : buffer.size(); } public void stopRecording() { this.buffer = null; } @Override public void close() throws IOException { this.delegate.close(); } } }
3.
查看其调用链如下:
可以看出其调用链没经过SpringMVC的DispatcherServlet。
这种方式经过的Filter和上面的一样,只是这里不经过DispatcherServlet, 所以ServletDetectionFilter 放置的IS_DISPATCHER_SERVLET_REQUEST_KEY 为false。 后面Servlet30WrapperFilter 也不会对Request 进行包装。
4. 文件上传
下面研究文件上传在两种模式下的区别。
过SpringMVC和不过MVC的两种方式都可以上传文件。官方建议是大文件上传绕过SpringMVC,直接走第二种ZuulServlet 的方式(/cloud-zuul/** 路径方式),因为过SpringMVC的话会占用内存,相当于会把文件解析一下。
两者区别:
1. 过SpringMVC:
(2) org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter#run 前置过滤器会包装表单,这里会占用内存:解析文件(自己测试是在这里调用 解析占用内存), 调用wrapper.getContentType() 会调用org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter.FormBodyRequestWrapper#buildContentData 解析内容
2. 不过SpringMVC