SpringCloud Zuul执行流程与源码分析

请求在网关内的执行流程

## 第一步DispatcherServlet
用户请求大致上是按这个顺序来进行的,我们再看具体内部这四种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内置的过滤器

我们先罗列一下所有的内置过滤器,有一个整体的印象
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"。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值