部分图例摘自https://m.imooc.com/article/23679
网关Zuul——路由转发
1.路由映射
通常网关的路由映射都在配置文件中配置好了,那么,在程序运行时,路由配置会被读取到哪里进行存储呢?
在源码中可以看到由ZuulProperties类,看名字可以知道这类是zuul的属性类,配置文件中的信息解析后大概率会存放到这个类里面
@ConfigurationProperties("zuul") public class ZuulProperties { //...省略 /**配置文件中的路由配置会存放在routes中*/
/**网关路由的配置写法
第一种:
zuul:routes:<服务名>:<访问路径>
例如:zuul:routes:user_service:/myUser/**
第二种:
zuul:routes:<路由名称>:
path:<访问路径>
serviceId:<服务名>
zuul:routes:user:
path:/myUser/**
serviceId:user_service
第三种:
zuul:routes:<路由名称>:
path:<访问路径>
url:<转发地址>
zuul:routes:user:
path:/myUser/**
url:http://example.com/users_service
第三种需要额外配置,具体是什么,以后再说吧
*/
private Map<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap(); //...省略 /***/ @PostConstruct public void init() { Iterator var1 = this.routes.entrySet().iterator(); while(var1.hasNext()) { Entry<String, ZuulProperties.ZuulRoute> entry = (Entry)var1.next(); ZuulProperties.ZuulRoute value = (ZuulProperties.ZuulRoute)entry.getValue(); /***/ if (!StringUtils.hasText(value.getLocation())) { value.serviceId = (String)entry.getKey(); } /***/ if (!StringUtils.hasText(value.getId())) { value.id = (String)entry.getKey(); } /***/ if (!StringUtils.hasText(value.getPath())) { value.path = "/" + (String)entry.getKey() + "/**"; } } }
其中ZuulRoute为单个的路由转发配置
public static class ZuulRoute { /***/ private String id; private String path; private String serviceId; private String url; private boolean stripPrefix = true; private Boolean retryable; private Set<String> sensitiveHeaders = new LinkedHashSet(); private boolean customSensitiveHeaders = false; /***/ 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 = this.extractId(path); if (!path.startsWith("/")) { path = "/" + path; } this.setLocation(location); this.path = path; } /***/ public ZuulRoute(String path, String location) { this.id = this.extractId(path); this.path = path; this.setLocation(location); } /***/ public String getLocation() { return StringUtils.hasText(this.url) ? this.url : this.serviceId; } /***/ public void setLocation(String location) { if (location == null || !location.startsWith("http:") && !location.startsWith("https:")) { this.serviceId = location; } else { this.url = 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, this.getLocation(), prefix, this.retryable, this.isCustomSensitiveHeaders() ? this.sensitiveHeaders : null, this.stripPrefix); } /***/ public void setSensitiveHeaders(Set<String> headers) { this.customSensitiveHeaders = true; this.sensitiveHeaders = new LinkedHashSet(headers); } /***/ public boolean isCustomSensitiveHeaders() { return this.customSensitiveHeaders; } public String getId() { return this.id; } public String getPath() { return this.path; } public String getServiceId() { return this.serviceId; } public String getUrl() { return this.url; } public boolean isStripPrefix() { return this.stripPrefix; } public Boolean getRetryable() { return this.retryable; } public Set<String> getSensitiveHeaders() { return this.sensitiveHeaders; } public void setId(String id) { this.id = id; } public void setPath(String path) { this.path = path; } public void setServiceId(String serviceId) { this.serviceId = serviceId; } public void setUrl(String url) { this.url = url; } public void setStripPrefix(boolean stripPrefix) { this.stripPrefix = stripPrefix; } public void setRetryable(Boolean retryable) { this.retryable = retryable; } public void setCustomSensitiveHeaders(boolean customSensitiveHeaders) { this.customSensitiveHeaders = customSensitiveHeaders; }
程序启动后,会将配置文件中的信息提取出来,然后存入上面的类的实例中,具体怎么做到的,以后再研究,
现在已经确定路由映射的配置是存放在ZuulProperties中的,那么,若想做到路由转发功能,就需要调用ZuulProperties了。
通过下图可以知道,网关组件的内部运行逻辑:
现在,我们来逐个分析网关组件的各个部分的作用
在zuul .20.0.RELEASE中;pre filter有如下这些
pre Fliters:
类名 | 类型(filterType) | 优先级(filterOrder) | 是否执行(shouldFilter) | 主要功能(run) | 补充说明 |
DebugFilter | pre | 1 | 有条件的执行 | 而它的具体操作内容则是将当前的请求上下文中的 debugRouting和debugRequest参数设置为true。 | |
FormBodyWrapperFilter | pre | -1 | 有条件的执行 | 主要目的是将符合要求的请求体包装成FormBodyRequestWrapper对象。 | |
PreDecorationFilter | pre | 5 | 根据请求来判断是否执行 | 它的具体操作内容就是为当前请求做一些预处理,比如:进行路由规则的匹配、在请求上下文中设置该请求的基本信息以及将路由匹配结果等一些设置信息等,这些信息将是后续过滤器进行处理的重要依据,我们可以通过RequestContext.getCurrentContext()来访问这些信息。 | |
Servlet30WrapperFilter | pre | -2 | true | 目前的实现会对所有请求生效, 主要为了将原始的HttpServletRequest包装成 Servlet30RequestWrapper对象。 | |
ServletDetectionFilter | pre | -3 | true | 判断请求是从DispatcherServlet访问的还是 从ZuulServlet访问的。 | |
由于在同一个请求的不同生命周期中,都可以访问到这两个值,所以我们在后续的各个过滤器中可以利用这两值来定义一些debug信息,这样当线上环境出现问题的时候,可以通过请求参数的方式来激活这些debug信息以帮助分析问题。
另外,对于请求参数中的debug参数,我们也可以通过zuul.debug.parameter来进行自定义。
ServletDetectionFilter:序号-3,第一个执行的过滤器
/**该过滤器主要是判断请求是从哪里来的,是从DispatcherServlet过来的还是从ZuulServlet过来的*/ public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); /***/ if (!(request instanceof HttpServletRequestWrapper) && this.isDispatcherServletRequest(request)) { ctx.set("isDispatcherServletRequest", true); } else { ctx.set("isDispatcherServletRequest", false); } return null; } /**判断请求是否从dispatcherServlet过来的*/ private boolean isDispatcherServletRequest(HttpServletRequest request) { return request.getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null; }
ServletDetectionFilter的处理结果(true/false)会存放在RequestContext的isDispatcherServletRequest参数中
而RequestContext是一个静态的全局单例,并且是线程安全的,所以其他任何过滤器都可以取到它;
获取方法为RequestUtils.isDispatcherServletRequest()和RequestUtils.isZuulServletRequest();
Servlet30WrapperFilter:序号-2;第二个执行的过滤器
主要功能代码为:
/***/ public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); /**将原始的HttpServletRequest包装成Servlet30RequestWrapper对象*/ if (request instanceof HttpServletRequestWrapper) {
/**ReflectionUtils.getField(,)主要是利用反射获取对应的属性*/ request = (HttpServletRequest)ReflectionUtils.getField(this.requestField, request);
ctx.setRequest(new Servlet30RequestWrapper(request)); } else if (RequestUtils.isDispatcherServletRequest()) { ctx.setRequest(new Servlet30RequestWrapper(request)); } return null; }
Servlet30WrapperFilter的处理只是将原始的请求包装成Servlet30RequestWrapper对象。
FormBodyWrapperFilter:序号-1;第三个过滤器
主要功能代码为:
/**判断Http请求的Content-Type*/ public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); String contentType = request.getContentType(); if (contentType == null) { return false; } else { try { /**APPLICATION_FORM_URLENCODED:application/x-www-form-urlencoded; MULTIPART_FORM_DATA:multipart/form-data */ MediaType mediaType = MediaType.valueOf(contentType); return MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType) || this.isDispatcherServletRequest(request) && MediaType.MULTIPART_FORM_DATA.includes(mediaType); } catch (InvalidMediaTypeException var5) { return false; } } }
/***/ public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); FormBodyWrapperFilter.FormBodyRequestWrapper wrapper = null; /***/ if (request instanceof HttpServletRequestWrapper) { HttpServletRequest wrapped = (HttpServletRequest)ReflectionUtils.getField(this.requestField, request); wrapper = new FormBodyWrapperFilter.FormBodyRequestWrapper(wrapped); ReflectionUtils.setField(this.requestField, request, wrapper); /***/ if (request instanceof ServletRequestWrapper) { ReflectionUtils.setField(this.servletRequestField, request, wrapper); } } else { /***/ wrapper = new FormBodyWrapperFilter.FormBodyRequestWrapper(request); ctx.setRequest(wrapper); } /***/ if (wrapper != null) { ctx.getZuulRequestHeaders().put("content-type", wrapper.getContentType()); } return null; }
FormBodyWrapperFilter的主要工作是将符合要求的请求体包装成FormBodyRequestWrapper对象
DebugFilter:序号1;第四个过滤器
/**根据配置的参数判断是否执行*/ private static final DynamicBooleanProperty ROUTING_DEBUG = DynamicPropertyFactory.getInstance().getBooleanProperty("zuul.debug.request", false); private static final DynamicStringProperty DEBUG_PARAMETER = DynamicPropertyFactory.getInstance().getStringProperty("zuul.debug.parameter", "debug"); public boolean shouldFilter() { HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); return "true".equals(request.getParameter(DEBUG_PARAMETER.get())) ? true : ROUTING_DEBUG.get(); } /***/ public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); ctx.setDebugRouting(true); ctx.setDebugRequest(true); return null; }
DebugFilter通过判断配置文件中的zuul.debug.parameter和zuul.debug.request来判断是否执行过滤器;这2个参数的默认值返回的结果为false,即默认情况下不执行此过滤器
通过配置可以开启这个过滤器;配置参数为zuul.debug.request=true或者在参数中添加debug=true来开启;例如用zuul_host:zuul_port/路径?debug=true;参数名默认是debug,一般不用修改;但也可以自定义,在配置文件中zuul.debug.parameter=参数名;然后再请求中添加 参数名=true也可以;
例如:zuul_host:zuul_port/路径?flag=true;其中flag就是再配置文件中配置的参数名
debugFilter主要作用就是设置setDebugRouting和setDebugRequest为true;
由于在同一个请求的不同生命周期中,都可以访问到这两个值,所以我们在后续的各个过滤器中可以利用这两值来定义一些debug信息,这样当线上环境出现问题的时候,可以通过请求参数的方式来激活这些debug信息以帮助分析问题。
PreDecorationFilter:序号5;默认是第五个执行的过滤器
1 /** 判断请求中是否包含forward.to和serviceId, 2 如果包含,则表明,此url已经处理过转发逻辑了; 3 就不在拦截处理*/ 4 public boolean shouldFilter() { 5 RequestContext ctx = RequestContext.getCurrentContext(); 6 return !ctx.containsKey("forward.to") && !ctx.containsKey("serviceId"); 7 } 8 9 public Object run() { 10 //1.根据请求的url找到对应的路由Route 11 RequestContext ctx = RequestContext.getCurrentContext(); 12 String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest()); 13 //下面方法是根据url路径获取路由的核心逻辑 14 Route route = this.routeLocator.getMatchingRoute(requestURI); 15 String location; 16 String xforwardedfor; 17 String remoteAddr; 18 //2.根据Route进行相应的转发 19 if (route != null) { 20 location = route.getLocation(); 21 if (location != null) { 22 //route.getPath()表示配置文件中定义的路由请求地址 23 ctx.put("requestURI", route.getPath()); 24 ctx.put("proxy", route.getId()); 25 if (!route.isCustomSensitiveHeaders()) { 26 this.proxyRequestHelper.addIgnoredHeaders((String[])this.properties.getSensitiveHeaders().toArray(new String[0])); 27 } else { 28 this.proxyRequestHelper.addIgnoredHeaders((String[])route.getSensitiveHeaders().toArray(new String[0])); 29 } 30 //设置是否重试 31 if (route.getRetryable() != null) { 32 ctx.put("retryable", route.getRetryable()); 33 } 34 //判断路由映射路是否以服务名为基准的配置 35 if (!location.startsWith("http:") && !location.startsWith("https:")) { 36 //判断是否为转发的,并进行处理:将服务名与访问路径拼接起来 37 if (location.startsWith("forward:")) { 38 ctx.set("forward.to", StringUtils.cleanPath(location.substring("forward:".length()) + route.getPath())); 39 ctx.setRouteHost((URL)null); 40 return null; 41 } 42 43 ctx.set("serviceId", location); 44 ctx.setRouteHost((URL)null); 45 ctx.addOriginResponseHeader("X-Zuul-ServiceId", location); 46 } else { 47 //url中存的是真实地址 48 ctx.setRouteHost(this.getUrl(location)); 49 ctx.addOriginResponseHeader("X-Zuul-Service", location); 50 } 51 //添加请求头信息 52 if (this.properties.isAddProxyHeaders()) { 53 this.addProxyHeaders(ctx, route); 54 xforwardedfor = ctx.getRequest().getHeader("X-Forwarded-For"); 55 remoteAddr = ctx.getRequest().getRemoteAddr(); 56 if (xforwardedfor == null) { 57 xforwardedfor = remoteAddr; 58 } else if (!xforwardedfor.contains(remoteAddr)) { 59 xforwardedfor = xforwardedfor + ", " + remoteAddr; 60 } 61 62 ctx.addZuulRequestHeader("X-Forwarded-For", xforwardedfor); 63 } 64 65 if (this.properties.isAddHostHeader()) { 66 ctx.addZuulRequestHeader("Host", this.toHostHeader(ctx.getRequest())); 67 } 68 } 69 } else { 70 3.Route为null,进行相应的fallback处理 71 log.warn("No route found for uri: " + requestURI); 72 xforwardedfor = this.dispatcherServletPath; 73 if (RequestUtils.isZuulServletRequest()) { 74 log.debug("zuulServletPath=" + this.properties.getServletPath()); 75 location = requestURI.replaceFirst(this.properties.getServletPath(), ""); 76 log.debug("Replaced Zuul servlet path:" + location); 77 } else { 78 log.debug("dispatcherServletPath=" + this.dispatcherServletPath); 79 location = requestURI.replaceFirst(this.dispatcherServletPath, ""); 80 log.debug("Replaced DispatcherServlet servlet path:" + location); 81 } 82 83 if (!location.startsWith("/")) { 84 location = "/" + location; 85 } 86 87 remoteAddr = xforwardedfor + location; 88 remoteAddr = remoteAddr.replaceAll("//", "/"); 89 ctx.set("forward.to", remoteAddr); 90 } 91 92 return null; 93 }
PreDecorationFilter会先判断请求中是否包含有"forward.to"或"serviceId",如果包含了,就表明这个请求时被处理过的,不需要再过滤了;不然就开启过滤
PreDecorationFilter主要是对请求信息做一些预处理;首先是进行路由规则的匹配
首先是将外部的请求url与我们配置的路由映射对应上,即将外部请求通过path的匹配与对应的微服务对应上;
Route route = this.routeLocator.getMatchingRoute(requestURI);
再上述方法中过滤掉了请求的前半部分,然后与配置文件中的信息进行匹配,组装;
然后判断是否设置请求重试(配置文件中可配置)
再设置了一些请求头的信息,比如:”X-Zuul-ServiceId“——表示是服务请求;”X-Zuul-Service“——表示是真实url请求;
还设置了”X-Forwarded-For“,”X-Forwarded-Host“、X-Forwarded-Port、X-Forwarded-Prefix、X-Forwarded-Proto等信息,具体的细节以后单独补充。
这些头域信息有些可以在配置文件中进行配置,后面单独开章再研究。
Route过滤器
route()过滤器主要有3个RibbonRoutingFilter、SendForwardFilter、SimpleHostRoutingFilter
RibbonRoutingFilter:序号10;第一个执行的route过滤器
public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); return ctx.getRouteHost() == null && ctx.get("serviceId") != null && ctx.sendZuulResponse(); }
该过滤器只对RequestContext 存在”serviceId“参数的url做过滤;实际就是针对通过注册名来调用微服务的访问;不通过注册名,而是直接调用url的不会执行该过滤器。这里就是面向服务路由的核心部分。
public Object run() { RequestContext context = RequestContext.getCurrentContext(); this.helper.addIgnoredHeaders(new String[0]); /***/ try { /**服务在这里完成了对微服务的路由访问,代码的内部细节后面单开一章分析*/ RibbonCommandContext commandContext = this.buildCommandContext(context); ClientHttpResponse response = this.forward(commandContext); this.setResponse(response); return response; } catch (ZuulException var4) { throw new ZuulRuntimeException(var4); } catch (Exception var5) { throw new ZuulRuntimeException(var5); } }
RibbonRoutingFilter在这里通过Ribbon和Hystrix来向服务实例发起请求,并将服务实例的请求结果返回。在这个过滤器中完成了对负载均衡和熔断。
所以可以说网关本身就自带了负载均衡和熔断。
SimpleHostRoutingFilter:序号为100;默认情况下第二个执行;该过滤器针对一般的访问,只对通过url配置路由规则的请求生效
public boolean shouldFilter() { return RequestContext.getCurrentContext().getRouteHost() != null && RequestContext.getCurrentContext().sendZuulResponse(); }
这里的访问地址也是在配置文件中配置的,只是不是通过服务名,而是通过url来达到转发的目的,所以做不到负载均衡
/***/ 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 = this.getVerb(request); InputStream requestEntity = this.getRequestBody(request); if (this.getContentLength(request) < 0L) { context.setChunkedRequestBody(); } /***/ String uri = this.helper.buildZuulRequestURI(request); this.helper.addIgnoredHeaders(new String[0]); /***/ try { /***/ CloseableHttpResponse response = this.forward(this.httpClient, verb, uri, request, headers, params, requestEntity); this.setResponse(response); return null; } catch (Exception var9) { throw new ZuulRuntimeException(var9); } }
该过滤器中也做到了服务的转发,并得到的返回数据,只是它是直接向routeHost参数的物理地址发起请求,请求是直接通过httpclient包实现的,而且没有使用Hystrix命令进行包装,所以这类请求并没有线程隔离和断路器的保护。
SendForwardFilter:序号500,第三个执行;该过滤器针对的是带有”forward.to“参数的url请求
/**本过滤器只针对”forward.to的本地跳转“*/ public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); return ctx.containsKey("forward.to") && !ctx.getBoolean("sendForwardFilter.ran", false); }
具体执行过程如下
public Object run() { try { RequestContext ctx = RequestContext.getCurrentContext(); String path = (String)ctx.get("forward.to"); RequestDispatcher dispatcher = ctx.getRequest().getRequestDispatcher(path); if (dispatcher != null) { ctx.set("sendForwardFilter.ran", true); if (!ctx.getResponse().isCommitted()) { dispatcher.forward(ctx.getRequest(), ctx.getResponse()); ctx.getResponse().flushBuffer(); } } } catch (Exception var4) { ReflectionUtils.rethrowRuntimeException(var4); } return null; }
通过分析上述代码,可以看出,他们的执行条件都是在pre过滤器中的PreDecorationFilter中设置的,并且3个条件是互斥的,所以
在route类型过滤器中,一个url请求通常只能被上述3个过滤器中的一个来处理。
Post过滤器
SendErrorFilter:序号0;是post阶段第一个执行的过滤器;之前的执行过程发生错误的情况下执行该过滤器
执行条件如下:之前的过滤器出现异常并在RequestContext中设置了异常的才允许执行
请求上下文中包含error.status_code参数(由之前执行的过滤器设置的错误编码)并且还没有被该过滤器处理过的时候执行。
public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); return ctx.getThrowable() != null && !ctx.getBoolean("sendErrorFilter.ran", false); }
具体处理逻辑如下:
public Object run() { try { /***/ RequestContext ctx = RequestContext.getCurrentContext(); SendErrorFilter.ExceptionHolder exception = this.findZuulException(ctx.getThrowable()); HttpServletRequest request = ctx.getRequest(); request.setAttribute("javax.servlet.error.status_code", exception.getStatusCode()); log.warn("Error during filtering", exception.getThrowable()); request.setAttribute("javax.servlet.error.exception", exception.getThrowable()); if (StringUtils.hasText(exception.getErrorCause())) { request.setAttribute("javax.servlet.error.message", exception.getErrorCause()); } /***/ RequestDispatcher dispatcher = request.getRequestDispatcher(this.errorPath); if (dispatcher != null) {
/**错误被处理后,标记一下,避免二次处理*/ ctx.set("sendErrorFilter.ran", true); if (!ctx.getResponse().isCommitted()) { ctx.setResponseStatusCode(exception.getStatusCode()); dispatcher.forward(request, ctx.getResponse()); } } } catch (Exception var5) { ReflectionUtils.rethrowRuntimeException(var5); } return null; }
将错误信息组织起来,通过发送一个forward到API网关/error错误端点的请求来产生错误响应。
SendResponseFilter:序号1000;
没有异常,并且响应头或者相应体或者响应数据流不为空的情况下执行
/***/ public boolean shouldFilter() { RequestContext context = RequestContext.getCurrentContext(); return context.getThrowable() == null && (!context.getZuulResponseHeaders().isEmpty() || context.getResponseDataStream() != null || context.getResponseBody() != null); }
具体执行逻辑
public Object run() { try { this.addResponseHeaders(); this.writeResponse(); } catch (Exception var2) { ReflectionUtils.rethrowRuntimeException(var2); } return null; }
利用请求上下文的响应信息来组织需要发送回客户端的响应内容。
LocationRewriteFilter:序号900;
只有重定向时才执行该过滤器
public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); int statusCode = ctx.getResponseStatusCode(); return HttpStatus.valueOf(statusCode).is3xxRedirection(); }
具体执行逻辑
public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); Route route = this.routeLocator.getMatchingRoute(this.urlPathHelper.getPathWithinApplication(ctx.getRequest())); /***/ if (route != null) { Pair<String, String> lh = this.locationHeader(ctx); /***/ if (lh != null) { String location = (String)lh.second(); URI originalRequestUri = UriComponentsBuilder.fromHttpRequest(new ServletServerHttpRequest(ctx.getRequest())).build().toUri(); UriComponentsBuilder redirectedUriBuilder = UriComponentsBuilder.fromUriString(location); UriComponents redirectedUriComps = redirectedUriBuilder.build(); String newPath = this.getRestoredPath(this.zuulProperties, route, redirectedUriComps); String modifiedLocation = redirectedUriBuilder.scheme(originalRequestUri.getScheme()).host(originalRequestUri.getHost()).port(originalRequestUri.getPort()).replacePath(newPath).build().toUriString(); lh.setSecond(modifiedLocation); } } return null; }
负责将标头重写为Zuul的URL,否则,浏览器会重定向到Web应用程序的URL而不是Zuul URL。