SpringCloud系列:源码解析Spring Cloud Zuul

举报
云享专家 发表于 2019/08/30 15:57:07 2019/08/30
【摘要】 我需要将响应结果,也要存储在log中,在之前已经分析了,在route结束后,将从具体服务获取的响应流存储在RequestContext中,在SendResponseFilter过滤器写入在HttpServletResponse中,最终返回给客户端。那么我只需要在SendResponseFilter写入响应流之前把响应流写入到log日志中即可,那么会引发另外一个问题

Zuul 架构图

1.png

 

 

zuul中,整个请求的过程是这样的,首先将请求给zuulservlet处理,zuulservlet中有一个zuulRunner对象,该对象中初始化了RequestContext:作为存储整个请求的一些数据,并被所有的zuulfilter共享。zuulRunner中还有FilterProcessorFilterProcessor作为执行所有的zuulfilter的管理器。FilterProcessorfilterloader 中获取zuulfilter,而zuulfilter是被filterFileManager所加载,并支持groovy热加载,采用了轮询的方式热加载。有了这些filter之后,zuulservelet首先执行的Pre类型的过滤器,再执行route类型的过滤器,最后执行的是post 类型的过滤器,如果在执行这些过滤器有错误的时候则会执行error类型的过滤器。执行完这些过滤器,最终将请求的结果返回给客户端。



 

zuul工作原理源码分析

在之前已经讲过,如何使用zuul,其中不可缺少的一个步骤就是在程序的启动类加上@EnableZuulProxy,该EnableZuulProxy类代码如下:

2.png

 

其中,引用了ZuulProxyConfiguration,跟踪ZuulProxyConfiguration,该类注入了DiscoveryClientRibbonCommandFactoryConfiguration用作负载均衡相关的。注入了一些列的filters,比如PreDecorationFilterRibbonRoutingFilterSimpleHostRoutingFilter,代码如如下:

3.png

 

它的父类ZuulConfiguration,引用了一些相关的配置。在缺失zuulServlet bean的情况下注入了ZuulServlet,该类是zuul的核心类。

4.png

 

同时也注入了其他的过滤器,比如ServletDetectionFilterDebugFilterServlet30WrapperFilter,这些过滤器都是pre类型的。

5.png

 

它也注入了post类型的,比如SendResponseFiltererror类型,比如SendErrorFilterroute类型比如SendForwardFilter,代码如下:

6.png

 

初始化ZuulFilterInitializer类,将所有的filter FilterRegistry注册。

7.png

 

FilterRegistry管理了一个ConcurrentHashMap,用作存储过滤器的,并有一些基本的CURD过滤器的方法,代码如下:

8.png

 

FilterLoader类持有FilterRegistryFilterFileManager类持有FilterLoader,所以最终是由FilterFileManager注入filterFilterRegistryConcurrentHashMap的。FilterFileManager到开启了轮询机制,定时的去加载过滤器,代码如下:

 

9.png

 

Zuulservlet作为类似于Spring MVC中的DispatchServlet,起到了前端控制器的作用,所有的请求都由它接管。它的核心代码如下:

10.png

 

跟踪init(),可以发现这个方法为每个请求生成了RequestContext,RequestContext继承了ConcurrentHashMap<String, Object>,在请求结束时销毁掉该RequestContextRequestContext的生命周期为请求到zuulServlet开始处理,直到请求结束返回结果。RequestContext类在存储了很多重要的信息,包括HttpServletRequestHttpServletResponsResponseDataStreamResponseStatusCode等。RequestContext对象在处理请求的过程中,一直存在,所以这个对象为所有Filter共享。

ZuulServletservice()方法可知,它是先处理pre()类型的处理器,然后在处理route()类型的处理器,最后再处理post类型的处理器。

首先来看一看pre()的处理过程,它会进入到ZuulRunner,该类的作用是将请求的HttpServletRequestHttpServletRespons放在RequestContext类中,并包装了一个FilterProcessor,代码如下:

spacer.gif11.png

 

FilterProcessor类为调用filters的类,比如调用pre类型所有的过滤器:

12.png

 

跟踪runFilters()方法,可以发现,它最终调用了FilterLoadergetFiltersByType(sType)方法来获取同一类的过滤器,然后用for循环遍历所有的ZuulFilter,执行了processZuulFilter()方法,跟踪该方法可以发现最终是执行了ZuulFilter的方法,最终返回了该方法返回的Object对象。

13.png

 

routepost类型的过滤器的执行过程和pre执行过程类似。

Zuul默认过滤器

默认的核心过滤器一览表

Zuul默认注入的过滤器,它们的执行顺序在FilterConstants类,我们可以先定位在这个类,然后再看这个类的过滤器的执行顺序以及相关的注释,可以很轻松定位到相关的过滤器,也可以直接打开spring-cloud-netflix-core.jarzuul.filters包,可以看到一些列的filter,现在我以表格的形式,列出默认注入的filter.

过滤器

order

描述

类型

ServletDetectionFilter

-3

检测请求是用DispatcherServlet还是ZuulServlet

pre

Servlet30WrapperFilter

-2

Servlet 3.0 下,包装requests

pre

FormBodyWrapperFilter

-1

解析表单数据

pre

SendErrorFilter

0

如果中途出现错误

error

DebugFilter

1

设置请求过程是否开启debug

pre

PreDecorationFilter

5

根据uri决定调用哪一个route过滤器

pre

RibbonRoutingFilter

10

如果写配置的时候用ServiceId则用这个route过滤器,该过滤器可以用Ribbon做负载均衡,用hystrix做熔断

route

SimpleHostRoutingFilter

100

如果写配置的时候用url则用这个route过滤

route

SendForwardFilter

500

RequestDispatcher请求转发

route

SendResponseFilter

1000

RequestDispatcher请求转发

post

过滤器的order值越小,就越先执行,并且在执行过滤器的过程中,它们共享了一个RequestContext对象,该对象的生命周期贯穿于请求,可以看出优先执行了pre类型的过滤器,并将执行后的结果放在RequestContext中,供后续的filter使用,比如在执行PreDecorationFilter的时候,决定使用哪一个route,它的结果的是放在RequestContext对象中,后续会执行所有的route的过滤器,如果不满足条件就不执行该过

滤器的run方法。最终达到了就执行一个route过滤器的run()方法。

error类型的过滤器,是在程序发生异常的时候执行的。

post类型的过滤,在默认的情况下,只注入了SendResponseFilter,该类型的过滤器是将最终的请求结果以流的形式输出给客户单。

现在来看SimpleHostRoutingFilter是如何工作?

进入到SimpleHostRoutingFilter类的方法的run()方法,核心代码如下:

14.png

 

查阅这个类的全部代码可知,该类创建了一个HttpClient作为请求类,并重构了url,请求到了具体的服务,得到的一个CloseableHttpResponse对象,并将CloseableHttpResponse对象的保存到RequestContext对象中。并调用了ProxyRequestHelpersetResponse方法,将请求状态码,流等信息保存在RequestContext对象中。

15.png

 

现在来看SendResponseFilter是如何工作?

这个过滤器的order1000,在默认且正常的情况下,是最后一个执行的过滤器,该过滤器是最终将得到的数据返回给客户端的请求。

在它的run()方法里,有两个方法:addResponseHeaders()writeResponse(),即添加响应头和写入响应数据流。

16.png

 

其中writeResponse()方法是通过从RequestContext中获取ResponseBody获或者ResponseDataStream来写入到HttpServletResponse中的,但是在默认的情况下ResponseBodynull,而ResponseDataStreamroute类型过滤器中已经设置进去了。具体代码如下:

17.png

 

如何在zuul上做日志处理

由于zuul作为api网关,所有的请求都经过这里,所以在网关上,可以做请求相关的日志处理。我的需求是这样的,需要记录请求的url,ip地址,参数,请求发生的时间,整个请求的耗时,请求的响应状态,甚至请求响应的结果等。很显然,需要实现这样的一个功能,需要写一个ZuulFliter,它应该是在请求发送给客户端之前做处理,并且在route过滤器路由之后,在默认的情况下,这个过滤器的order应该为500-1000之间。那么如何获取这些我需要的日志信息呢?找RequestContext,在请求的生命周期里这个对象里,存储了整个请求的所有信息。

现在编码,在代码的注释中,做了详细的说明,代码如下:

18.png

 

现在读者也许有疑问,如何得到的statrtTime,即请求开始的时间,其实这需要另外一个过滤器,在网络请求route之前(大部分耗时都在route这一步),在过滤器中,在RequestContext存储一个时间即可,另写一个过滤器,代码如下:

19.png

 

可能还有这样的需求,我需要将响应结果,也要存储在log中,在之前已经分析了,在route结束后,将从具体服务获取的响应流存储在RequestContext中,在SendResponseFilter过滤器写入在HttpServletResponse中,最终返回给客户端。那么我只需要在SendResponseFilter写入响应流之前把响应流写入到log日志中即可,那么会引发另外一个问题,因为响应流写入到log后,RequestContext就没有响应流了,在SendResponseFilter就没有流输入到HttpServletResponse中,导致客户端没有任何的返回数据,那么解决的办法是这样的:

InputStream inputStream =RequestContext.getCurrentContext().getResponseDataStream();

InputStream newInputStream= copy(inputStream);

transerferTolog(inputStream);RequestContext.getCurrentContext().setResponseDataStream(newInputStream);

RequestContext获取到流之后,首先将流copy一份,将流转化下字符串,存在日志中,再setRequestContext中,这样SendResponseFilter就可以将响应返回给客户端。这样的做法有点影响性能,如果不是字符流,可能需要做更多的处理工作。

 

原创作者:方志朋

方志朋简介:SpringCloud中国社区联合创始人,博客访问量突破一千万,爱好开源,热爱分享,活跃于各大社区,保持着非常强的学习驱动力,终身学习践行者,终身学习受益者。目前就职于国内某家知名互联网保险公司,担任DEVOPS工程师,对微服务领域和持续集成领域研究较深,精通微服务框架SpringCloud

 

 


【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。