首先,引入 spring-cloud-starter-zuul 之后会间接引入:
hystrix依赖已经引入,那么何种情况下使用hystrix呢?
在Zuul的自动配置类 ZuulServerAutoConfiguration 和 ZuulProxyAutoConfiguration 中总共会向Spring容器注入3个Zuul的RouteFilter,分别是
SimpleHostRoutingFilter 简单路由,通过HttpClient向预定的URL发送请求 生效条件: RequestContext.getCurrentContext().getRouteHost() != null && RequestContext.getCurrentContext().sendZuulResponse() 1、RequestContext中的routeHost不为空,routeHost就是URL,即使用URL直连 2、RequestContext中的sendZuulResponse为true,即是否将response发送给客户端,默认为true
RibbonRoutingFilter 使用Ribbon、Hystrix和可插入的http客户端发送请求 生效条件: (RequestContext.getRouteHost() == null && RequestContext.get(SERVICE_ID_KEY) != null && RequestContext.sendZuulResponse()) 1、RequestContext中的routeHost为空,即URL为空 2、RequestContext中的serviceId不为空 3、RequestContext中的sendZuulResponse为true,即是否将response发送给客户端,默认为true
SendForwardFilter forward到本地URL 生效条件: RequestContext.containsKey(FORWARD_TO_KEY) && !RequestContext.getBoolean(SEND_FORWARD_FILTER_RAN, false) 1、RequestContext中包含FORWARD_TO_KEY,即URL使用 forward: 映射 2、RequestContext中SEND_FORWARD_FILTER_RAN为false,SEND_FORWARD_FILTER_RAN意为“send forward是否运行过了”,在SendForwardFilter#run()时会 ctx.set(SEND_FORWARD_FILTER_RAN, true)
综上所述,在使用serviceId映射的方法路由转发的时候,会使用Ribbon+Hystrix
而哪种路由配置方式是“URL映射”,哪种配置方式又是“serviceId映射”呢?
Zuul有一个前置过滤器 PreDecorationFilter 用于通过 RouteLocator路由定位器 决定在何时以何种方式路由转发
RouteLocator是用于通过请求地址匹配到Route路由的,之后 PreDecorationFilter 再通过Route信息设置RequestContext上下文,决定后续使用哪个RouteFilter做路由转发
所以就引出以下问题:
什么是Route
RouteLocator路由定位器如何根据请求路径匹配路由
匹配到路由后,PreDecorationFilter如何设置RequestContext请求上下文
什么是Route
我总共见到两个和Route相关的类
ZuulProperties.ZuulRoute ,用于和zuul配置文件关联,保存相关信息
org.springframework.cloud.netflix.zuul.filters.Route , RouteLocator找到的路由信息就是这个类,用于路由转发
public static class ZuulRoute { private String id; //ZuulRoute的id private String path; //路由的pattern,如 /foo/** private String serviceId; //要映射到此路由的服务id private String url; //要映射到路由的完整物理URL private boolean stripPrefix = true; //用于确定在转发之前是否应剥离此路由前缀的标志位 private Boolean retryable; //此路由是否可以重试,通常重试需要serviceId和ribbon private Set
public class Route { private String id; private String fullPath; private String path; private String location; //可能是 url 或 serviceId private String prefix; private Boolean retryable; private Set
可以看到 org.springframework.cloud.netflix.zuul.filters.Route 和 ZuulProperties.ZuulRoute 基本一致,只是Route用于路由转发定位的属性location根据不同的情况,可能是一个具体的URL,可能是一个serviceId
RouteLocator路由定位器如何根据请求路径匹配路由
Zuul在自动配置加载时注入了2个RouteLocator
CompositeRouteLocator : 组合的RouteLocator,在 getMatchingRoute() 时会依次调用其它的RouteLocator,先找到先返回;CompositeRouteLocator的routeLocators集合中只有DiscoveryClientRouteLocator
DiscoveryClientRouteLocator : 可以将静态的、已配置的路由与来自DiscoveryClient服务发现的路由组合在一起,来自DiscoveryClient的路由优先;SimpleRouteLocator的子类(SimpleRouteLocator 基于加载到 ZuulProperties 中的配置定位Route路由信息)
其中CompositeRouteLocator是 @Primary 的,它是组合多个RouteLocator的Locator,其 getMatchingRoute() 方法会分别调用其它所有RouteLocator的getMatchingRoute()方法,通过请求路径匹配路由信息,只要匹配到了就马上返回
默认CompositeRouteLocator混合路由定位器的routeLocators只有一个DiscoveryClientRouteLocator,故只需分析 DiscoveryClientRouteLocator#getMatchingRoute(path)
//----------DiscoveryClientRouteLocator是SimpleRouteLocator子类,其实是调用的SimpleRouteLocator##getMatchingRoute(path) @Override public Route getMatchingRoute(final String path) { return getSimpleMatchingRoute(path); } protected Route getSimpleMatchingRoute(final String path) { if (log.isDebugEnabled()) { log.debug("Finding route for path: " + path); } // routes是保存路由信息的map,如果此时还未加载,调用locateRoutes() if (this.routes.get() == null) { this.routes.set(locateRoutes()); } if (log.isDebugEnabled()) { log.debug("servletPath=" + this.dispatcherServletPath); log.debug("zuulServletPath=" + this.zuulServletPath); log.debug("RequestUtils.isDispatcherServletRequest()=" + RequestUtils.isDispatcherServletRequest()); log.debug("RequestUtils.isZuulServletRequest()=" + RequestUtils.isZuulServletRequest()); } /** * 下面的方法主要是先对path做微调 * 再根据path到routes中匹配到ZuulRoute * 最后根据 ZuulRoute 和 adjustedPath 生成 Route */ String adjustedPath = adjustPath(path); ZuulRoute route = getZuulRoute(adjustedPath); return getRoute(route, adjustedPath); }
下面我们来看看 locateRoutes() 是如何加载静态的、已配置的路由与来自DiscoveryClient服务发现的路由的
//----------DiscoveryClientRouteLocator#locateRoutes() 服务发现路由定位器的locateRoutes() @Override protected LinkedHashMap
此方法运行后就已经加载了配置文件中所有路由信息,以及注册中心中的服务路由信息,有的通过URL路由,有的通过serviceId路由
只需根据本次请求的requestURI与 路由的pattern匹配找到对应的路由
匹配到路由后,PreDecorationFilter如何设置RequestContext请求上下文
//----------PreDecorationFilter前置过滤器 @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest()); Route route = this.routeLocator.getMatchingRoute(requestURI); //找到匹配的路由 //----------------到上面为止是已经分析过的,根据requestURI找到匹配的Route信息 // ==== 匹配到路由信息 if (route != null) { String location = route.getLocation(); if (location != null) { ctx.put(REQUEST_URI_KEY, route.getPath());//RequestContext设置 requestURI:路由的pattern路径 ctx.put(PROXY_KEY, route.getId());//RequestContext设置 proxy:路由id //设置需要忽略的敏感头信息,要么用全局默认的,要么用路由自定义的 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()); } //如果location是 http/https开头的,RequestContext设置 routeHost:URL //如果location是 forward:开头的,RequestContext设置 forward信息、routeHost:null //其它 RequestContext设置 serviceId、routeHost:null、X-Zuul-ServiceId 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); } //是否添加代理头信息 X-Forwarded-For 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); } //是否添加Host头信息 if (this.properties.isAddHostHeader()) { ctx.addZuulRequestHeader(HttpHeaders.HOST, toHostHeader(ctx.getRequest())); } } } // ==== 没有匹配到路由信息 else { log.warn("No route found for uri: " + requestURI); String fallBackUri = requestURI; String fallbackPrefix = this.dispatcherServletPath; // default fallback // servlet is // DispatcherServlet 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 { // 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 + fallBackUri; forwardURI = forwardURI.replaceAll("//", "/"); ctx.set(FORWARD_TO_KEY, forwardURI); } return null; }
总结:
只要引入了spring-cloud-starter-zuul就会间接引入Ribbon、Hystrix
路由信息可能是从配置文件中加载的,也可能是通过DiscoveryClient从注册中心加载的
zuul是通过前置过滤器PreDecorationFilter找到与当前requestURI匹配的路由信息,并在RequestContext中设置相关属性的,后续的Route Filter会根据RequestContext中的这些属性判断如何路由转发
Route Filter主要使用 SimpleHostRoutingFilter 和 RibbonRoutingFilter
当RequestContext请求上下文中存在routeHost,即URL直连信息时,使用SimpleHostRoutingFilter简单Host路由
当RequestContext请求上下文中存在serviceId,即服务id时(可能会与注册中心关联获取服务列表,或者读取配置文件中serviceId.ribbon.listOfServers的服务列表),使用RibbonRoutingFilter,会使用Ribbon、Hystrix
欢迎工作一到五年的Java工程师朋友们加入Java架构开发: 855835163
群内提供免费的Java架构学习资料(里面有高可用、高并发、高性能及分布式、Jvm性能调优、Spring源码,MyBatis,Netty,Redis,Kafka,Mysql,Zookeeper,Tomcat,Docker,Dubbo,Nginx等多个知识点的架构资料)合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!