Spring Cloud Zuul 精進

接着上一篇繼續講Zuul,上一篇搭建了Zuul的環境簡單說明了怎麼使用,本篇查缺補漏將一些經常使用的配置貼出來。文末咱們會一塊兒分析一下Zuul的源碼,昇華一下本篇的格調!java

忽略全部微服務或某些微服務web

默認狀況下,只要引入了zuul後,就會自動一個默認的路由配置,但有些時候咱們可能不想要默認的路由配置規則,想本身進行定義,忽略全部微服務:(後面寫 * ):正則表達式

zuul:  
  ignored-services: "*"

忽略某些微服務:(直接寫微服務的名字=>能夠理解爲spring.application.name的值,多個以都好分隔)spring

zuul:  
  ignored-services: product-provider,product-consumer-8201

忽略全部爲服務,只路由指定的微服務api

zuul:
  # 排除全部的服務,只由指定的服務進行路由
  ignored-services: "*"
  routes:
    eureka-client:
      path: /client1/**
      serviceId: eureka-client

經過path和url訪問到具體的某臺機器上瀏覽器

有時候咱們測試的時候須要訪問到具體的某臺機器上,而不但願負載均衡到別的機器上或者須要訪問到第三方的某臺機器上:服務器

zuul:  
  routes:  
    product-provider:  
      path: /product/**  
      url: http://localhost:8202/

注意:cookie

  1. product-provider 這個值能夠隨便寫,即便是一個不存在的值;
  2. 這種方式訪問不會做爲 HystrixCommand 來進行訪問;
  3. url 裏面也不能夠寫多個url

敏感頭的傳遞(好比Cookie等)全局設置和某個微服務設置多線程

有些時候咱們微服務上游可能想傳遞一些請求頭到下游的服務,好比Token、Cookie等值,默認狀況下,zuul 不會將 Cookie,Set-Cookie,Authorization 這三個頭傳遞到下游服務,若是須要傳遞,就須要忽略這些敏感頭。app

zuul:
  #全部服務路徑前統一加上前綴
  prefix: /api
  # 排除某些路由, 支持正則表達式
  ignored-patterns:
    - /**/modify/pwd
  # 排除服務
  ignored-services: user-center
  routes:
    eureka-client:
      path: /client1/**
      serviceId: eureka-client
      sensitiveHeaders:  #當前這個路由的header爲空
  sensitiveHeaders: Cookie,Set-cookie #全局路由都帶這些header

Zuul 源碼淺析

開啓Zuul很簡單,在啓動類上添加Zuul 開啓註解:

@EnableZuulProxy
/**
 * Sets up a Zuul server endpoint and installs some reverse proxy filters in it, so it can
 * forward requests to backend servers. The backends can be registered manually through
 * configuration or via DiscoveryClient.
 *
 * @see EnableZuulServer for how to get a Zuul server without any proxying
 *
 * @author Spencer Gibb
 * @author Dave Syer
 * @author Biju Kunjummen
 */
@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}

上面的註釋上有一句話:EnableZuulServer 是不使用代理功能來獲取Zuul server。開啓Zuul 網關有兩種方式:

  • @EnableZuulServer : 普通網關,只支持基本的route與filter;
  • @EnableZuulProxy :配合上服務發現與熔斷開關,是 EnableZuulServer 的加強版,具備反向代理功能。

簡單來講,@EnableZuulProxy可理解爲@EnableZuulServer的加強版,當Zuul與Eureka、Ribbon等組件配合使用時,咱們使用@EnableZuulProxy。

接着看 EnableZuulProxy,在類頭引用了ZuulProxyMarkerConfiguration,ZuulProxyAutoConfiguration 的做用是開啓 ZuulProxyAutoConfiguration的標記。

ZuulProxyAutoConfiguration 繼承了 ZuulServerAutoConfiguration,是 ZuulServerAutoConfiguration 的超集。該類注入了DiscoveryClient、RibbonCommandFactoryConfiguration用做負載均衡相關的。注入了一些列的filters,好比PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter。

ZuulServerAutoConfiguration更爲重要:

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass({ZuulServlet.class, ZuulServletFilter.class})
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
// Make sure to get the ServerProperties from the same place as a normal web app would
// FIXME @Import(ServerPropertiesAutoConfiguration.class)
public class ZuulServerAutoConfiguration {

    @Autowired
    protected ZuulProperties zuulProperties;

    @Autowired
    protected ServerProperties server;

    @Autowired(required = false)
    private ErrorController errorController;

    private Map<String, CorsConfiguration> corsConfigurations;

    @Autowired(required = false)
    private List<WebMvcConfigurer> configurers = emptyList();

    @Bean
    public HasFeatures zuulFeature() {
        return HasFeatures.namedFeature("Zuul (Simple)", ZuulServerAutoConfiguration.class);
    }

    @Bean
    @Primary
    public CompositeRouteLocator primaryRouteLocator(
            Collection<RouteLocator> routeLocators) {
        return new CompositeRouteLocator(routeLocators);
    }

 /**
  * 路由定位器
  *
  */
    @Bean
    @ConditionalOnMissingBean(SimpleRouteLocator.class)
    public SimpleRouteLocator simpleRouteLocator() {
        return new SimpleRouteLocator(this.server.getServlet().getContextPath(),
                this.zuulProperties);
    }

  /**
  * Zuul建立的一個Controller,用於將請求交由ZuulServlet處理
  *
  */
    @Bean
    public ZuulController zuulController() {
        return new ZuulController();
    }

  /**
  * 會添加到SpringMvc的HandlerMapping鏈中,
  *只有選擇了ZuulHandlerMapping的請求才能出發到Zuul的後續流程
  *
  */
    @Bean
    public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
        ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
        mapping.setErrorController(this.errorController);
        mapping.setCorsConfigurations(getCorsConfigurations());
        return mapping;
    }

/**
  * ZuulServlet是整個流程的核心
  *
  *
  */
    @Bean
    @ConditionalOnMissingBean(name = "zuulServlet")
    @ConditionalOnProperty(name = "zuul.use-filter", havingValue = "false", matchIfMissing = true)
    public ServletRegistrationBean zuulServlet() {
        ServletRegistrationBean<ZuulServlet> 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;
    }

    
    ......
    ......
    ......
    
}

在 ZuulServerAutoConfiguration 中定義了幾個核心對象:

  • ZuulController:全部 路由的默認Controller;
  • ZuulHandlerMapping:Zuul 路由Mapping映射器;
  • ZuulServlet:繼承自HttpServlet,過濾邏輯從這裏始,到這裏終。

ZuulServlet是整個流程的核心,大體的請求過程以下:

當Zuulservlet收到請求後, 會建立一個ZuulRunner對象,該對象中初始化了RequestContext:存儲請求的ServletRequest 和 ServletResponse對象,並被當前請求鏈上的全部Zuulfilter共享;

ZuulRunner中還有一個 FilterProcessor,FilterProcessor做爲執行全部的Zuulfilter的管理器;

FilterProcessor從filterloader 中獲取zuulfilter,而zuulfilter是被filterFileManager所加載,並支持groovy熱加載,採用了輪詢的方式熱加載;

有了這些filter以後,zuulservelet首先執行的Pre類型的過濾器,再執行route類型的過濾器, 最後執行的是post 類型的過濾器,若是在執行這些過濾器有錯誤的時候則會執行error類型的過濾器;

執行完這些過濾器,最終將請求的結果返回給客戶端。

RequestContext就是會一直跟着整個請求週期的上下文對象,filters之間有什麼信息須要傳遞就set一些值進去就好了。

ZuulServlet 掌控全部url的流轉,咱們先看它作了什麼工做:

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

    @RunWith(MockitoJUnitRunner.class)
    public static class UnitTest {

        @Mock
        HttpServletRequest servletRequest;
        @Mock
        HttpServletResponseWrapper servletResponse;
        @Mock
        FilterProcessor processor;
        @Mock
        PrintWriter writer;

        @Before
        public void before() {
            MockitoAnnotations.initMocks(this);
        }

        @Test
        public void testProcessZuulFilter() {

            ZuulServlet zuulServlet = new ZuulServlet();
            zuulServlet = spy(zuulServlet);
            RequestContext context = spy(RequestContext.getCurrentContext());


            try {
                FilterProcessor.setProcessor(processor);
                RequestContext.testSetCurrentContext(context);
                when(servletResponse.getWriter()).thenReturn(writer);

                zuulServlet.init(servletRequest, servletResponse);
                verify(zuulServlet, times(1)).init(servletRequest, servletResponse);
                assertTrue(RequestContext.getCurrentContext().getRequest() instanceof HttpServletRequestWrapper);
                assertTrue(RequestContext.getCurrentContext().getResponse() instanceof HttpServletResponseWrapper);

                zuulServlet.preRoute();
                verify(processor, times(1)).preRoute();

                zuulServlet.postRoute();
                verify(processor, times(1)).postRoute();
//                verify(context, times(1)).unset();

                zuulServlet.route();
                verify(processor, times(1)).route();
                RequestContext.testSetCurrentContext(null);

            } catch (Exception e) {
                e.printStackTrace();
            }


        }
    }

}

ZuulServlet 繼承了HttpServlet,主要的做用就是對HTTP請求進行攔截作對應的處理。直接看實現方法 service()中的實現:

@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
  try {
    init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

    //這裏從RequestContext中取出當前線程中封裝好的對象而後在該對象上打上zuul處理的標記
    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();
  }
}

第一句init方法:

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));
}

在這裏調用了一個RequestContext類將HttpServletRequest保存進去,而 RequestContext 類自己也比較特殊:

public class RequestContext extends ConcurrentHashMap<String, Object> {

  rotected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
    @Override
    protected RequestContext initialValue() {
      try {
        return contextClass.newInstance();
      } catch (Throwable e) {
        throw new RuntimeException(e);
      }
    }
  };
  
  public static RequestContext getCurrentContext() {
    if (testContext != null) return testContext;

    RequestContext context = threadLocal.get();
    return context;
  }
  
}

它自己就是一個Map,須要注意的是,該對象的使用方式並非直接new 一個新對象,而是調用getCurrentContext()方法,該方法中返回的是 threadLocal 封裝的反射生成new RequestContext的方式來建立對象。確保每一個建立的 RequestContext 只在當前線程內有效,即在當前線程內,getCurrentContext()方法取出的是同一個 RequestContext對象。

繼續回到service()方法,拿到了封裝好了的RequestContext方法以後,下面進入四個route中,上節已經講過這4個route都屬於Filter的生命週期,在這裏完成請求的過濾,轉發,後置邏輯處理。route完成以後,最後的finally方法中調用了RequestContext.getCurrentContext().unset()方法,既然使用了threadLocal,必然使用完要清除,否則極可能就內存泄漏。

小憩一會:

分析到 ZuulServlet,不知你是否發現Zuul的核心。對於Zuul實現網關的功能其實就是圍繞着HttpServlet拿到ServletRequest,對請求作過濾操做,拿到ServletResponse 對返回結果作後置處理操做。HttpServlet是單實例多線程的處理模型,若是存在某一個請求比較耗時,那麼該線程就會一直阻塞直處處理完成返回成功才結束。倘若這樣的請求不少,對Zuul所在的服務器壓力仍是不小。

Zuul如何處理一個請求

上面已經分析得出Zuul是基於Servlet這一套邏輯來作的,往下跟就變得簡單。SpringMVC是如何處理請求的呢?你們應該都比較熟悉,瀏覽器發出一個請求到達服務端,首先到達DispatcherServlet,Servlet容器將請求交給HandlerMapping,找到對應的Controller訪問路徑和處理方法對應關係,接着交由HandlerAdapter路由到真實的處理邏輯中去進行處理。

上面我貼出來 ZuulServerAutoConfiguration#ZuulHandlerMapping,定義了ZuulHandlerMapping bean對象。

public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {

}

ZuulHandlerMapping 自身繼承了AbstractUrlHandlerMapping,即經過url來查找對應的處理器。判斷的核心邏輯在 lookupHandler方法中:

@Override
protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
  if (this.errorController != null && urlPath.equals(this.errorController.getErrorPath())) {
    return null;
  }
  //判斷urlPath是否被忽略,若是忽略則返回null
  if (isIgnoredPath(urlPath, this.routeLocator.getIgnoredPaths())) return null;
  RequestContext ctx = RequestContext.getCurrentContext();
  if (ctx.containsKey("forward.to")) {
    return null;
  }
  if (this.dirty) {
    synchronized (this) {
      if (this.dirty) {
        //若是沒有加載過路由或者路由有刷新,則加載路由
        registerHandlers();
        this.dirty = false;
      }
    }
  }
  return super.lookupHandler(urlPath, request);
}


private void registerHandlers() {
  Collection<Route> routes = this.routeLocator.getRoutes();
  if (routes.isEmpty()) {
    this.logger.warn("No routes found from RouteLocator");
  }
  else {
    for (Route route : routes) {
      //調用父類,註冊處理器,這裏全部路徑的處理器都是ZuulController
      registerHandler(route.getFullPath(), this.zuul);
    }
  }
}

總體邏輯就是在路由加載的時候須要爲每一個路由指定處理器,由於Zuul不負責邏輯處理,因此它也沒有對應的Controller可使用,那怎麼辦呢,註冊處理器的時候,使用的是ZuulController,是Controller的子類,對應的適配器是SimpleControllerHandlerAdapter,也就說每個路由規則公共處理器都是ZuulController,這個處理器最終會調用ZuulServlet通過zuul定義的和自定義的攔截器。

上面還有一句:

Collection<Route> routes = this.routeLocator.getRoutes();

RouteLocator的做用是路由定位器,先看它有哪些實現類:

  • SimpleRouteLocator:主要加載配置文件的路由規則;
  • DiscoveryClientRouteLocator:服務發現的路由定位器,去註冊中心如Eureka,Consul等拿到服務名稱,以這樣的方式/服務名稱/**映射成路由規則;
  • CompositeRouteLocator:複合路由定位器,主要集成全部的路由定位器(如配置文件路由定位器,服務發現定位器,自定義路由定位器等)來路由定位;
  • RefreshableRouteLocator:路由刷新,只有實現了此接口的路由定位器才能被刷新。

從實現類的功能看路由定位器的做用就是區分當前從哪裏加載路由進行註冊。上面這幾個實現類都實現了Ordered類,加載的順序依照getOrder()數值大小來定。

至此咱們已經把Zuul最核心的路由部分擼了一遍,從Spring MVC 加載Servlet 的過程入手,到自定義 ZuulServlet 進行處理,進而使用Zuul中定義的各類Filter來作邏輯過濾。原理其實很簡單,重要的是思想。做爲一個網關,它是很重要的服務,這種實現方式你們以爲是否優雅,是否還有別的實現方式呢?若是是你你會如何實現網關,這些問題你們能夠慢慢思考。

相關文章
相關標籤/搜索