Zuul 源码分析

zuul是spring cloud 微服务体系中的网关,可以路由请求到具体的服务,同时做一些验签,解密等的与业务无关的事情。今天我们从一个注解@EnableZuulProxy开始讲述。这个注解导入了一个配置文件ZuulProxyConfiguration,他继承了ZuulConfiguration,整个zuul的流程的定义就在这两个类中。

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)
// Make sure to get the ServerProperties from the same place as a normal web app would
@Import(ServerPropertiesAutoConfiguration.class)
public class ZuulConfiguration {

   @Autowired
   protected ZuulProperties zuulProperties;

   @Autowired
   protected ServerProperties server;

   @Autowired(required = false)
   private ErrorController errorController;

 

   @Bean
   public ZuulController zuulController() {
      return new ZuulController();
   }

   @Bean
   public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
      ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
      mapping.setErrorController(this.errorController);
      return mapping;
   }

   @Bean
   @ConditionalOnMissingBean(name = "zuulServlet")
   public ServletRegistrationBean zuulServlet() {
      ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(),
            this.zuulProperties.getServletPattern());
      // The whole point of exposing this servlet is to provide a route that doesn't
      // buffer requests.
      servlet.addInitParameter("buffer-requests", "false");
      return servlet;
   }

   // pre filters

   @Bean
   public ServletDetectionFilter servletDetectionFilter() {
      return new ServletDetectionFilter();
   }

   @Bean
   public FormBodyWrapperFilter formBodyWrapperFilter() {
      return new FormBodyWrapperFilter();
   }

   @Bean
   public DebugFilter debugFilter() {
      return new DebugFilter();
   }

   @Bean
   public Servlet30WrapperFilter servlet30WrapperFilter() {
      return new Servlet30WrapperFilter();
   }

   // post filters

   @Bean
   public SendResponseFilter sendResponseFilter() {
      return new SendResponseFilter();
   }

   @Bean
   public SendErrorFilter sendErrorFilter() {
      return new SendErrorFilter();
   }

   @Bean
   public SendForwardFilter sendForwardFilter() {
      return new SendForwardFilter();
   }
 
   ...
}

zuulProperties 是用户通过配置文件编写的服务路由等信息,表明请求是哪种模式时需要跳转到哪个具体的服务,这是主要的内容,当然还有一些其他的信息。具体的看一下这个类的内容就可以了,都可以配置,这是我们定制化zuul的一个基础配置文件。剩下的zuulController,zuulHandlerMapping,zuulServlet就是spring mvc 的组件了,通过zuulHandlerMapping可以发现所有的请求都交给了zuulController来处理,它里面包装的Servlet就是zuulServlet,由他的service方法来处理。这个下面细说。例外还配置了ZuulFilter的过滤器,着重看一下pre类型的servletDetectionFilter与post类型的sendResponseFilter。

@Configuration
public class ZuulProxyConfiguration extends ZuulConfiguration {

   @Autowired(required = false)
   private TraceRepository traces;

   @Autowired
   private SpringClientFactory clientFactory;

   @Autowired
   private DiscoveryClient discovery;

   @Autowired
   private ServiceRouteMapper serviceRouteMapper;

   @Bean
   @Override
   @ConditionalOnMissingBean(RouteLocator.class)
   public DiscoveryClientRouteLocator routeLocator() {
      return new DiscoveryClientRouteLocator(this.server.getServletPrefix(),
            this.discovery, this.zuulProperties, this.serviceRouteMapper);
   }

   @Bean
   @ConditionalOnMissingBean
   public RibbonCommandFactory ribbonCommandFactory() {
      return new RestClientRibbonCommandFactory(this.clientFactory);
   }

   // pre filters
   @Bean
   public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator,
         ProxyRequestHelper proxyRequestHelper) {
      return new PreDecorationFilter(routeLocator,
            this.server.getServletPrefix(),
            this.zuulProperties,
            proxyRequestHelper);
   }

   // route filter
   @Bean
   public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper,
         RibbonCommandFactory ribbonCommandFactory) {
      RibbonRoutingFilter filter = new RibbonRoutingFilter(helper,
            ribbonCommandFactory);
      return filter;
   }

   @Bean
   public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper,
         ZuulProperties zuulProperties) {
      return new SimpleHostRoutingFilter(helper, zuulProperties);
   }

   @Bean
   public ProxyRequestHelper proxyRequestHelper() {     //作为一个帮助类,主要用来设置值
      ProxyRequestHelper helper = new ProxyRequestHelper();
      if (this.traces != null) {
         helper.setTraces(this.traces);
      }
      helper.setIgnoredHeaders(this.zuulProperties.getIgnoredHeaders());
      helper.setTraceRequestBody(this.zuulProperties.isTraceRequestBody());
      return helper;
   }

   @Bean
   @ConditionalOnMissingBean(ServiceRouteMapper.class)
   public ServiceRouteMapper serviceRouteMapper() {
      return new SimpleServiceRouteMapper();
   }

   @Configuration
   @ConditionalOnClass(Endpoint.class)
   protected static class RoutesEndpointConfiguration {

      @Bean
      public RoutesEndpoint zuulEndpoint(RouteLocator routeLocator) {
         return new RoutesEndpoint(routeLocator);
      }
   }
}

routeLocator是一个路由的匹配器,他的实现是一个
DiscoveryClientRouteLocator,我这边使用的是Consul作为服务注册的容器,他会从consul中拉取各个服务的信息,比如我们再zuul中配置了要路由的serviceId,那么从consul中寻找,如果有对应的serviceId,那么就是这个服务来处理这个请求。proxyRequestHelper是一个帮助类,用来再context中set值。还有两个route类型zuulFilter,虽然都是Bean,但不一定都启用。simpleHostRoutingFilter再设置了url的情况下使用,ribbonRoutingFilter在设置了serviceId的情况下使用,有客户端负载均衡的作用。还有一个pre类型的zuulFilter
preDecorationFilter,主要是填充一些数据。OK,准备工作做的差不多了,我们来详细看一下流程。请求过来了,交给了ZuulServlet进行处理。

public class ZuulServlet extends HttpServlet {

    private static final long serialVersionUID = -3374242278843351500L;
    private ZuulRunner zuulRunner;


    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);

        String bufferReqsStr = config.getInitParameter("buffer-requests");
        boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true") ? true : false;

        zuulRunner = new ZuulRunner(bufferReqs);
    }

    @Override
    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();
        }
    }

    /**
     * executes "post" ZuulFilters
     *
     * @throws ZuulException
     */
    void postRoute() throws ZuulException {
        zuulRunner.postRoute();
    }

    /**
     * executes "route" filters
     *
     * @throws ZuulException
     */
    void route() throws ZuulException {
        zuulRunner.route();
    }

    /**
     * executes "pre" filters
     *
     * @throws ZuulException
     */
    void preRoute() throws ZuulException {
        zuulRunner.preRoute();
    }

    /**
     * initializes request
     *
     * @param servletRequest
     * @param servletResponse
     */
    void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
        zuulRunner.init(servletRequest, servletResponse);
    }

    /**
     * sets error context info and executes "error" filters
     *
     * @param e
     */
    void error(ZuulException e) {
        RequestContext.getCurrentContext().setThrowable(e);
        zuulRunner.error();
    }
}

在这个servlet初始化的时候会调用init方法,里面有个属性
buffer-requests用来判断是否走spring mvc。不知道大家有没有遇到过这种情况,上传文件走网关时,需要在链接上加上/zuul,加上就不需要走spring mvc ,这样不会对数据的大小有限制,所以可以传递大数据量的文件。另外初始化了一个zuulRunner,他所作的事情
第一点是在RequestContext设置请求按照buffer-requests这个参数来进行,如果为真,request被HttpServletRequestWrapper包装一下 ,第二点的作用是处理按照类型处理filter.。

/**
 * executes "route" filterType  ZuulFilters
 *
 * @throws ZuulException
 */
public void route() throws ZuulException {
    FilterProcessor.getInstance().route();
}
 
/**
 * 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 (Throwable e) {
        if (e instanceof ZuulException) {
            throw (ZuulException) 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 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;
}

当调用zuulRunner中的route方法时,就会搜索这个类型的zuulFilter,依次执行。当我们知道了这些,在回头看一下zuulServlet就特别容易理解了,就是根据流程走的先pre,再route,再post,再一直走这些filter啊。流程大抵知道了。我们再看一下,数据真实来了,怎么流转。我们就从pre(PreDecorationFilter),route(SimpleHostRoutingFilter),post(SendResponseFilter) 这三个部分来看一下。
pre: PreDecorationFilter

public class PreDecorationFilter extends ZuulFilter {

   private RouteLocator routeLocator;

   private String dispatcherServletPath;

   private ZuulProperties properties;

   private UrlPathHelper urlPathHelper = new UrlPathHelper();

   private ProxyRequestHelper proxyRequestHelper;

   public PreDecorationFilter(RouteLocator routeLocator, String dispatcherServletPath,
         ZuulProperties properties, ProxyRequestHelper proxyRequestHelper) {
      this.routeLocator = routeLocator;
      this.properties = properties;
      this.urlPathHelper
            .setRemoveSemicolonContent(properties.isRemoveSemicolonContent());
      this.dispatcherServletPath = dispatcherServletPath;
      this.proxyRequestHelper = proxyRequestHelper;
   }

   ...

   @Override
   public Object run() {
      RequestContext ctx = RequestContext.getCurrentContext();
      final String requestURI = this.urlPathHelper
            .getPathWithinApplication(ctx.getRequest());
      Route route = this.routeLocator.getMatchingRoute(requestURI);
      if (route != null) {
         String location = route.getLocation();
         if (location != null) {
            ctx.put("requestURI", route.getPath());
            ctx.put("proxy", route.getId());
            if (!route.isCustomSensitiveHeaders()) {
               this.proxyRequestHelper.addIgnoredHeaders(
                     this.properties.getSensitiveHeaders().toArray(new String[0]));
            }
            else {
               this.proxyRequestHelper.addIgnoredHeaders(
                     route.getSensitiveHeaders().toArray(new String[0]));
            }

            if (route.getRetryable() != null) {
               ctx.put("retryable", route.getRetryable());
            }

            if (location.startsWith("http:") || location.startsWith("https:")) {
               ctx.setRouteHost(getUrl(location));
               ctx.addOriginResponseHeader("X-Zuul-Service", location);
            }
            else if (location.startsWith("forward:")) {
               ctx.set("forward.to", StringUtils.cleanPath(
                     location.substring("forward:".length()) + route.getPath()));
               ctx.setRouteHost(null);
               return null;
            }
            else {
               // set serviceId for use in filters.route.RibbonRequest
               ctx.set("serviceId", location);
               ctx.setRouteHost(null);
               ctx.addOriginResponseHeader("X-Zuul-ServiceId", location);
            }
            if (this.properties.isAddProxyHeaders()) {
               ctx.addZuulRequestHeader("X-Forwarded-Host",
                     ctx.getRequest().getServerName());
               ctx.addZuulRequestHeader("X-Forwarded-Port",
                     String.valueOf(ctx.getRequest().getServerPort()));
               ctx.addZuulRequestHeader(ZuulHeaders.X_FORWARDED_PROTO,
                     ctx.getRequest().getScheme());
               if (StringUtils.hasText(route.getPrefix())) {
                  String existingPrefix = ctx.getRequest()
                        .getHeader("X-Forwarded-Prefix");
                  StringBuilder newPrefixBuilder = new StringBuilder();
                  if (StringUtils.hasLength(existingPrefix)) {
                     if (existingPrefix.endsWith("/")
                           && route.getPrefix().startsWith("/")) {
                        newPrefixBuilder.append(existingPrefix, 0,
                              existingPrefix.length() - 1);
                     }
                     else {
                        newPrefixBuilder.append(existingPrefix);
                     }
                  }
                  newPrefixBuilder.append(route.getPrefix());
                  ctx.addZuulRequestHeader("X-Forwarded-Prefix",
                        newPrefixBuilder.toString());
               }
               String xforwardedfor = ctx.getRequest().getHeader("X-Forwarded-For");
               String remoteAddr = ctx.getRequest().getRemoteAddr();
               if (xforwardedfor == null) {
                  xforwardedfor = remoteAddr;
               }
               else if (!xforwardedfor.contains(remoteAddr)) { // Prevent duplicates
                  xforwardedfor += ", " + remoteAddr;
               }
               ctx.addZuulRequestHeader("X-Forwarded-For", xforwardedfor);
            }
         }
      }
      else {
         log.warn("No route found for uri: " + requestURI);

         String fallBackUri = requestURI;
         String fallbackPrefix = this.dispatcherServletPath; // default fallback
                                                // servlet is
                                                // DispatcherServlet

         if (RequestUtils.isZuulServletRequest()) {
            // remove the Zuul servletPath from the requestUri
            log.debug("zuulServletPath=" + this.properties.getServletPath());
            fallBackUri = fallBackUri.replaceFirst(this.properties.getServletPath(),
                  "");
            log.debug("Replaced Zuul servlet path:" + fallBackUri);
         }
         else {
            // remove the DispatcherServlet servletPath from the requestUri
            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", forwardURI);
      }
      return null;
   }

   private URL getUrl(String target) {
      try {
         return new URL(target);
      }
      catch (MalformedURLException ex) {
         throw new IllegalStateException("Target URL is malformed", ex);
      }
   }
}

这个pre就是在上下文中set一系列的值。
route: SimpleHostRoutingFilter

@Override
public Object run() {
   RequestContext context = RequestContext.getCurrentContext();
   HttpServletRequest request = context.getRequest();
   MultiValueMap headers = this.helper
         .buildZuulRequestHeaders(request);
   MultiValueMap params = this.helper
         .buildZuulRequestQueryParams(request);
   String verb = getVerb(request);
   InputStream requestEntity = getRequestBody(request);
   if (request.getContentLength() < 0) {
      context.setChunkedRequestBody();
   }

   String uri = this.helper.buildZuulRequestURI(request);
   this.helper.addIgnoredHeaders();

   try {
      HttpResponse response = forward(this.httpClient, verb, uri, request, headers,
            params, requestEntity);
      setResponse(response);
      setErrorCodeFor4xx(context, response);
   }
   catch (Exception ex) {
      context.set(ERROR_STATUS_CODE,
            HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
      context.set("error.exception", ex);
   }
   return null;
}
 
 
private void setResponse(HttpResponse response) throws IOException {
   this.helper.setResponse(response.getStatusLine().getStatusCode(),
         response.getEntity() == null ? null : response.getEntity().getContent(),
         revertHeaders(response.getAllHeaders()));
}

利用httpClient调用服务拿到数据,里面有个setReponse,就是借助helper,还记得我们有个Helper的Bean,不记得,请看上文,把response设置到了上下文中。
post :SendResponseFilter

@Override
public Object run() {
   try {
      addResponseHeaders();
      writeResponse();
   }
   catch (Exception ex) {
      ReflectionUtils.rethrowRuntimeException(ex);
   }
   return null;
}

private void writeResponse() throws Exception {
   RequestContext context = RequestContext.getCurrentContext();
   // there is no body to send
   if (context.getResponseBody() == null
         && context.getResponseDataStream() == null) {
      return;
   }
   HttpServletResponse servletResponse = context.getResponse();
   if (servletResponse.getCharacterEncoding() == null) { // only set if not set
      servletResponse.setCharacterEncoding("UTF-8");
   }
   OutputStream outStream = servletResponse.getOutputStream();
   InputStream is = null;
   try {
      if (RequestContext.getCurrentContext().getResponseBody() != null) {
         String body = RequestContext.getCurrentContext().getResponseBody();
         writeResponse(
               new ByteArrayInputStream(
                     body.getBytes(servletResponse.getCharacterEncoding())),
               outStream);
         return;
      }
      boolean isGzipRequested = false;
      final String requestEncoding = context.getRequest()
            .getHeader(ZuulHeaders.ACCEPT_ENCODING);

      if (requestEncoding != null
            && HTTPRequestUtils.getInstance().isGzipped(requestEncoding)) {
         isGzipRequested = true;
      }
      is = context.getResponseDataStream();
      InputStream inputStream = is;
      if (is != null) {
         if (context.sendZuulResponse()) {
            // if origin response is gzipped, and client has not requested gzip,
            // decompress stream
            // before sending to client
            // else, stream gzip directly to client
            if (context.getResponseGZipped() && !isGzipRequested) {
               // If origin tell it's GZipped but the content is ZERO bytes,
               // don't try to uncompress
               final Long len = context.getOriginContentLength();
               if (len == null || len > 0) {
                  try {
                     inputStream = new GZIPInputStream(is);
                  }
                  catch (java.util.zip.ZipException ex) {
                     log.debug(
                           "gzip expected but not "
                                 + "received assuming unencoded response "
                                 + RequestContext.getCurrentContext()
                                       .getRequest().getRequestURL()
                                       .toString());
                     inputStream = is;
                  }
               }
               else {
                  // Already done : inputStream = is;
               }
            }
            else if (context.getResponseGZipped() && isGzipRequested) {
               servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");
            }
            writeResponse(inputStream, outStream);
         }
      }
   }
   finally {
      try {
         if (is != null) {
            is.close();
         }
         outStream.flush();
         // The container will close the stream for us
      }
      catch (IOException ex) {
      }
   }
}

private void writeResponse(InputStream zin, OutputStream out) throws Exception {
   byte[] bytes = new byte[INITIAL_STREAM_BUFFER_SIZE.get()];
   int bytesRead = -1;
   while ((bytesRead = zin.read(bytes)) != -1) {
      try {
         out.write(bytes, 0, bytesRead);
         out.flush();
      }
      catch (IOException ex) {
         // ignore
      }
      // doubles buffer size if previous read filled it
      if (bytesRead == bytes.length) {
         bytes = new byte[bytes.length * 2];
      }
   }
}

private void addResponseHeaders() {
   RequestContext context = RequestContext.getCurrentContext();
   HttpServletResponse servletResponse = context.getResponse();
   List> zuulResponseHeaders = context.getZuulResponseHeaders();
   @SuppressWarnings("unchecked")
   List rd = (List) RequestContext.getCurrentContext()
         .get("routingDebug");
   if (rd != null) {
      StringBuilder debugHeader = new StringBuilder();
      for (String it : rd) {
         debugHeader.append("[[[" + it + "]]]");
      }
      if (INCLUDE_DEBUG_HEADER.get()) {
         servletResponse.addHeader("X-Zuul-Debug-Header", debugHeader.toString());
      }
   }
   if (zuulResponseHeaders != null) {
      for (Pair it : zuulResponseHeaders) {
         servletResponse.addHeader(it.first(), it.second());
      }
   }
   RequestContext ctx = RequestContext.getCurrentContext();
   Long contentLength = ctx.getOriginContentLength();
   // Only inserts Content-Length if origin provides it and origin response is not
   // gzipped
   if (SET_CONTENT_LENGTH.get()) {
      if (contentLength != null && !ctx.getResponseGZipped()) {
         servletResponse.setContentLength(contentLength.intValue());
      }
   }
}

重点看writeResponse,从上下文中找到response,将数据利用输出流写出去就行了。这个流程就结束了

你可能感兴趣的:(Zuul 源码分析)