zuul 作為springCloud 的全家桶組件之一,有着不可或缺的分量。它作為一個普通java API網關,自有網關的好處:
避免將內部信息暴露給外部;
統一服務端應用入口;
為微服務添加額外的安全層;
支持混合通信協議;
降低構建微服務的復雜性;
微服務模擬與虛擬化;
zuul 基本上已經被springCloud 處理為一個開箱即用的一個組件了,所以基本上只需要添加相應依賴和一些必要配置,該網關就可以跑起來了。(這表面和nginx反向代理部分功能看起來是差不多的)
讓我們來快速實踐一下吧!
一、zuul入坑基本實踐步驟
1.1. 引入 pom 依賴
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <modelVersion>4.0.0</modelVersion> <groupId>zuul-test</groupId> <artifactId>com.youge</artifactId> <version>1.0</version> <!-- 引入spingcloud 全家桶 --> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.RC2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <!-- 導入服務網關zuul --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> </dependencies>
以上就是我們整個demo的全部maven依賴了,很簡潔吧。這也是springboot的初衷,把所有的工作都移到幕后,讓業務更簡潔。
1.2. 編寫網關入口類
如下為整個網關的入口類,實際上就是兩個注解發生了化學反應。@EnableZuulProxy 是本文的主角,它會開啟網關相關的服務。
@SpringBootApplication @EnableZuulProxy public class MyZuulGateway { // 只有一個空的main方法 public static void main(String[] args) { SpringApplication.run(MyZuulGateway.class, args); } }
就是這么簡單!
1.3. 添加測試配置項
在application.properties配置文件中添加如下配置,主要使用一個路由配置驗證即可!
server.port=9000 spring.application.name=my-zuul-gateway #本地環境配置zuul轉發的規則: # 忽略所有微服務,只路由指定微服務 # 如下配置為將 /sapi/** 的路徑請求,轉發到 http://127.0.0.1:8082/fileenc/ 上去。 zuul.ignored-services=* zuul.routes.fileenc1.url=http://127.0.0.1:8082/fileenc/ zuul.routes.fileenc1.path=/sapi/**
如上就可以將網關跑起來了,如果你連后台服務也沒有,沒關系,自己寫一個就好了。
@GetMapping("hello")
public Object hello() {
return "hello, world";
}
1.4. 測試網關
以上就已經將整個網關搞好了,run一下就ok. 測試方式就是直接瀏覽器里訪問下該網關地址就好了:
http://localhost:9000/sapi/test/hello?a=1&b=22 .
如果你看到 “hello, world”, 恭喜你,zuul已入坑。
二、zuul是如何轉發請求的?
根據上面的觀察,zuul已經基本可以滿足我們的開發需求了,后續更多要做的可能就是一些安全相關,業務相關,優化相關的東西了。不過在做這些之前,我們可以先多問一個問題,zuul是如何將請求轉發給后台服務的呢?
這實際上和zuul的架構相關:

zuul的中核心概念是:Filter. 運行時邏輯上分為多種類型的Filter,各類型Filter處理時機不同! PRE:這種過濾器在請求被路由之前調用;ROUTING:這種過濾器將請求路由到微服務;POST:這種過濾器在路由到微服務以后執行;ERROR:在其他階段發生錯誤時執行該過濾器;
所以,整體上來說,它的轉發流程會經過一系列的過濾器,然后再進行實際的轉發。
如果只想了解其最終是如何轉的可以直奔主題,而如果要添加你的功能,則需要編寫一些對應生命周期的過濾器。
原本要分析zuul是如何處理請求的,但是實際上,zuul被整合到spring之后,就完全地符合了一個springmvc的編程模型了。所有對該網關的請求會先調用 ZuulController 進行請求的接收,然后到 service處理,再到響應這么一個過程。
整個 ZuulController 非常地簡單:就是一個請求的委托過程!
// org.springframework.cloud.netflix.zuul.web.ZuulController public class ZuulController extends ServletWrappingController { public ZuulController() { setServletClass(ZuulServlet.class); setServletName("zuul"); setSupportedMethods((String[]) null); // Allow all } @Override public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { try { // We don't care about the other features of the base class, just want to // handle the request return super.handleRequestInternal(request, response); } finally { // @see com.netflix.zuul.context.ContextLifecycleFilter.doFilter RequestContext.getCurrentContext().unset(); } } } // org.springframework.web.servlet.mvc.ServletWrappingController#handleRequestInternal /** * Invoke the wrapped Servlet instance. * @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse) */ @Override protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { Assert.state(this.servletInstance != null, "No Servlet instance"); // 該 servletInstance 是 ZuulServlet, 整個zuul的實現框架由其控制 this.servletInstance.service(request, response); return null; } // com.netflix.zuul.http.ZuulServlet#service @Override public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException { try { // 初始化請求,由 zuulRunner 處理 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 // setZuulEngineRan 會旋轉一個標識: "zuulEngineRan", true 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(); } }
以上就是整個zuul對於普通請求的處理框架部分了。邏輯還是比較清晰的,簡單的,前置+轉發+后置處理。我們就幾個重點部分說明一下:
2.1. 請求初始化
該部分主要是將外部請求,接入到 zuul 的處理流程上,當然下面的實現主要是使用了 ThreadLocal 實現了上下文的銜接。
// com.netflix.zuul.http.ZuulServlet#init /** * initializes request * * @param servletRequest * @param servletResponse */ void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) { zuulRunner.init(servletRequest, servletResponse); } // com.netflix.zuul.ZuulRunner#init /** * sets HttpServlet request and HttpResponse * * @param servletRequest * @param servletResponse */ public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) { // RequestContext 使用 ThreadLocal 進行保存,且保證有值 // 且 RequestContext 繼承了 ConcurrentHashMap, 保證了操作的線程安全 RequestContext ctx = RequestContext.getCurrentContext(); if (bufferRequests) { ctx.setRequest(new HttpServletRequestWrapper(servletRequest)); } else { ctx.setRequest(servletRequest); } ctx.setResponse(new HttpServletResponseWrapper(servletResponse)); }
以上就是一個 zuul 請求的初始化了,簡單地說就是設置好請求上下文,備用。
2.2. 前置處理過濾器
前置處理過濾器主要用於標記一些請求類型,權限驗證,安全過濾等等。是不可或缺一環。具體實現自行處理!我們來看一個整體的通用流程:
// com.netflix.zuul.http.ZuulServlet#preRoute /** * executes "pre" filters * * @throws ZuulException */ void preRoute() throws ZuulException { zuulRunner.preRoute(); } // com.netflix.zuul.ZuulRunner#preRoute /** * executes "pre" filterType ZuulFilters * * @throws ZuulException */ public void preRoute() throws ZuulException { // FilterProcessor 是個單例 FilterProcessor.getInstance().preRoute(); } // com.netflix.zuul.FilterProcessor#preRoute /** * runs all "pre" filters. These filters are run before routing to the orgin. * * @throws ZuulException */ public void preRoute() throws ZuulException { try { // 調用Type 為 pre 的過濾器 runFilters("pre"); } catch (ZuulException e) { throw e; } catch (Throwable e) { throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName()); } } // com.netflix.zuul.FilterProcessor#runFilters /** * 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; // 通過 FilterLoader 的單例,獲取所有注冊為 sType 的過濾器 // 存放 Filters 的容器自然也是線程安全的,為 ConcurrentHashMap // - org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter // - org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter // - org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter // - org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter // - org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType); if (list != null) { for (int i = 0; i < list.size(); i++) { ZuulFilter zuulFilter = list.get(i); // 依次處理每個 filter Object result = processZuulFilter(zuulFilter); if (result != null && result instanceof Boolean) { bResult |= ((Boolean) result); } } } return bResult; } // 獲取相應的 filters // com.netflix.zuul.FilterLoader#getFiltersByType /** * Returns a list of filters by the filterType specified * * @param filterType * @return a List<ZuulFilter> */ public List<ZuulFilter> getFiltersByType(String filterType) { List<ZuulFilter> list = hashFiltersByType.get(filterType); if (list != null) return list; list = new ArrayList<ZuulFilter>(); Collection<ZuulFilter> filters = filterRegistry.getAllFilters(); for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) { ZuulFilter filter = iterator.next(); if (filter.filterType().equals(filterType)) { list.add(filter); } } Collections.sort(list); // sort by priority hashFiltersByType.putIfAbsent(filterType, list); return list; } // com.netflix.zuul.FilterProcessor#processZuulFilter /** * 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(); } // 調用各filter的 runFilter() 方法,觸發filter作用 // 如果filter被禁用,則不會調用 zuul.ServletDetectionFilter.pre.disable=true, 代表禁用 pre // 具體實現邏輯由各 filter 決定 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(); // 使用 StringBuilder 記錄請求處理日志 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; } } } // com.netflix.zuul.ZuulFilter#runFilter /** * runFilter checks !isFilterDisabled() and shouldFilter(). The run() method is invoked if both are true. * * @return the return from ZuulFilterResult */ public ZuulFilterResult runFilter() { ZuulFilterResult zr = new ZuulFilterResult(); // 如果被禁用則不會觸發真正地調用 if (!isFilterDisabled()) { // shouldFilter() 由各filter決定,返回true時執行filter if (shouldFilter()) { Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName()); try { Object res = run(); zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS); } catch (Throwable e) { t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed"); zr = new ZuulFilterResult(ExecutionStatus.FAILED); zr.setException(e); } finally { t.stopAndLog(); } } else { // 打上跳過標識 zr = new ZuulFilterResult(ExecutionStatus.SKIPPED); } } return zr; } // run樣例: org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter#run @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; }
如上,就是一個preFilter的處理流程了:
1. 從 FilterLoader 中獲取所有 pre 類型的filter;
2. 依次調用各filter的runFilter()方法,觸發filter;
3. 調用前先調用 shouldFilter() 進行判斷該filter對於此次請求是否有用, 各filter實現可以從上下文中取得相應的信息,各自判定;
4. 計數器加1;
5. 默認就會有多個filter可調用, 不夠滿足業務場景再自行添加,各業務執行方法為 run();
2.3. 正常路由處理
zuul 的本職工作,是對路徑的轉發路由(正向代理 or 反向代理),如下處理:
// com.netflix.zuul.http.ZuulServlet#route /** * executes "route" filters * * @throws ZuulException */ void route() throws ZuulException { zuulRunner.route(); } // com.netflix.zuul.ZuulRunner#route /** * executes "route" filterType ZuulFilters * * @throws ZuulException */ public void route() throws ZuulException { FilterProcessor.getInstance().route(); } // com.netflix.zuul.FilterProcessor#route /** * Runs all "route" filters. These filters route calls to an origin. * * @throws ZuulException if an exception occurs. */ public void route() throws ZuulException { try { // 同樣,獲取filter類型為 route 的 filters, 進行調用處理即可 // - org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter // - org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter // - org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter runFilters("route"); } catch (ZuulException e) { throw e; } catch (Throwable e) { throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_ROUTE_FILTER_" + e.getClass().getName()); } } // 其中,Ribbon 的處理需要有 ribbon 組件的引入和配置 // org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter#shouldFilter @Override public boolean shouldFilter() { RequestContext ctx = RequestContext.getCurrentContext(); // 判斷是否有 serviceId, 且 sendZuulResponse=true 才會進行 ribbon 處理 return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null && ctx.sendZuulResponse()); } 以下是普通路由轉發的實現,只要配置了相應的路由信息,則會進行相關轉發: // org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#shouldFilter @Override public boolean shouldFilter() { return RequestContext.getCurrentContext().getRouteHost() != null && RequestContext.getCurrentContext().sendZuulResponse(); } @Override public Object run() { RequestContext context = RequestContext.getCurrentContext(); // step1. 構建http請求頭信息 HttpServletRequest request = context.getRequest(); MultiValueMap<String, String> headers = this.helper .buildZuulRequestHeaders(request); // step2. 構建 params 信息, 如: a=111&&b=222 MultiValueMap<String, String> params = this.helper .buildZuulRequestQueryParams(request); // 獲取請求類型, GET,POST,PUT,DELETE String verb = getVerb(request); // step3. 構建請求體信息,如文件 InputStream requestEntity = getRequestBody(request); // 如果沒有 Content-Length 字段,則設置 chunkedRequestBody:true if (getContentLength(request) < 0) { context.setChunkedRequestBody(); } // step4. 構建要轉發的uri地址信息 String uri = this.helper.buildZuulRequestURI(request); this.helper.addIgnoredHeaders(); try { // step5. 請求轉發出去,等待響應 // 具體如何轉發請求,是在 forward 中處理的 CloseableHttpResponse response = forward(this.httpClient, verb, uri, request, headers, params, requestEntity); // 將結果放到上下文中,以備后續filter處理 setResponse(response); } catch (Exception ex) { throw new ZuulRuntimeException(ex); } return null; } // step1. 構建http請求頭信息 // org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper#buildZuulRequestHeaders public MultiValueMap<String, String> buildZuulRequestHeaders( HttpServletRequest request) { RequestContext context = RequestContext.getCurrentContext(); MultiValueMap<String, String> headers = new HttpHeaders(); Enumeration<String> headerNames = request.getHeaderNames(); // 獲取所有的 header 信息,還原到 headers 中 if (headerNames != null) { while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); // 排除一些特別的的頭信息 if (isIncludedHeader(name)) { Enumeration<String> values = request.getHeaders(name); while (values.hasMoreElements()) { String value = values.nextElement(); headers.add(name, value); } } } } // 添加本次路由轉發新增的頭信息 Map<String, String> zuulRequestHeaders = context.getZuulRequestHeaders(); for (String header : zuulRequestHeaders.keySet()) { headers.set(header, zuulRequestHeaders.get(header)); } headers.set(HttpHeaders.ACCEPT_ENCODING, "gzip"); return headers; } // step2. 構建 params 信息, 如: a=111&&b=222 // org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper#buildZuulRequestQueryParams public MultiValueMap<String, String> buildZuulRequestQueryParams( HttpServletRequest request) { // 解析 getQueryString 中的 a=111&b=222... 信息 Map<String, List<String>> map = HTTPRequestUtils.getInstance().getQueryParams(); MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); if (map == null) { return params; } for (String key : map.keySet()) { for (String value : map.get(key)) { params.add(key, value); } } return params; } // 解析請求url中的k=v&k2=v2 為 map 格式 // com.netflix.zuul.util.HTTPRequestUtils#getQueryParams /** * returns query params as a Map with String keys and Lists of Strings as values * @return */ public Map<String, List<String>> getQueryParams() { Map<String, List<String>> qp = RequestContext.getCurrentContext().getRequestQueryParams(); if (qp != null) return qp; HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); qp = new LinkedHashMap<String, List<String>>(); if (request.getQueryString() == null) return null; StringTokenizer st = new StringTokenizer(request.getQueryString(), "&"); int i; while (st.hasMoreTokens()) { String s = st.nextToken(); i = s.indexOf("="); if (i > 0 && s.length() >= i + 1) { String name = s.substring(0, i); String value = s.substring(i + 1); try { name = URLDecoder.decode(name, "UTF-8"); } catch (Exception e) { } try { value = URLDecoder.decode(value, "UTF-8"); } catch (Exception e) { } List<String> valueList = qp.get(name); if (valueList == null) { valueList = new LinkedList<String>(); qp.put(name, valueList); } valueList.add(value); } else if (i == -1) { String name=s; String value=""; try { name = URLDecoder.decode(name, "UTF-8"); } catch (Exception e) { } List<String> valueList = qp.get(name); if (valueList == null) { valueList = new LinkedList<String>(); qp.put(name, valueList); } valueList.add(value); } } RequestContext.getCurrentContext().setRequestQueryParams(qp); return qp; } // step3. 構建請求體信息,如文件 // org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#getRequestBody protected InputStream getRequestBody(HttpServletRequest request) { InputStream requestEntity = null; try { // 先向 requestEntity 中獲取輸入流,如果沒有則向 servlet 中獲取 requestEntity = (InputStream) RequestContext.getCurrentContext().get(REQUEST_ENTITY_KEY); if (requestEntity == null) { // 向 HttpServletRequest 中獲取原始的輸入流 requestEntity = request.getInputStream(); } } catch (IOException ex) { log.error("error during getRequestBody", ex); } return requestEntity; } // step4. 構建要轉發的uri地址信息 // org.springframework.cloud.netflix.zuul.filters.ProxyRequestHelper#buildZuulRequestURI public String buildZuulRequestURI(HttpServletRequest request) { RequestContext context = RequestContext.getCurrentContext(); // 原始請求 uri String uri = request.getRequestURI(); // 路由轉換之后的請求 uri String contextURI = (String) context.get(REQUEST_URI_KEY); if (contextURI != null) { try { // 防止亂碼,urlencode 一下 uri = UriUtils.encodePath(contextURI, characterEncoding(request)); } catch (Exception e) { log.debug( "unable to encode uri path from context, falling back to uri from request", e); } } return uri; } // step5. 請求轉發出去,等待響應 // 具體如何轉發請求,是在 forward 中處理的 // org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#forward 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 uri = StringUtils.cleanPath((host.getPath() + uri).replaceAll("/{2,}", "/")); long contentLength = getContentLength(request); ContentType contentType = null; if (request.getContentType() != null) { contentType = ContentType.parse(request.getContentType()); } // 使用InputStreamEntity封裝inputStream請求,該inputStream是從socket接入后的原始輸入流 // 后續 httpclient 進行數據讀取時,將由其進行提供相應讀數據方法 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()); // 提交給 httpclient 組件執行 http 請求,並返回結果 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(); } } // org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#buildHttpRequest 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; // DELETE 時會做兩步操作 entityRequest.setEntity(entity); break; default: // 除以上幾種情況,都使用 BasicHttpRequest 進行處理即可 httpRequest = new BasicHttpRequest(verb, uriWithQueryString); log.debug(uriWithQueryString); } // 統一都設置請求頭,將map轉換為 BasicHeader httpRequest.setHeaders(convertHeaders(headers)); return httpRequest; } // org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter#forwardRequest private CloseableHttpResponse forwardRequest(CloseableHttpClient httpclient, HttpHost httpHost, HttpRequest httpRequest) throws IOException { return httpclient.execute(httpHost, httpRequest); }
可見整個真正的轉發流程,主要分幾步:
1. 解析http請求頭信息,並添加自己部分的頭信息;
2. 解析並保留請求參數信息, 如: a=111&&b=222;
3. 獲取原始的inputStream信息,如文件;
4. 根據路由配置,構建要轉發的uri地址信息;
5. 使用httpclient組件,將請求轉發出去,並等待響應,設置到 response中;
實際上,真正的轉發仍然是依次做好相應判斷,然后還原成對應的請求,再轉發后后端服務中。
以上,就是一個普通的服務轉發實現了。並沒有太多的技巧,而是最基礎的步驟:接收請求,解析參數,重新構建請求,請求后端,獲得結果。
2.4. 后置過濾器
后置處理器可以做一些請求完服務端之后,對客戶端的響應數據,包括正常數據流的輸出,錯誤信息的返回等。如 SendResponseFilter, SendErrorFilter...
// com.netflix.zuul.http.ZuulServlet#postRoute /** * executes "post" ZuulFilters * * @throws ZuulException */ void postRoute() throws ZuulException { zuulRunner.postRoute(); } // com.netflix.zuul.ZuulRunner#postRoute /** * executes "post" filterType ZuulFilters * * @throws ZuulException */ public void postRoute() throws ZuulException { FilterProcessor.getInstance().postRoute(); } // com.netflix.zuul.FilterProcessor#postRoute /** * 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 { // 獲取類型為 post 的 filter, 調用 // 默認為: org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter runFilters("post"); } catch (ZuulException e) { throw e; } catch (Throwable e) { throw new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_IN_POST_FILTER_" + e.getClass().getName()); } } // org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#shouldFilter @Override public boolean shouldFilter() { // 有響應的數據,就可以進行處理 RequestContext context = RequestContext.getCurrentContext(); return context.getThrowable() == null && (!context.getZuulResponseHeaders().isEmpty() || context.getResponseDataStream() != null || context.getResponseBody() != null); } // org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#run @Override public Object run() { try { // 添加header信息 addResponseHeaders(); // 輸出數據流到請求端 writeResponse(); } catch (Exception ex) { ReflectionUtils.rethrowRuntimeException(ex); } return null; } // org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#addResponseHeaders 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()); } } // 向 response 中添加header List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders(); if (zuulResponseHeaders != null) { for (Pair<String, String> it : zuulResponseHeaders) { 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()); } } } } // org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#writeResponse() 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"); } 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)) { servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip"); } else { is = handleGzipStream(is); } } } 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); } } 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()); } } } // org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter#writeResponse private void writeResponse(InputStream zin, OutputStream out) throws Exception { // 默認大小 8192 byte[] bytes = buffers.get(); int bytesRead = -1; // 依次向 outputStream 中寫入字節流 while ((bytesRead = zin.read(bytes)) != -1) { out.write(bytes, 0, bytesRead); } }
同樣,對客戶端的輸出,就是這么簡單:解析出header信息,將response write() 到客戶端的socket中。即完成任務。
以上,我們主要看了幾個非常普通的filter的處理過程,理解了下 zuul 的運行流程,當然主要的目的分析zuul是如何轉發請求的。基本上上面所有的filter都會繼承 ZuulFilter 的抽象,它提供兩個重要的統一的方法:isFilterDisabled() 和 shouldFilter() 方法用於控制過慮器是否啟用或者是否應該使用,並統一了返回結果。

zuul 整體實現也是非常簡單明了,基於模板方法模式 和 責任鏈模式 和 單例模式,基本搞定。只是更多的花需要應用自己去玩了。
三、自行實現一個業務filter
要想做到通用的框架,這點事情是必須要做的。當然,還必須要足夠簡單,如下:一個注解加一個繼承實現即可!
// 一個注解,@Component, 成功 spring bean 組件 // 一個繼承,ZuulFilter, 以使 zuul 框架可以按照規范進行filter 的接入 @Component public class MyOneFilter extends ZuulFilter { private final UrlPathHelper urlPathHelper = new UrlPathHelper(); @Autowired private ZuulProperties zuulProperties; @Autowired private RouteLocator routeLocator; public MyOneFilter() { } public MyOneFilter(ZuulProperties zuulProperties, RouteLocator routeLocator) { this.routeLocator = routeLocator; this.zuulProperties = zuulProperties; } @Override public String filterType() { // 自定義過濾器的類型,知道為什么不用枚舉類嗎?嘿嘿 return PRE_TYPE; } @Override public int filterOrder() { // 定義過濾器的出場順序,越小越牛 return 1; } @Override public boolean shouldFilter() { // 是否可以啟用當前filter, 按你的業務規則來說了算 return true; } @Override public Object run() { // 如果滿足了過濾條件,你想怎么做都行,RequestContext中有你想要的一切 RequestContext ctx = RequestContext.getCurrentContext(); Route route = routeLocator.getMatchingRoute( urlPathHelper.getPathWithinApplication(ctx.getRequest())); System.out.println("in my one filter"); return null; } }
至於其他配置項什么的,自行查看官網即可! https://www.springcloud.cc/spring-cloud-greenwich.html#_router_and_filter_zuul
四、幾點思考
zuul 既然作為cloud的通用網關,必然會承受着比其他應用更大的流量,同時也要擔起着比其他應用更高的QOC。可謂責任重大!
然而縱觀前面的實現,並沒有什么牛逼的技術。相反,看到更多是為了業務的需要,需要進行反復的數據拷貝。
很顯然,網關類的服務,是非常典型的IO密集型應用,但似乎並沒有看到它在這方面的努力(默認web服務器是tomcat,這就是其上限,如果換成netty又當如何)。也許,它還得需要前置網關,負載均衡,流量分發,才能夠發揮其應有的作用。(把它當作普通應用就沒事了,雖然它也在做負載均衡流量分發)
對於大文件的上傳,它是通過先將文件流存儲到本地臨時文件,再上傳后端服務中,這個過程必然會導致響應緩慢以及應對異常能力的變弱。 而且,普通網關請求 zuul 中對於會從servlet中獲取輸入流,並轉化為byte數組,也就是說他會保持全量上傳數據,這對於超大文件來說,肯定是不可取的。所以,官網上也特別說你得如何小心處理大文件的上傳!
