zuul 1.x 是如何實現請求轉發的
文檔寫的再好,也不如源碼寫的好
源碼地址:
GitHub: https://github.com/Netflix/zuul
Gitee: https://gitee.com/github_mirror_plus/zuul
簡介
官方簡介,其實你要看這篇,說明你知道 zuul
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/wiki
Here are some links to help you learn more about the Zuul Project. Feel free to PR to add any other info, presentations, etc.
實現邏輯
上一篇文章 Go | Go 結合 Consul 實現動態反向代理 里面簡單的實現了一個反向代理,並簡述了一下步驟,這里復述一下
根據代理的描述一共分成幾個步驟:
- 代理接收到客戶端的請求,復制了原來的請求對象
- 根據一些規則,修改新請求的請求指向
- 把新請求發送到根據服務器端,並接收到服務器端返回的響應
- 將上一步的響應根據需求處理一下,然后返回給客戶端
源碼
注意:這里的源碼指的是 1.x 分支的代碼
基於 Servlet 的請求轉發
在一開始學習 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
ZuulServlet 核心代碼
代碼在 com.netflix.zuul.http.ZuulServlet
下面的代碼中省略了一部分,在這個過程中主要做了以下幾件事
- 將 將原始的 Request,Response 保存在 ThreadLocal 中,方便以后處理。因為 Tomcat 等 Servlet 容器默認使用了一個請求一個線程處理的方式,所以存在 ThreadLocal 即可在以后的處理流程中方便處理
- 執行前置過濾器
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();
}
}
ZuulRunner 核心代碼
從上面的代碼可以看出轉發的關鍵在於 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();
}
}
RequestContext 核心代碼
主要是 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;
}
}
FilterProcessor 核心代碼
主要邏輯就是找到對應 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());
}
}
}
通過上面的代碼中,可以看到得到簡單的流程圖

在官方示例中,提供了兩個簡單的 Route 的 ZuulFilter 實現
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

