文檔寫的再好,也不如源碼寫的好
源碼地址:
GitHub: https://github.com/Netflix/zuul
Gitee: https://gitee.com/github_mirror_plus/zuulhtml
官方簡介,其實你要看這篇,說明你知道 zuuljava
Zuul is an edge service that provides dynamic routing, monitoring, resiliency, security, and more. Please view the wiki for usage, information, HOWTO, etc https://github.com/Netflix/zuul/wikigit
Here are some links to help you learn more about the Zuul Project. Feel free to PR to add any other info, presentations, etc.github
上一篇文章 Go | Go 結合 Consul 實現動態反向代理 裏面簡單的實現了一個反向代理,並簡述了一下步驟,這裏複述一下web
根據代理的描述一共分紅幾個步驟:apache
注意:這裏的源碼指的是 1.x 分支的代碼編程
在一開始學習 Java Web 時,Servlet 是一個繞不過去的坎,zuul 也是基於 Servlet 實現的,在源碼服務器
<?xml version="1.0" encoding="UTF-8"?> <web-app> <listener> <listener-class>com.netflix.zuul.StartServer</listener-class> </listener> <servlet> <servlet-name>ZuulServlet</servlet-name> <servlet-class>com.netflix.zuul.http.ZuulServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ZuulServlet</servlet-name> <url-pattern>/*</url-pattern> </servlet-mapping> <filter> <filter-name>ContextLifecycleFilter</filter-name> <filter-class>com.netflix.zuul.context.ContextLifecycleFilter</filter-class> </filter> <filter-mapping> <filter-name>ContextLifecycleFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
在這裏須要重點關注下 com.netflix.zuul.http.ZuulServlet
和 com.netflix.zuul.context.ContextLifecycleFilter
app
代碼在 com.netflix.zuul.http.ZuulServlet
ide
下面的代碼中省略了一部分,在這個過程當中主要作了如下幾件事
preRoute()
route()
postRoute()
其中轉發的關鍵就在 route()
方法
public class ZuulServlet extends HttpServlet { private ZuulRunner zuulRunner; @Override public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException { try { // 這一步是將原始的 Request,Response 保存在 ThreadLocal 中 init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); 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(); } } void postRoute() throws ZuulException { zuulRunner.postRoute(); } void route() throws ZuulException { zuulRunner.route(); } void preRoute() throws ZuulException { zuulRunner.preRoute(); } void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) { zuulRunner.init(servletRequest, servletResponse); } void error(ZuulException e) { RequestContext.getCurrentContext().setThrowable(e); zuulRunner.error(); } }
從上面的代碼能夠看出轉發的關鍵在於 ZuulServlet#route()
, 而 ZuulServlet#route()
在於 zuulRunner.route()
ZuulRunner 主要功能
init
將 Request 和 Response 保存到 RequestContext.getCurrentContext()
, 這裏面就是上面提到的 ThreadLocal
的處理類FilterProcessor.getInstance().route()
public class ZuulRunner { private boolean bufferRequests; public ZuulRunner() { this.bufferRequests = true; } public ZuulRunner(boolean bufferRequests) { this.bufferRequests = bufferRequests; } 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)); } public void route() throws ZuulException { FilterProcessor.getInstance().route(); } }
主要是 ThreadLocal 和 copy
public class RequestContext extends ConcurrentHashMap<String, Object> { private static final Logger LOG = LoggerFactory.getLogger(RequestContext.class); protected static Class<? extends RequestContext> contextClass = RequestContext.class; private static RequestContext testContext = null; protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() { @Override protected RequestContext initialValue() { try { return contextClass.newInstance(); } catch (Throwable e) { throw new RuntimeException(e); } } }; /** * sets the "responseBody" value as a String. This is the response sent back to the client. * * @param body */ public void setResponseBody(String body) { set("responseBody", body); } /** * Use this instead of response.setStatusCode() * * @param nStatusCode */ public void setResponseStatusCode(int nStatusCode) { getResponse().setStatus(nStatusCode); set("responseStatusCode", nStatusCode); } /** * Mkaes a copy of the RequestContext. This is used for debugging. * * @return */ public RequestContext copy() { RequestContext copy = new RequestContext(); // 這裏省略了一部分代碼,意思就是把原來的 request 深度複製一份 return copy; } }
主要邏輯就是找到對應 type 的 List<ZuulFilter>
並執行 runFilter()
public class FilterProcessor { static FilterProcessor INSTANCE = new FilterProcessor(); /** * @return the singleton FilterProcessor */ public static FilterProcessor getInstance() { return INSTANCE; } /** * 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 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; } } } /** * Publishes a counter metric for each filter on each use. */ public static class BasicFilterUsageNotifier implements FilterUsageNotifier { private static final String METRIC_PREFIX = "zuul.filter-"; @Override public void notify(ZuulFilter filter, ExecutionStatus status) { DynamicCounter.increment(METRIC_PREFIX + filter.getClass().getSimpleName(), "status", status.name(), "filtertype", filter.filterType()); } } }
經過上面的代碼中,能夠看到獲得簡單的流程圖
SimpleHostRoutingFilter.groovy
在這個示例中,在 Filter 實現中將請求複製並轉發到目標服務,這個是簡單的邏輯
class SimpleHostRoutingFilter extends ZuulFilter { // 聲明這個過濾器是 route 類型 @Override String filterType() { return 'route' } // 過濾器的執行邏輯 Object run() { HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); Header[] headers = buildZuulRequestHeaders(request) String verb = getVerb(request); InputStream requestEntity = request.getInputStream(); CloseableHttpClient httpclient = CLIENT.get() String uri = request.getRequestURI() if (RequestContext.getCurrentContext().requestURI != null) { uri = RequestContext.getCurrentContext().requestURI } try { // 將請求轉發到指定服務器 HttpResponse response = forward(httpclient, verb, uri, request, headers, requestEntity) setResponse(response) } catch (Exception e) { throw e; } return null } HttpResponse forward(CloseableHttpClient httpclient, String verb, String uri, HttpServletRequest request, Header[] headers, InputStream requestEntity) { requestEntity = debug(verb, uri, request, headers, requestEntity) HttpHost httpHost = getHttpHost() HttpRequest httpRequest; switch (verb) { case 'POST': httpRequest = new HttpPost(uri + getQueryString()) InputStreamEntity entity = new InputStreamEntity(requestEntity, request.getContentLength()) httpRequest.setEntity(entity) break case 'PUT': httpRequest = new HttpPut(uri + getQueryString()) InputStreamEntity entity = new InputStreamEntity(requestEntity, request.getContentLength()) httpRequest.setEntity(entity) break; default: httpRequest = new BasicHttpRequest(verb, uri + getQueryString()) } try { httpRequest.setHeaders(headers) return forwardRequest(httpclient, httpHost, httpRequest) } finally { //httpclient.close(); } } HttpResponse forwardRequest(HttpClient httpclient, HttpHost httpHost, HttpRequest httpRequest) { return httpclient.execute(httpHost, httpRequest); } }
ZuulNFRequest 結合 Netflix 的 route 過濾器
這個示例中,從 HttpClient
轉發改成了使用 RibbonCommand
轉發,從而使用了 Ribbon 的功能。關於 Ribbon
之後有時間再說
class ZuulNFRequest extends ZuulFilter { @Override String filterType() { return 'route' } boolean shouldFilter() { return NFRequestContext.currentContext.getRouteHost() == null && RequestContext.currentContext.sendZuulResponse() } Object run() { NFRequestContext context = NFRequestContext.currentContext HttpServletRequest request = context.getRequest(); MultivaluedMap<String, String> headers = buildZuulRequestHeaders(request) MultivaluedMap<String, String> params = buildZuulRequestQueryParams(request) Verb verb = getVerb(request); Object requestEntity = getRequestBody(request) IClient restClient = ClientFactory.getNamedClient(context.getRouteVIP()); String uri = request.getRequestURI() if (context.requestURI != null) { uri = context.requestURI } //remove double slashes uri = uri.replace("//", "/") HttpResponse response = forward(restClient, verb, uri, headers, params, requestEntity) setResponse(response) return response } def HttpResponse forward(RestClient restClient, Verb verb, uri, MultivaluedMap<String, String> headers, MultivaluedMap<String, String> params, InputStream requestEntity) { debug(restClient, verb, uri, headers, params, requestEntity) // restClient.apacheHttpClient.params.setVirtualHost(headers.getFirst("host")) String route = NFRequestContext.getCurrentContext().route if (route == null) { String path = RequestContext.currentContext.requestURI if (path == null) { path = RequestContext.currentContext.getRequest() getRequestURI() } route = "route" //todo get better name } route = route.replace("/", "_") RibbonCommand<AbstractLoadBalancerAwareClient<HttpRequest, HttpResponse>> command = new RibbonCommand<>(restClient, verb, uri, headers, params, requestEntity); try { HttpResponse response = command.execute(); return response } catch (HystrixRuntimeException e) { if (e?.fallbackException?.cause instanceof ClientException) { ClientException ex = e.fallbackException.cause as ClientException throw new ZuulException(ex, "Forwarding error", 500, ex.getErrorType().toString()) } throw new ZuulException(e, "Forwarding error", 500, e.failureType.toString()) } } }
從 zuul 實現中看,仍是基於 Servlet 的,並在過程當中加入 前、中、後和異常處理鏈。由於基於 Servlet 其處理流程是阻塞的,性能會有所降低。
在 zuul 裏面採用了 java 和 groovy 混合編程的方式,編程更加靈活。經過自定了一個 GroovyCompiler 來加載指定路徑的 groovy 文件來實如今運行中動態添加 ZuulFilter 這種動態機制在必定程度上實現了熱更新 ZuulFilter 功能,也是值得學習的。
GitHub: https://github.com/Netflix/zuul