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. 代理接收到客户端的请求,复制了原来的请求对象
  2. 根据一些规则,修改新请求的请求指向
  3. 把新请求发送到根据服务器端,并接收到服务器端返回的响应
  4. 将上一步的响应根据需求处理一下,然后返回给客户端

源码

注意:这里的源码指的是 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.ZuulServletcom.netflix.zuul.context.ContextLifecycleFilter

ZuulServlet 核心代码

代码在 com.netflix.zuul.http.ZuulServlet

下面的代码中省略了一部分,在这个过程中主要做了以下几件事

  1. 将 将原始的 Request,Response 保存在 ThreadLocal 中,方便以后处理。因为 Tomcat 等 Servlet 容器默认使用了一个请求一个线程处理的方式,所以存在 ThreadLocal 即可在以后的处理流程中方便处理
  2. 执行前置过滤器 preRoute()
  3. 执行转发中过滤器 route()
  4. 执行后置过滤器 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 主要功能

  1. init 将 Request 和 Response 保存到 RequestContext.getCurrentContext(), 这里面就是上面提到的 ThreadLocal 的处理类
  2. 调用下 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

Java | zuul 1.x 是如何实现请求转发的的更多相关文章

  1. Java Web中请求转发和请求包含

    1.都是在一个请求中跨越多个Servlet 2.多个Servlet在一个请求中,他们共享request对象.就是在AServle中setAttribute()保存数据在BServlet中由getAtt ...

  2. [Java] 模拟HTTP的Get和Post请求

    在之前,写了篇Java模拟HTTP的Get和Post请求的文章,这篇文章起源与和一个朋友砍飞信诈骗网站的问题,于是动用了Apache的comments-net包,也实现了get和post的http请求 ...

  3. 深入浅出Java 重定向和请求转发的区别

    深入浅出Java 重定向和请求转发的区别 <span style="font-family:FangSong_GB2312;font-size:18px;">impor ...

  4. 7.java的请求转发和请求重定向

    1.请求重定向:是客户端的行为,response.sendRedirect(),从本质上讲等同于两次请求,前一次的请求对象不会保存,地址栏的URL地址会改变,一次新的转发. 2.请求转发:是服务器的行 ...

  5. java重定向与请求转发的区别

    最近工作不算太忙,今天在这里对java中的重定向和请求转发稍作总结,希望能帮助到大家. 请求转发: request.getRequestDispatcher().forward(); 重定向: res ...

  6. JAVA记录-Servlet RequestDispatcher请求转发

    RequestDispatcher接口提供将请求转发送到另一个资源的功能,它可能是html,servlet或jsp等. 此接口也可用于包括另一资源的内容.它是servlet协作的一种方式. 在Requ ...

  7. Angular2,Springboot,Zuul,Shiro跨域CORS请求踩坑实录

    前言:前后端分离,业务分离,网关路由等已经成为当下web application开发的流行趋势.前端以单页面路由为核心的框架为主体,可以单独部署在nodejs或nginx上.后端以springboot ...

  8. 【JAVA】通过URLConnection/HttpURLConnection发送HTTP请求的方法(一)

    Java原生的API可用于发送HTTP请求 即java.net.URL.java.net.URLConnection,JDK自带的类: 1.通过统一资源定位器(java.net.URL)获取连接器(j ...

  9. java重定向与请求转发

    重定向是不能直接访问WEB-INF下的资源的,因为重定向是浏览器二次请求,众所周知,客户端是不能直接访问WEB-INF下的资源的. 而请求转发却可以直接访问. 然而重定向却可以间接访问WEN-INF下 ...

  10. 【Java Web开发学习】跨域请求

    [Java Web开发学习]跨域请求 ================================================= 1.使用jsonp ===================== ...

随机推荐

  1. LeetCode刷题日记 2020/03/25

    力扣刷题继续! 题目:计算三维形体表面积 题干 在 N * N 的网格上,我们放置一些 1 * 1 * 1  的立方体. 每个值 v = grid[i][j] 表示 v 个正方体叠放在对应单元格 (i ...

  2. Spring声明式事务控制配置

    xml配置(配置文件) <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="h ...

  3. 苹果正在测试新款Mac mini:搭载M3芯片 配备24GB大内存

    据悉苹果目前正在测试新的Mac机型,亮点是采用最新的M3芯片. 据报道,首款搭载M3芯片的设备应该是13英寸的MacBook Pro和重新设计的MacBook Air,Mac mini机型并不在名单上 ...

  4. Docker从认识到实践再到底层原理(六-2)|Docker容器操作实例

    前言 那么这里博主先安利一些干货满满的专栏了! 首先是博主的高质量博客的汇总,这个专栏里面的博客,都是博主最最用心写的一部分,干货满满,希望对大家有帮助. 高质量博客汇总 然后就是博主最近最花时间的一 ...

  5. FDConnection的事务测试讲解。。

    总之用事务的宗旨是: 1.不用嵌套事务EnableNested设置为False 2.事务一定要回滚,避免发生异常的情况下,没有回滚 造成,不可估量的错误. try frmClientDm.MyMain ...

  6. TActionManager Delphi 超级方便的快捷键 草草

    delphi 中用快捷键 草 实在是 太简单了 . 自己摸索出来的 方法 --------------------------------------------------------------- ...

  7. .NET Core开发实战(第21课:中间件:掌控请求处理过程的关键)--学习笔记(下)

    21 | 中间件:掌控请求处理过程的关键 如果在 Map 的时候逻辑复杂一点,不仅仅判断它的 URL 地址,而且要做特殊的判断的话,可以这么做把判断逻辑变成一个委托 我们要判断当我们的请求地址包含 a ...

  8. .NET Avalonia开源、免费的桌面UI库 - SukiUI

    前言 今天分享一款.NET Avalonia基于MIT License协议开源.免费的桌面UI库:SukiUI. Avalonia介绍 Avalonia是一个强大的框架,使开发人员能够使用.NET创建 ...

  9. NC24370 [USACO 2012 Dec S]Milk Routing

    题目链接 题目 题目描述 Farmer John's farm has an outdated network of M pipes (1 <= M <= 500) for pumping ...

  10. NC14698 模拟战役

    题目链接 题目 题目描述 齐齐和司机在玩单机游戏<红色警戒IV>,现在他们的游戏地图被划分成一个n*m的方格地图.齐齐的基地在最上方的4行格内,司机的基地在最下方的4行格内.他们只有一种攻 ...