请求在网关内的执行流程
用户请求大致上是按这个顺序来进行的,我们再看具体内部这四种Filter的执行顺序,搬一段ZuulServlet.service()源码:
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
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
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();
}
}
从这里我们可以看到,许多其他博客总结的四种执行器执行流程:
无报错情况:pre -> route -> post
有报错情况:error -> post
是没有问题的,从源码上来看确实是这样的。
过滤器的具体调用逻辑
为了测试我自己申明了三个过滤器,两个pre类型,order分别是0和1;一个error类型,order为-1
ZuulRunner的preRoute()、route()、postRoute()、error()调用的都是FilterProcessor对应的同名方法,FilterProcessor中的逻辑就调用的runFilters(),传入不同的FilterType来执行不同的过滤器。我们看一下其具体实现:
public Object runFilters(String sType) throws Throwable {
if (RequestContext.getCurrentContext().debugRouting()) {
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
boolean bResult = false;
// 通过传入的Filter类型,获取到要执行的过滤器列表
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;
}
通过上面的代码,我们看到一种类型过滤器的执行大约分为两步:
1.获取指定类型的所有ZuulFilter
2.循环执行每一个过滤器
那我们也要从这两方面去思考:
1.过滤器是如何加载进FilterLoader的(这个问题下面专门会讲)
2.Zuul预置了哪些过滤器,它们分别有什么用,执行顺序是怎样的
FilterProcessor.processZuulFilter()方法体因为篇幅我就不罗列了,感兴趣的话可以去看一下,我这里只结合自己的理解,总结了它的功能:
1.调用ZuulFilter的runFilter()方法获取到ZuulFilterResult对象
2.对ZuulFilterResult进行处理,统计单个ZuulFilter的执行时间,调用RequestContext.addFilterExecutionSummary()记录日志。
然后我们重点看一下ZuulFilter是如何执行的:
public ZuulFilterResult runFilter() {
ZuulFilterResult zr = new ZuulFilterResult();
// 重点在这里,要经过isFilterDisabled()和shouldFilter()两个方法,才会判定这个Filter可以执行
// 第一个方法是判断Filter是否被禁用,第二个方法是判断业务逻辑这个过滤器是否执行
if (!isFilterDisabled()) {
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;
}
重点:禁用Zuul预置过滤器的方法在于isFilterDisabled()
public boolean isFilterDisabled() {
filterDisabledRef.compareAndSet(null, DynamicPropertyFactory.getInstance().getBooleanProperty(disablePropertyName(), false));
return filterDisabledRef.get().get();
}
public String disablePropertyName() {
return "zuul." + this.getClass().getSimpleName() + "." + filterType() + ".disable";
}
// 例如:禁用预置的SendErrorFilter的配置
// zuul.SendErrorFilter.error.disable=true
Zuul内置的过滤器
我们先罗列一下所有的内置过滤器,有一个整体的印象
内置的pre过滤器
ServletDetectionFilter
它的执行顺序为-3,是最先被执行的过滤器。该过滤器总是会被执行,主要用来检测当前请求是通过Spring的DispatcherServlet处理运行,还是通过ZuulServlet来处理运行的。然后将判断结果生成一个key存放在RequestContext 中。
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;
}
// request中的DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE这个key
// 是在DispatcherServlet中的doService()方法被添加上的,如果感兴趣可以去看一下
private boolean isDispatcherServletRequest(HttpServletRequest request) {
return request.getAttribute(DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null;
}
维护在RequestContext 中的这个boolean值只在RequestUtils.isDispatcherServletRequest()方法调用,用来返回当前请求是否是有一个来自DispatcherServletRequest的请求,后续这个工具方法会被频繁使用。
public static boolean isDispatcherServletRequest() {
return RequestContext.getCurrentContext().getBoolean(IS_DISPATCHER_SERVLET_REQUEST_KEY);
}
Servlet30WrapperFilter
它的执行顺序为-2,是第二个执行的过滤器。目前的实现会对所有请求生效,主要为了将原始的HttpServletRequest包装成Servlet30RequestWrapper对象。
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 条件不成立,感兴趣的小伙伴可以看一下ZuulServlet的service方法中第一行的init方法是如何委托zuulRunner处理request和response 的
// Tips:"buffer-requests"这个key是在ZuulServerAutoConfiguration创建zuulServlet设置的默认false
if (request instanceof HttpServletRequestWrapper) {
request = (HttpServletRequest) ReflectionUtils.getField(this.requestField,
request);
ctx.setRequest(new Servlet30RequestWrapper(request));
}
// 条件成立
else if (RequestUtils.isDispatcherServletRequest()) {
// If it's going through the dispatcher we need to buffer the body
ctx.setRequest(new Servlet30RequestWrapper(request));
}
return null;
}
FormBodyWrapperFilter
它的执行顺序为-1,是第三个执行的过滤器。该过滤器仅对两种类请求生效,第一类是Content-Type为application/x-www-form-urlencoded的请求,第二类是Content-Type为multipart/form-data并且是由Spring的DispatcherServlet处理的请求(用到了ServletDetectionFilter的处理结果)。而该过滤器的主要目的是将符合要求的请求体包装成FormBodyRequestWrapper对象。
DebugFilter
它的执行顺序为1,是第四个执行的过滤器。该过滤器会根据配置参数zuul.debug.request和请求中的debug参数来决定是否执行过滤器中的操作。而它的具体操作内容则是将当前的请求上下文中的debugRouting和debugRequest参数设置为true。由于在同一个请求的不同生命周期中,都可以访问到这两个值,所以我们在后续的各个过滤器中可以利用这两值来定义一些debug信息,这样当线上环境出现问题的时候,可以通过请求参数的方式来激活这些debug信息以帮助分析问题。另外,对于请求参数中的debug参数,我们也可以通过zuul.debug.parameter来进行自定义。
public boolean shouldFilter() {
HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
// 判断请求的是否带了开启debug的标志,默认是一个key为"debug"的parameter
// 通过zuul.debug.parameter这个配置可以自定义请求中的key
if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) {
return true;
}
// 判断zuul启动时是否配置了开启debug
// 通过zuul.debug.request这个配置可以开启
return ROUTING_DEBUG.get();
}
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.setDebugRouting(true);
ctx.setDebugRequest(true);
return null;
}
我在具体实践和阅读源码之后,我发现如果开启了bubug模式,Zuul会在FilterProcessor调用具体Filter执行获得执行果之后进行RoutingDebug,并没有RequestDebug,因此如果需要,还需自己实现。
PreDecorationFilter
它的执行顺序为5,是pre阶段最后被执行的过滤器。该过滤器会判断当前请求上下文中是否存在forward.to和serviceId参数,如果都不存在,那么它就会执行具体过滤器的操作(如果有一个存在的话,说明当前请求已经被处理过了,因为这两个信息就是根据当前请求的路由信息加载进来的)。
我们根据其run()方法的源码分析其具体的执行逻辑:
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
final String requestURI = this.urlPathHelper.getPathWithinApplication(ctx.getRequest());
Route route = this.routeLocator.getMatchingRoute(requestURI);
if (route != null) {
// location是配置的serviceId,或者一个跳转的url
String location = route.getLocation();
if (location != null) {
ctx.put(REQUEST_URI_KEY, route.getPath());
ctx.put(PROXY_KEY, route.getId());
// 处理想附带的敏感请求头,通过这里我们发现两种配置请求头的方式:
// 1.对所有应用进行统一过滤
// zuul.customSensitiveHeaders 是否开启自定义敏感请求头,默认false
// zuul.sensitiveHeaders 配置向下传递的请求头
// 2.对跳转的单个应用可以进行分别定义
if (!route.isCustomSensitiveHeaders()) {
this.proxyRequestHelper
.addIgnoredHeaders(this.properties.getSensitiveHeaders().toArray(new String[0]));
}
else {
this.proxyRequestHelper.addIgnoredHeaders(route.getSensitiveHeaders().toArray(new String[0]));
}
// 设置这个Route是否可以重试
if (route.getRetryable() != null) {
ctx.put(RETRYABLE_KEY, route.getRetryable());
}
// 如果以http:或https:开头,那说明跳转配置的是一个url,增加一个key为"X-Zuul-Service"的Header
if (location.startsWith(HTTP_SCHEME+":") || location.startsWith(HTTPS_SCHEME+":")) {
ctx.setRouteHost(getUrl(location));
ctx.addOriginResponseHeader(SERVICE_HEADER, location);
}
// 如果以"forward:"开头,说明是要重定向到具体的uri
else if (location.startsWith(FORWARD_LOCATION_PREFIX)) {
ctx.set(FORWARD_TO_KEY,
StringUtils.cleanPath(location.substring(FORWARD_LOCATION_PREFIX.length()) + route.getPath()));
ctx.setRouteHost(null);
return null;
}
else {
// 如果都不是,那就认为是用户配置的serviceId,增加一个key为"X-Zuul-ServiceId"的Header
ctx.set(SERVICE_ID_KEY, location);
ctx.setRouteHost(null);
ctx.addOriginResponseHeader(SERVICE_ID_HEADER, location);
}
// 是否增加代理请求头 使用zuul.addProxyHeaders参数配置,默认为true
// 封装了请求地址的host、port、协议、route前缀等信息
if (this.properties.isAddProxyHeaders()) {
addProxyHeaders(ctx, route);
String xforwardedfor = ctx.getRequest().getHeader(X_FORWARDED_FOR_HEADER);
String remoteAddr = ctx.getRequest().getRemoteAddr();
if (xforwardedfor == null) {
xforwardedfor = remoteAddr;
}
// 封装请求时发出的ip
else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
xforwardedfor += ", " + remoteAddr;
}
ctx.addZuulRequestHeader(X_FORWARDED_FOR_HEADER, xforwardedfor);
}
// 是否封装host信息 使用zuul.addHostHeaders参数配置,默认为false
if (this.properties.isAddHostHeader()) {
ctx.addZuulRequestHeader(HttpHeaders.HOST, toHostHeader(ctx.getRequest()));
}
}
}
// 如果请求url没有匹配到配置的路由跳转,执行下面的逻辑
else {
log.warn("No route found for uri: " + requestURI);
String fallBackUri = requestURI;
String fallbackPrefix = this.dispatcherServletPath; // default fallback
// servlet is
// DispatcherServlet
// 判断这次请求是否是一个来自ZuulServlet的请求,决定是否转发到ZuulController
if (RequestUtils.isZuulServletRequest()) {
// 替换掉zuul请求路径标志/zuul,这个标志可以通过zuul.servletPath参数进行配置
log.debug("zuulServletPath=" + this.properties.getServletPath());
fallBackUri = fallBackUri.replaceFirst(this.properties.getServletPath(), "");
log.debug("Replaced Zuul servlet path:" + fallBackUri);
}
else {
// 如果是一个经过DispatcherServlet的请求,替换dispatcherServletPath
// dispatcherServletPath默认值是 “/”,可以通过server.path参数进行配置
log.debug("dispatcherServletPath=" + this.dispatcherServletPath);
fallBackUri = fallBackUri.replaceFirst(this.dispatcherServletPath, "");
log.debug("Replaced DispatcherServlet servlet path:" + fallBackUri);
}
if (!fallBackUri.startsWith("/")) {
fallBackUri = "/" + fallBackUri;
}
String forwardURI = fallbackPrefix + fallBackUri;
forwardURI = forwardURI.replaceAll("//", "/");
// 设置重定向标志
ctx.set(FORWARD_TO_KEY, forwardURI);
}
return null;
}
内置的Route过滤器
RibbonRoutingFilter
它的执行顺序为10,是route阶段第一个执行的过滤器。该过滤器只对请求上下文中存在serviceId参数的请求进行处理,即只对通过serviceId配置路由规则的请求生效。而该过滤器的执行逻辑就是面向服务路由的核心,它通过使用Ribbon和Hystrix来向服务实例发起请求,并将服务实例的请求结果返回。
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// 这两个值是在PreDecorationFilter逻辑中处理的
return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null
&& ctx.sendZuulResponse());
}
第一步:构建RibbonCommandContext
protected RibbonCommandContext buildCommandContext(RequestContext context) {
HttpServletRequest request = context.getRequest();
// 1.遍历请求头,循环判断请求是否有在zuul.ignoredHeaders中配置,如果没配置,则加入到向业务系统分发的请求中
// 2.将X-Forwarded-*的请求头加入到向业务系统分发的请求中
// 3.请求头加上Accept-Encoding=gzip
MultiValueMap<String, String> headers = this.helper
.buildZuulRequestHeaders(request);
// 将URL里的参数进行截取和Decode, 封装进MultiValueMap
MultiValueMap<String, String> params = this.helper
.buildZuulRequestQueryParams(request);
String verb = getVerb(request);
InputStream requestEntity = getRequestBody(request);
if (request.getContentLength() < 0 && !verb.equalsIgnoreCase("GET")) {
context.setChunkedRequestBody();
}
String serviceId = (String) context.get(SERVICE_ID_KEY);
Boolean retryable = (Boolean) context.get(RETRYABLE_KEY);
Object loadBalancerKey = context.get(LOAD_BALANCER_KEY);
// 如果配置文件Route中的path不为空, 进行请求uri的组装
String uri = this.helper.buildZuulRequestURI(request);
// remove double slashes
uri = uri.replace("//", "/");
long contentLength = useServlet31 ? request.getContentLengthLong(): request.getContentLength();
return new RibbonCommandContext(serviceId, verb, uri, retryable, headers, params,
requestEntity, this.requestCustomizers, contentLength, loadBalancerKey);
}
第二步:构建RibbonCommand并执行
第三步:对业务系统的返回信息封装进RequestContext
public void setResponse(int status, InputStream entity,
MultiValueMap<String, String> headers) throws IOException {
RequestContext context = RequestContext.getCurrentContext();
context.setResponseStatusCode(status);
if (entity != null) {
context.setResponseDataStream(entity);
}
boolean isOriginResponseGzipped = false;
for (Entry<String, List<String>> header : headers.entrySet()) {
String name = header.getKey();
for (String value : header.getValue()) {
context.addOriginResponseHeader(name, value);
// 如果header是Content-Encoding, 判断是否是gzip响应
if (name.equalsIgnoreCase(HttpHeaders.CONTENT_ENCODING)
&& HTTPRequestUtils.getInstance().isGzipped(value)) {
isOriginResponseGzipped = true;
}
// 如果header是Content-Length, 设置响应内容大小
if (name.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH)) {
context.setOriginContentLength(value);
}
// 判断是否配置zuul.ignoredHeaders过滤响应头
if (isIncludedHeader(name)) {
context.addZuulResponseHeader(name, value);
}
}
}
// 在上下文中设置是否是gzip响应的标志
context.setResponseGZipped(isOriginResponseGzipped);
}
SimpleHostRoutingFilter
它的执行顺序为100,是route阶段第二个执行的过滤器。该过滤器只对请求上下文中存在routeHost参数的请求进行处理,即只对通过url配置路由规则的请求生效。而该过滤器的执行逻辑就是直接向routeHost参数的物理地址发起请求,从源码中我们可以知道该请求是直接通过httpclient包实现的,而没有使用Hystrix命令进行包装,所以这类请求并没有线程隔离和断路器的保护。
// RequestContext中的routeHost是在PreDecorationFilter中设置的(如果是转发一个具体Url的话)
// RequestContext.getCurrentContext().sendZuulResponse()默认返回true,代表是否将响应内容发给客户端
public boolean shouldFilter() {
return RequestContext.getCurrentContext().getRouteHost() != null
&& RequestContext.getCurrentContext().sendZuulResponse();
}
SendForwardFilter
它的执行顺序为500,是route阶段第三个执行的过滤器。该过滤器只对请求上下文中存在forward.to参数的请求进行处理,即用来处理路由规则中的forward本地跳转配置。
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
return ctx.containsKey(FORWARD_TO_KEY)
&& !ctx.getBoolean(SEND_FORWARD_FILTER_RAN, false);
}
内置的Post过滤器
SendResponseFilter
zuul默认了一个Post过滤器来处理最终返回给前端的响应,其执行顺序为1000。其执行条件需要满足以下两点:
1.上下文RequestContext中的throwable为null。
2.ResponseHeader、ResponseBody、ResponseDataStream任意一个不为null。
第一步:添加响应头
private void addResponseHeaders() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletResponse servletResponse = context.getResponse();
// 判断配置文件是否配置zuul.includeDebugHeader为true(默认false)
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());
}
}
// 添加routing filter里面在RequestContext封装的请求头
List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders();
if (zuulResponseHeaders != null) {
for (Pair<String, String> it : zuulResponseHeaders) {
servletResponse.addHeader(it.first(), it.second());
}
}
// 1.判断配置文件zuul.setContentLength是否为true(默认false)
// 2.判断请求上下文中是否有设置ContentLength和gzip标志
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());
}
}
}
}
第二步:写入响应体
1.如果CharacterEncoding没有设置则默认为"UTF-8"。
2.如果响应是gzip的,设置ContentEncoding为给"gzip"。