Spring Cloud Zuul 精進


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

忽略所有微服務或某些微服務

默認情況下,只要引入了zuul后,就會自動一個默認的路由配置,但有些時候我們可能不想要默認的路由配置規則,想自己進行定義,忽略所有微服務:(后面寫 * ):

zuul:  
  ignored-services: "*"  

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

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

忽略所有為服務,只路由指定的微服務

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

通過path和url訪問到具體的某台機器上

有時候我們測試的時候需要訪問到具體的某台機器上,而不希望負載均衡到別的機器上或者需要訪問到第三方的某台機器上:

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

注意:

  1. product-provider 這個值可以隨便寫,即使是一個不存在的值;
  2. 這種方式訪問不會作為 HystrixCommand 來進行訪問;
  3. url 里面也不可以寫多個url

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

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

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來做邏輯過濾。原理其實很簡單,重要的是思想。作為一個網關,它是很重要的服務,這種實現方式大家覺得是否優雅,是否還有別的實現方式呢?如果是你你會如何實現網關,這些問題大家可以慢慢思考。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM