微服務實戰(zhàn)SpringCloud之Zuul

注意:本文的前提是基于zuul的1.3.X版本來解析的炸茧,2.0版本采用了netty作為底層框架重新設(shè)計了整個zuul的架構(gòu)乌逐,將在后面進行分析擎椰。

zuul是什么

zuul是Netflix設(shè)計用來為所有面向設(shè)備逻住、web網(wǎng)站提供服務的所有應用的門面钟哥,zuul可以提供動態(tài)路由、監(jiān)控鄙信、彈性擴展瞪醋、安全認證等服務,他還可以根據(jù)需求將請求路由到多個應用中装诡。

zuul是用來解決什么問題的

在使用網(wǎng)關(guān)之前银受,動態(tài)的路由是通過Nginx的配置來做的,但是一旦發(fā)生改變鸦采,比如IP地址發(fā)生改變宾巍,加入其它路由,就要重新配置Nginx渔伯,重啟Nginx顶霞。安全認證是放在每一個應用中,應用中包含了非業(yè)務強相關(guān)的內(nèi)容锣吼,看起來也是不夠優(yōu)雅选浑。

在目前的應用中,zuul主要用來做如下幾件事情:

  • 動態(tài)路由:APP玄叠、web網(wǎng)站通過zuul來訪問不同的服務提供方古徒,且與ribbon結(jié)合,還可以負載均衡的路由到同一個應用不同的實例中读恃。

  • 安全認證:zuul作為互聯(lián)網(wǎng)服務架構(gòu)中的網(wǎng)關(guān)隧膘,可以用來校驗非法訪問、授予token寺惫、校驗token等疹吃。

  • 限流:zuul通過記錄每種請求的類型來達到限制訪問過多導致服務down掉的目的。

  • 靜態(tài)響應處理:直接在zuul就處理一些請求西雀,返回響應內(nèi)容萨驶,不轉(zhuǎn)發(fā)到微服務內(nèi)部。

  • 區(qū)域彈性:主要是針對AWS上的應用做一些彈性擴展艇肴。

zuul的最佳實踐是怎樣的

Netflix是如何使用zuul的篡撵?

[站外圖片上傳中...(image-f9d654-1547796647001)]

可以看到判莉,在Netflix的架構(gòu)中,亞馬遜的彈性負載均衡作為第一層育谬,zuul作為第二層券盅,為所有應用的網(wǎng)關(guān),請求經(jīng)過AWS的負載均衡先發(fā)送到zuul膛檀,zuul再將流量轉(zhuǎn)發(fā)到各個API锰镀、website等等,然后各個API咖刃、website應用再調(diào)用各個微服務泳炉。

當然在Netflix的使用中,zuul也結(jié)合了Netflix的其他微服務組件一起使用嚎杨。

  • Hystrix:用來服務降級及熔斷

  • Ribbon:用來作為軟件的負載均衡花鹅,微服務之間相互調(diào)用是通過ribbon作為軟件負載均衡使用負載到微服務集群內(nèi)的不同的實例

  • Turbin:監(jiān)控服務的運行狀況

  • Feign:用作微服務之間發(fā)送rest請求的組件,可以將rest調(diào)用類似spring的其他bean一樣直接注入使用功能

  • Eureka:服務注冊中心

入門示例

引入zuul

dependencies {
    //spring-cloud-starter-netflix-zuul模塊
    implementation('org.springframework.cloud:spring-cloud-starter-netflix-zuul')
    //test模塊
    testImplementation('org.springframework.boot:spring-boot-starter-test')
    //引入eureka-client模塊
    implementation('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
}

這里引入eureka-client是為了將zuul也注冊到eureka中枫浙,作為微服務中的一個應用刨肃,那么zuul就能將eureka中注冊的所有微服務應用的注冊信息都拿到,從而做到動態(tài)路由箩帚。

啟動類

@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }
}

zuul的路由配置

zuul的配置類為ZuulProperties真友,配置前綴為zuul。

下面為路由的一些示例配置(未結(jié)合eureka):

zuul:
  routes:
    product: http://localhost:8082/product/**
    cart: http://localhost:8081/cart/**

zuul服務引用了eureka-client后配置可以改成如下:

zuul:
  routes:
    product: /product/**
    cart: /cart/**

或者更細致的配置:

zuul:
  routes:
    cart:
      path: /cart/**
      serviceId: cart

原訪問地址:http://localhost:8081/cart/1

經(jīng)過zuul后的訪問地址:http://localhost:8080/cart/cart/1

訪問效果:

zuul-訪問效果1.png

注意:zuul只有結(jié)合了eureka紧帕,才會有ribbon作為軟件負載均衡盔然,直接配置邏輯URL,不會起到負載均衡的效果是嗜,也不會有hystrix作為熔斷器使用愈案。

如下:

zuul:
  routes:
  users:
  path: /product/**
  url: http://localhost:8082/product

如果想指定多個服務的列表且需要通過ribbon實現(xiàn)負載均衡,配置可參考如下:

zuul:
  routes:
    users:
      path: /product/**
      serviceId: product

ribbon:
  eureka:
    enabled: false

product:
  ribbon:
    listOfServers: example.com,google.com

當整合eureka時鹅搪,配置簡單站绪,服務太多,不想一個個配置的時候涩嚣,可以通過定義PatternServiceRouteMapper來全局定義匹配規(guī)則崇众。

個人推薦這種方式掂僵,簡單航厚。

示例如下:

@Configuration
public class ZuuPatternServiceRouteMapperConfiguration {

    //注意!C膛睢幔睬!這兩者保留一個即可。
    /**
     * 沒有版本號的路由匹配規(guī)則bean
     * @retu沒rn 路由匹配規(guī)則bean
     */
    @Bean
    public PatternServiceRouteMapper patternServiceRouteMapper(){

        return new PatternServiceRouteMapper("(?<version>v.*$)","${name}");
    }

    /**
     * 有版本號的路由匹配規(guī)則bean
     * @return 路由匹配規(guī)則bean
     */
    @Bean
    public PatternServiceRouteMapper patternServiceRouteMapperWithVersion(){
        return new PatternServiceRouteMapper("(?<name>.*)-(?<version>v.*$)","${version}/${name}");
    }

}

全局添加映射前綴

通過zuul.prefix可以指定全局映射前綴芹扭,可以避免服務URL直接暴露出去麻顶,如前綴為/api(注意赦抖,這個”/“要加上,否則可能會出現(xiàn)404)辅肾,默認情況下队萤,代理前綴會在請求轉(zhuǎn)發(fā)前從請求中刪除前綴,可以通過zuul.stripPrefix來配置矫钓,默認是true要尔。

zuul:
  prefix: /api
  strip-prefix: true

這樣原來直接訪問比如說cart服務的地址為http://localhost:8081/cart/1,通過zuul網(wǎng)關(guān)新娜,且添加了全局映射前綴/api后的訪問路徑變?yōu)?a href="http://localhost:8080/api/cart/cart/1" target="_blank" rel="nofollow">http://localhost:8080/api/cart/cart/1赵辕。

如果在局部路由想關(guān)閉這個刪除前綴映射,可以通過以下配置指定:

zuul:
   routes:
     cart:
       path: /cart/**
       stripPrefix: false

不走路由的服務或者映射配置

ZuulProperties配置中概龄,有兩個屬性ignoredServices还惠,ignoredPatterns,屬性類型均為LinkedHashSet私杜。

ignoredServices:指定忽略某些服務的路由

ignoredPatterns:配置不走路由的某些路由匹配規(guī)則

zuul的filter

1.X版本的zuul實現(xiàn)是依賴于servlet的蚕键,所以對過濾器支持較好。zuul本身提供了較多的filter歪今,如SendForwardFilter嚎幸、DebugFilter、SendResponseFilter寄猩、SendErrorFilter等嫉晶。

zuul本身也提供了抽象類ZuulFilter,供自定義filter田篇。

  • pre:在zuul按照規(guī)則路由到下游服務之前執(zhí)行替废,如果需要對請求進行預處理,比如鑒權(quán)泊柬、限流等椎镣,應在此類型的過濾器實現(xiàn)。
  • route:這類filter是zuul路由的具體執(zhí)行者兽赁,是HTTPClient状答、OKhttp、或ribbon發(fā)送原始http請求的地方刀崖。
  • post:這類filter是下游服務返回響應信息后執(zhí)行的地方惊科,如果需要對response做一些處理,可以考慮在此類過濾器實現(xiàn)亮钦。
  • error:在整個生命周期類如果出現(xiàn)異常馆截,則會進入此類過濾器,可作為全局異常處理。

自定義ZuulFilter

自定義ZuulFilter蜡娶,需要實現(xiàn)幾個方法混卵,下面為前置過濾器的簡單示例。

public class AuthenticationFilter extends ZuulFilter {
    @Override
    public String filterType() {
        //fiterType,有pre窖张、route幕随、post、error四種類型宿接,分別代表路由前合陵、路由時
        //路由后以及異常時的過濾器
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        //排序,指定相同filterType時的執(zhí)行順序澄阳,越小越優(yōu)先執(zhí)行拥知,這里指定順序為路由過濾器的順序-1,即在路由前執(zhí)行
        return return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        //是否應該調(diào)用run()方法
        return false;
    }

    @Override
    public Object run() throws ZuulException {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
        String token = request.getHeader("X-Authentication");
        if (StringUtils.isBlank(token)) {
            throw new TokenNotValidException("token不存在碎赢!");
        }
        //校驗token正確性
        return null;
    }
}

多個過濾器之間可能想要傳輸內(nèi)容低剔,那么可以通過zuul提供的RequestContext來完成,RequestContext本身也是依賴于ThreadLocal來完成的肮塞。RequestContext本身也攜帶了HttpServletRequest和HttpServletResponse襟齿,可以獲取請求或響應中的內(nèi)容。

zuul的過濾器也可以配置為不啟用

通過zuul.<SimpleClassName>.<filterType>.disable=false來關(guān)閉指定過濾器枕赵。

例如:

zuul.SendResponseFilter.post.disable=true

@EnableZuulServer與@EnableZuulProxy的區(qū)別

在zuul的@EnableZuulProxy注解的源代碼中猜欺,是這么說的,@EnableZuulProxy提供了一些基本的反向代理過濾器拷窜,@EnableZuulServer只是將zuul指定為一個zuul的server开皿,并不提供任何反向代理的過濾器。一般我們推薦使用@EnableZuulProxy篮昧,如果不想用zuul自帶的過濾器赋荆,可以通過上面的方式關(guān)閉指定的過濾器。

@EnableZuulProxy自帶的filter如下:

  1. pre類型:

    • ServletDetectionFilter

    • FormBodyWrapperFilter:解析請求form表單并在轉(zhuǎn)發(fā)到下游請求前重新編碼

    • DebugFilter:會打印執(zhí)行每個過濾器的執(zhí)行日志懊昨,但僅僅是打印”Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);“

    • SendForwardFilter:將請求轉(zhuǎn)發(fā)(路由)到下游服務

  2. post類型:

    • SendResponseFilter:將下游服務對請求的響應寫入當前response中窄潭。
  3. error類型:

    • SendErrorFilter:處理異常情況的過濾器

zuul的http客戶端

zuul實現(xiàn)路由,是通過http方式轉(zhuǎn)發(fā)到其他服務的酵颁,那么就需要http客戶端嫉你。默認情況下是通過Apache HTTP Client來發(fā)送http請求的,如果想使用restClient或者OKhttp可以通過配置指定躏惋。

ribbon:
  okhttp:
    enabled: true

或者

ribbon:
  restclient:
    enabled: true

當然可以自定義Apache HTTP Client和OkHttpClient并聲明為Bean幽污。

敏感header內(nèi)容

zuul支持將一些敏感的header內(nèi)容不轉(zhuǎn)發(fā)到下游的其他服務中,通過zuul.sensitiveHeaders將這些header內(nèi)容對下游服務屏蔽其掂。默認情況下油挥,header中的Cookie潦蝇、Set-Cookie款熬、Authorization將不被傳遞到下游服務器中深寥,可以通過zuul.sensitiveHeaders指定全局的敏感header內(nèi)容。所以在下游服務器想要獲取到cookie內(nèi)容時贤牛,這里需要重新配置下惋鹅。個人建議,cookie殉簸、Authorization內(nèi)容應該在zuul內(nèi)將其轉(zhuǎn)換成下游服務需要的userId闰集、user等,再傳遞到下游服務器般卑。

超時配置

zuul支持配置超時時間武鲁,如果使用的是eureka作為服務注冊中心,那么只要指定ribbon的超時時間即可蝠检。即ribbon.ReadTimeout和ribbon.SocketTimeout沐鼠。如果使用的是具體的URL路由,那么通過zuul.host.connect-timeout-millis和zuul.host.socket-timeout-millis指定叹谁。zuulProperties內(nèi)部類Host中還有一些諸如maxTotalConnections最大連接數(shù)等的一些配置饲梭。

CORS配置

默認情況下,zuul是允許所有cors請求的焰檩,如果要自定義cors憔涉,可以通過自定義WebMvcConfigurer來完成。

如:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/*")
                .allowedOrigins("http://baidu.com");
    }
}

其他配置

重試

zuul.retryable可以配置zuul是否使用ribbon的重試機制析苫。

代理請求header

zuul.addProxyHeaders可以配置是否將X-Forwarded-Host放入轉(zhuǎn)發(fā)的請求中

zuul的實現(xiàn)原理及設(shè)計是怎樣的

了解了zuul的使用方式兜叨,我們要開始了解下zuul的源碼以及zuul的整個架構(gòu)設(shè)計。

先從啟動類ZuulApplication入手衩侥。

@EnableDiscoveryClient
@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }
}

可以看到啟動類上加了注解@EnableZuulProxy浪腐,我們看下這個注解的源碼。

@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}

這個注解導入了一個配置類ZuulProxyMarkerConfiguration顿乒。

/**
 * Responsible for adding in a marker bean to trigger activation of 
 * {@link ZuulProxyAutoConfiguration}
 *
 * @author Biju Kunjummen
 */

@Configuration
public class ZuulProxyMarkerConfiguration {
   @Bean
   public Marker zuulProxyMarkerBean() {
      return new Marker();
   }

   class Marker {
   }
}

可以看到這個類什么都沒做议街,但是注釋上說明了這個類的作用是觸發(fā)另一個配置類ZuulProxyAutoConfiguration。

@Configuration
//導入了發(fā)送http請求的配置類璧榄,分別是對restClient特漩、httpClient、OKhttp等封裝的配置類
@Import({ RibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class,
      RibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class,
      RibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class,
      HttpClientConfiguration.class })
@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class)
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {

   @SuppressWarnings("rawtypes")
   @Autowired(required = false)
   private List<RibbonRequestCustomizer> requestCustomizers = Collections.emptyList();

   @Autowired(required = false)
   private Registration registration;

   @Autowired
   private DiscoveryClient discovery;

   @Autowired
   private ServiceRouteMapper serviceRouteMapper;

   @Override
   public HasFeatures zuulFeature() {
      return HasFeatures.namedFeature("Zuul (Discovery)",
            ZuulProxyAutoConfiguration.class);
   }
   //依賴服務發(fā)現(xiàn)的路由定向類
   @Bean
   @ConditionalOnMissingBean(DiscoveryClientRouteLocator.class)
   public DiscoveryClientRouteLocator discoveryRouteLocator() {
      return new DiscoveryClientRouteLocator(this.server.getServlet().getContextPath(), this.discovery, this.zuulProperties,
            this.serviceRouteMapper, this.registration);
   }

   // pre filters 前置過濾器骨杂,基于RouteLocator決定如何路由以及路由到哪個服務
   @Bean
   @ConditionalOnMissingBean(PreDecorationFilter.class)
   public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator, ProxyRequestHelper proxyRequestHelper) {
      return new PreDecorationFilter(routeLocator, this.server.getServlet().getContextPath(), this.zuulProperties,
            proxyRequestHelper);
   }

   // route filters 路由過濾器涂身,和ribbon結(jié)合的路由過濾器,決定路由到服務的具體哪個實例
   @Bean
   @ConditionalOnMissingBean(RibbonRoutingFilter.class)
   public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper,
         RibbonCommandFactory<?> ribbonCommandFactory) {
      RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, ribbonCommandFactory,
            this.requestCustomizers);
      return filter;
   }
   //route filter 路由過濾器搓蚪,針對預先決定的(配置了URL的)服務蛤售,執(zhí)行簡單的路由功能,并且此時也沒有配置  CloseableHttpClient的bean
   @Bean
   @ConditionalOnMissingBean({SimpleHostRoutingFilter.class, CloseableHttpClient.class})
   public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper,
         ZuulProperties zuulProperties,
         ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
         ApacheHttpClientFactory httpClientFactory) {
      return new SimpleHostRoutingFilter(helper, zuulProperties,
            connectionManagerFactory, httpClientFactory);
   }
//route filter 路由過濾器,針對預先決定的(配置了URL的)服務悴能,執(zhí)行簡單的路由功能揣钦,并且配置  CloseableHttpClient的bean
   @Bean
   @ConditionalOnMissingBean({SimpleHostRoutingFilter.class})
   public SimpleHostRoutingFilter simpleHostRoutingFilter2(ProxyRequestHelper helper,
                                             ZuulProperties zuulProperties,
                                             CloseableHttpClient httpClient) {
      return new SimpleHostRoutingFilter(helper, zuulProperties,
            httpClient);
   }

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

   @Configuration
   @ConditionalOnMissingClass("org.springframework.boot.actuate.health.Health")
   protected static class NoActuatorConfiguration {

      //請求代理工具類,處理請求的URL轉(zhuǎn)換漠酿、參數(shù)冯凹、請求頭等等
      @Bean
      public ProxyRequestHelper proxyRequestHelper(ZuulProperties zuulProperties) {
         ProxyRequestHelper helper = new ProxyRequestHelper(zuulProperties);
         return helper;
      }

   }

   @Configuration
   @ConditionalOnClass(Health.class)
   protected static class EndpointConfiguration {

      @Autowired(required = false)
      private HttpTraceRepository traces;

      //列出所有路由信息的endpoint 
      @Bean
      @ConditionalOnEnabledEndpoint
      public RoutesEndpoint routesEndpoint(RouteLocator routeLocator) {
         return new RoutesEndpoint(routeLocator);
      }
       
      //列出所有zuul的過濾器的endpoint 
      @ConditionalOnEnabledEndpoint
      @Bean
      public FiltersEndpoint filtersEndpoint() {
         FilterRegistry filterRegistry = FilterRegistry.instance();
         return new FiltersEndpoint(filterRegistry);
      }

      //增強了log的請求代理工具類
      @Bean
      public ProxyRequestHelper proxyRequestHelper(ZuulProperties zuulProperties) {
         TraceProxyRequestHelper helper = new TraceProxyRequestHelper(zuulProperties);
         if (this.traces != null) {
            helper.setTraces(this.traces);
         }
         return helper;
      }
   }
}

從源碼中可以看到,ZuulProxyAutoConfiguration配置了一些諸如DiscoveryClientRouteLocator(依賴服務發(fā)現(xiàn)的路由定向類)炒嘲、ProxyRequestHelper(請求代理工具類)宇姚、zuul的幾個pre、route夫凸、post過濾器等bean浑劳。

ZuulProxyAutoConfiguration還導入了兼容了幾種兼容發(fā)送http請求的配置類,如HTTPClient夭拌、OKhttp呀洲、restClient等。

除此之外啼止,ZuulProxyAutoConfiguration還繼承了ZuulProxyAutoConfiguration類道逗。我們看下這個類做了哪些配置操作。

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass(ZuulServlet.class)//基于ZuulServlet的配置
@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 {

   //zuul的配置類献烦,之前配置的zuul的prefix滓窍、routes等都是配置在這個類中
   @Autowired
   protected ZuulProperties zuulProperties;

   //server的配置類,主要是獲取contextPath 
   @Autowired
   protected ServerProperties server;

   //錯誤異常處理controller巩那,springboot自帶BasicErrorController 
   @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);
   }

   //簡單的路由定向類吏夯,基于ZuulProperties 
   @Bean
   @ConditionalOnMissingBean(SimpleRouteLocator.class)
   public SimpleRouteLocator simpleRouteLocator() {
      return new SimpleRouteLocator(this.server.getServlet().getContextPath(),
            this.zuulProperties);
   }

   //聲明ZuulController,將ZuulServlet交給spring管理即横,springMvc mapping到路由噪生,會路由到此controller 
   @Bean
   public ZuulController zuulController() {
      return new ZuulController();
   }

   //配置zuul handlerMapping,將請求的地址與遠程服務匹配 
   @Bean
   public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
      ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
      mapping.setErrorController(this.errorController);
      mapping.setCorsConfigurations(getCorsConfigurations());
      return mapping;
   }

   protected final Map<String, CorsConfiguration> getCorsConfigurations() {
      if (this.corsConfigurations == null) {
         ZuulCorsRegistry registry = new ZuulCorsRegistry();
         this.configurers
               .forEach(configurer -> configurer.addCorsMappings(registry));
         this.corsConfigurations = registry.getCorsConfigurations();
      }
      return this.corsConfigurations;
   }

   //zuul的應用監(jiān)聽器东囚,用于監(jiān)聽下游服務有沒有刷新applicationContext 
   @Bean
   public ApplicationListener<ApplicationEvent> zuulRefreshRoutesListener() {
      return new ZuulRefreshListener();
   }

   //注冊zuulServlet到spring中 
   @Bean
   @ConditionalOnMissingBean(name = "zuulServlet")
   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;
   }

   // pre filters跺嗽,決定請求是走DispatcherServlet還是ZuulServlet
   @Bean
   public ServletDetectionFilter servletDetectionFilter() {
      return new ServletDetectionFilter();
   }
   //解析form表單數(shù)據(jù),并重新編碼       
   @Bean
   public FormBodyWrapperFilter formBodyWrapperFilter() {
      return new FormBodyWrapperFilter();
   }

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

   //支持servlet 3.0的包裝過濾器页藻,對請求進行包裝 
   @Bean
   public Servlet30WrapperFilter servlet30WrapperFilter() {
      return new Servlet30WrapperFilter();
   }

   // post filters桨嫁,后置過濾器,將代理請求中的響應寫入response中
   @Bean
   public SendResponseFilter sendResponseFilter(ZuulProperties properties) {
      return new SendResponseFilter(zuulProperties);
   }

   //處理error的filter份帐,執(zhí)行順序為0 
   @Bean
   public SendErrorFilter sendErrorFilter() {
      return new SendErrorFilter();
   }

   //重定向的filter璃吧,執(zhí)行順序為500 
   @Bean
   public SendForwardFilter sendForwardFilter() {
      return new SendForwardFilter();
   }

   //饑餓加載時,從zuulPropertis中獲取所有路由信息 
   @Bean
   @ConditionalOnProperty(value = "zuul.ribbon.eager-load.enabled")
   public ZuulRouteApplicationContextInitializer zuulRoutesApplicationContextInitiazer(
         SpringClientFactory springClientFactory) {
      return new ZuulRouteApplicationContextInitializer(springClientFactory,
            zuulProperties);
   }

   @Configuration
   protected static class ZuulFilterConfiguration {

      @Autowired
      private Map<String, ZuulFilter> filters;

      //初始化zuul filter各種各樣組件
      @Bean
      public ZuulFilterInitializer zuulFilterInitializer(
            CounterFactory counterFactory, TracerFactory tracerFactory) {
         FilterLoader filterLoader = FilterLoader.getInstance();
         FilterRegistry filterRegistry = FilterRegistry.instance();
         return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
      }

   }
//....
}

zuul提供的過濾器執(zhí)行順序如下:

[圖片上傳失敗...(image-71beae-1547796647002)]

zuul的關(guān)鍵邏輯在ZuulServlet中废境。

public class ZuulServlet extends HttpServlet {

    private ZuulRunner zuulRunner;

    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        //從初始化方法參數(shù)中畜挨,獲取是否緩存請求的配置
        String bufferReqsStr = config.getInitParameter("buffer-requests");
        boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true") ? true : false;
        //構(gòu)造ZuulRunner筒繁,初始化request & response,也是執(zhí)行filter的門面
        zuulRunner = new ZuulRunner(bufferReqs);
    }

    @Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            //初始化request巴元、response
            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 {
                //1.先執(zhí)行前置過濾器
                preRoute();
            } catch (ZuulException e) {
                //發(fā)生異常執(zhí)行error過濾器
                error(e);
                //執(zhí)行后置過濾器
                postRoute();
                return;
            }
            try {
                //2.執(zhí)行路由過濾器
                route();
            } catch (ZuulException e) {
                //發(fā)生異常執(zhí)行error過濾器
                error(e);
                //執(zhí)行后置過濾器
                postRoute();
                return;
            }
            try {
                //3.執(zhí)行后置過濾器
                postRoute();
            } catch (ZuulException e) {
                //發(fā)生異常執(zhí)行error過濾器
                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執(zhí)行后置過濾器
        zuulRunner.postRoute();
    }

    /**
     * executes "route" filters
     *
     * @throws ZuulException
     */
    void route() throws ZuulException {
        //通過ZuulRunner執(zhí)行路由過濾器
        zuulRunner.route();
    }

    /**
     * executes "pre" filters
     *
     * @throws ZuulException
     */
    void preRoute() throws ZuulException {
        //通過ZuulRunner執(zhí)行前綴過濾器
        zuulRunner.preRoute();
    }

    /**
     * initializes request
     *
     * @param servletRequest
     * @param servletResponse
     */
    void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
        //通過ZuulRunner執(zhí)行init方法毡咏,初始化request & response
        zuulRunner.init(servletRequest, servletResponse);
    }

    /**
     * sets error context info and executes "error" filters
     *
     * @param e
     */
    void error(ZuulException e) {
        //通過ZuulRunner執(zhí)行error過濾器
        RequestContext.getCurrentContext().setThrowable(e);
        zuulRunner.error();
    }
}    

先看下init()方法。

void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
    //直接調(diào)用了ZuulRunner的init()方法
    zuulRunner.init(servletRequest, servletResponse);
}

ZuulRunner的init()方法务冕。

public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
    //獲取請求上下文
    RequestContext ctx = RequestContext.getCurrentContext();
    if (bufferRequests) {
        //如果緩存請求內(nèi)容,包裝下request幻赚,再將包裝后的request放入請求上下文中禀忆,供其他類使用,例如過濾器
        ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
    } else {
        //將request放入請求上下文中落恼,供其他類使用箩退,例如過濾器
        ctx.setRequest(servletRequest);
    }
     //將response放入請求上下文中,供其他類使用佳谦,例如過濾器
    ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
}

再看下preRoute()方法戴涝。

/**
 * executes "pre" filters
 *
 * @throws ZuulException
 */
void preRoute() throws ZuulException {
    //通過ZuulRunner執(zhí)行前綴過濾器
    zuulRunner.preRoute();
}

ZuulRunner的preRoute()方法。

/**
 * executes "pre" filterType  ZuulFilters
 *
 * @throws ZuulException
 */
public void preRoute() throws ZuulException {
    //通過FilterProcessor執(zhí)行preRoute方法
    FilterProcessor.getInstance().preRoute();
}

FilterProcessor的preRoute()方法钻蔑。

/**
 * runs all "pre" filters. These filters are run before routing to the orgin.
 *
 * @throws ZuulException
 */
public void preRoute() throws ZuulException {
    //...
    runFilters("pre");
    //...
}

public Object runFilters(String sType) throws Throwable {
        //...
        //定義全局結(jié)果布爾值
        boolean bResult = false;
        //從FilterLoader中根據(jù)過濾器類型獲取該類型的所有zuulFilters
        List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
        if (list != null) {
            for (int i = 0; i < list.size(); i++) {
                //逐個獲取過濾器實例
                ZuulFilter zuulFilter = list.get(i);
                //執(zhí)行過濾器
                Object result = processZuulFilter(zuulFilter);
                if (result != null && result instanceof Boolean) {
                    bResult |= ((Boolean) result);
                }
            }
        }
        return bResult;
}

public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
        //獲取請求上下文
        RequestContext ctx = RequestContext.getCurrentContext();
        long execTime = 0;
        try {
            //記錄執(zhí)行過濾器時開始時間
            long ltime = System.currentTimeMillis();
            Object o = null;
            Throwable t = null;
            //執(zhí)行過濾器
            ZuulFilterResult result = filter.runFilter();
            //過濾器執(zhí)行結(jié)果狀態(tài)
            ExecutionStatus s = result.getStatus();
            //記錄執(zhí)行過濾器所耗時間
            execTime = System.currentTimeMillis() - ltime;

            switch (s) {
                case FAILED:
                    //如果過濾器執(zhí)行結(jié)果狀態(tài)是FAILED啥刻,獲取執(zhí)行時異常,向請求上下文中塞入執(zhí)行結(jié)果概要
                    t = result.getException();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                    break;
                case SUCCESS:
                    //如果過濾器執(zhí)行結(jié)果狀態(tài)是SUCCESS咪笑,獲取執(zhí)行結(jié)果可帽,向請求上下文中塞入執(zhí)行結(jié)果概要
                    o = result.getResult();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
                    
                    break;
                default:
                    break;
            }
            //如果執(zhí)行過濾器失敗,有異常窗怒,這里再拋出異常
            if (t != null) throw t;
            return o;
        } 
    //...
    }

接下來看如何執(zhí)行過濾器映跟,追蹤ZuulFilter的runFilter()方法。

public ZuulFilterResult runFilter() {
    //構(gòu)造zuul 過濾器執(zhí)行結(jié)果類
    ZuulFilterResult zr = new ZuulFilterResult();
    //先判斷過濾器是否關(guān)閉了
    if (!isFilterDisabled()) {
        //判斷是否要執(zhí)行過濾器扬虚,通過調(diào)用該過濾器的shouldFilter()方法
        if (shouldFilter()) {
            //....
            try {
                //調(diào)用該過濾器的run()方法
                Object res = run();
                //封裝run()方法執(zhí)行結(jié)果到過濾器執(zhí)行結(jié)果類中
                zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
            } catch (Throwable e) {
                //如果執(zhí)行run()方法異常了努隙,封裝過濾器執(zhí)行結(jié)果類
                zr = new ZuulFilterResult(ExecutionStatus.FAILED);
                zr.setException(e);
            }
            //....
        } else {
            //不執(zhí)行過濾器,封裝過濾器執(zhí)行結(jié)果類
            zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
        }
    }
    return zr;
}

在FilterProcessor的runFilters()方法中辜昵,有一行代碼值得說一下荸镊。

? List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);

這句代碼的意思是根據(jù)zuul的過濾器類型,"pre"堪置、"route"贷洲、"post"、"error"等獲取過濾器列表晋柱。

追蹤下FilterLoader的getFiltersByType()方法优构。

/**
 * Returns a list of filters by the filterType specified
 *
 * @param filterType
 * @return a List<ZuulFilter>
 */
public List<ZuulFilter> getFiltersByType(String filterType) {
    //這里hashFiltersByType是一個ConcurrentHashMap,key為filterType雁竞,value為zuul的所有filterType類型的filters
    //先從內(nèi)存緩存中獲取該過濾器類型的過濾器列表
    List<ZuulFilter> list = hashFiltersByType.get(filterType);
    if (list != null) return list;

    list = new ArrayList<ZuulFilter>();
    //如果內(nèi)存緩存中沒有這些過濾器list钦椭,再從FilterRegistry中獲取所有過濾器拧额,遍歷,獲取所有該filterType的filter list
    Collection<ZuulFilter> filters = filterRegistry.getAllFilters();
    for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) {
        ZuulFilter filter = iterator.next();
        if (filter.filterType().equals(filterType)) {
            list.add(filter);
        }
    }
    //按照filterOrder排序彪腔,從小到大
    Collections.sort(list); // sort by priority
    //從FilterRegistry中獲取的所有過濾器再放入內(nèi)存緩存中
    hashFiltersByType.putIfAbsent(filterType, list);
    return list;
}

FilterRegistr整個類比較簡單侥锦,這里直接列出這個類。

public class FilterRegistry {

    private static final FilterRegistry INSTANCE = new FilterRegistry();

    public static final FilterRegistry instance() {
        return INSTANCE;
    }

    private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>();

    private FilterRegistry() {
    }

    public ZuulFilter remove(String key) {
        return this.filters.remove(key);
    }

    public ZuulFilter get(String key) {
        return this.filters.get(key);
    }

    public void put(String key, ZuulFilter filter) {
        this.filters.putIfAbsent(key, filter);
    }

    public int size() {
        return this.filters.size();
    }

    public Collection<ZuulFilter> getAllFilters() {
        //可以看到getAllFilters()方法就是直接將這個類中的ConcurrentHashMap緩存的值返回出去了德挣。
        return this.filters.values();
    }

}

既然FilterRegistry中的這個ConcurrentHashMap有所有過濾器的數(shù)據(jù)恭垦,那么這個數(shù)據(jù)是什么時候放進去的呢?

我們看下FilterRegistry的put()方法在什么時候被調(diào)用格嗅。

追蹤到了ZuulFilterInitializer的contextInitialized()方法中番挺。前面說過,這個ZuulFilterInitializer會在zuul啟動時初始化屯掖,那么就在啟動的時候就已經(jīng)將FilterRegistry緩存的所有過濾器數(shù)據(jù)塞進去了玄柏。

@PostConstruct
public void contextInitialized() {
   log.info("Starting filter initializer");

   TracerFactory.initialize(tracerFactory);
   CounterFactory.initialize(counterFactory);

   for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
      filterRegistry.put(entry.getKey(), entry.getValue());
   }
}

FilterProcessor的route()、postRoute()贴铜、error()方法和preRoute()方法類似粪摘,都是調(diào)用runFilters()方法,傳入不同的filterType绍坝,這里不再贅述徘意。

總結(jié)一下:

  • ZuulProperties:zuul的配置類,yaml文件中配置的zuul的prefix轩褐、routes等都是映射到這個類中

  • ZuulProxyMarkerConfiguration:配置類映砖,引入了ZuulProxyAutoConfiguration配置。

  • ZuulProxyAutoConfiguration:引入了restClient灾挨、HTTPClient邑退、OKhttp等配置類,以及PreDecorationFilter劳澄、RibbonRoutingFilter等內(nèi)置過濾器地技,還有ProxyRequestHelper請求代理類等。

  • ZuulServerAutoConfiguration:配置了CompositeRouteLocator(組合的路由定向類)秒拔、SimpleRouteLocator(簡單的路由定向類)莫矗、聲明了ZuulController、ZuulHandlerMapping等砂缩,注冊了ZuulRefreshListener作谚、ServletRegistrationBean等,配置了ServletDetectionFilter庵芭、FormBodyWrapperFilter妹懒、SendResponseFilter、SendErrorFilter双吆、SendForwardFilter等filter眨唬。

  • ZuulFilterInitializer:監(jiān)聽整個zuul Filter的生命周期会前,初始化zuul的各種組件,包括過濾器等匾竿,并初始化了FilterRegistry、FilterLoader临庇,保存了各個類型的過濾器的列表昵慌。以及生命周期結(jié)束時時清除FilterRegistry假夺、FilterLoader的過濾器緩存數(shù)據(jù)。

  • ZuulServlet:zuul的1.X版本是依賴于servlet的侄泽,是zuul的所有訪問的入口蜻韭,包括對請求的過濾器執(zhí)行。

  • ZuulRunner:初始化request & response柿扣,以及將zuulServlet的調(diào)用轉(zhuǎn)發(fā)到FilterProcessor中去肖方。

  • FilterProcessor:執(zhí)行過濾器的核心類未状。

zuul 幾種過濾器類型執(zhí)行順序.png

能不能有更好的方式解決這個問題

待完善司草。

zuul存在的一些問題是什么

  • 基于多線程+阻塞IO的網(wǎng)絡(luò)模型,注定在面對大并發(fā)時處理能力相對較弱猜憎,不過2.X版本已經(jīng)重新設(shè)計了架構(gòu)搔课,采用netty作為底層網(wǎng)絡(luò)模型,并發(fā)連接數(shù)有了大幅度提升柬讨,性能也有了一定提升袍啡。
  • 權(quán)限認證、限流等需要自己從頭開發(fā)卖鲤,無開箱即用的框架或接口。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市区匣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌亏钩,老刑警劉巖莲绰,帶你破解...
    沈念sama閱讀 211,042評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛤签,死亡現(xiàn)場離奇詭異栅哀,居然都是意外死亡留拾,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評論 2 384
  • 文/潘曉璐 我一進店門沦偎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來豪嚎,“玉大人谈火,你說我怎么就攤上這事⊥螅” “怎么了啦租?”我有些...
    開封第一講書人閱讀 156,674評論 0 345
  • 文/不壞的土叔 我叫張陵荒揣,是天一觀的道長系任。 經(jīng)常有香客問我虐块,道長嘉蕾,這世上最難降的妖魔是什么错忱? 我笑而不...
    開封第一講書人閱讀 56,340評論 1 283
  • 正文 為了忘掉前任以清,我火速辦了婚禮,結(jié)果婚禮上眉孩,老公的妹妹穿的比我還像新娘勒葱。我一直安慰自己允乐,他們只是感情好舒裤,可當我...
    茶點故事閱讀 65,404評論 5 384
  • 文/花漫 我一把揭開白布荣德。 她就那樣靜靜地躺著瓦阐,像睡著了一般篷牌。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上戳杀,一...
    開封第一講書人閱讀 49,749評論 1 289
  • 那天信卡,我揣著相機與錄音题造,去河邊找鬼界赔。 笑死牵触,一個胖子當著我的面吹牛揽思,可吹牛的內(nèi)容都是我干的见擦。 我是一名探鬼主播,決...
    沈念sama閱讀 38,902評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼儡湾,長吁一口氣:“原來是場噩夢啊……” “哼徐钠!你這毒婦竟也來了役首?” 一聲冷哼從身側(cè)響起衡奥,我...
    開封第一講書人閱讀 37,662評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎失息,沒想到半個月后盹兢,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體守伸,經(jīng)...
    沈念sama閱讀 44,110評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡尼摹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年蠢涝,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片把鉴。...
    茶點故事閱讀 38,577評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡庭砍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出诗轻,到底是詐尸還是另有隱情揭北,我是刑警寧澤搔体,帶...
    沈念sama閱讀 34,258評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站劝术,受9級特大地震影響呆奕,放射性物質(zhì)發(fā)生泄漏梁钾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,848評論 3 312
  • 文/蒙蒙 一零酪、第九天 我趴在偏房一處隱蔽的房頂上張望蛾娶。 院中可真熱鬧潜秋,春花似錦胎许、人聲如沸辜窑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽方面。三九已至,卻和暖如春操禀,著一層夾襖步出監(jiān)牢的瞬間横腿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留罗侯,地道東北人歇父。 一個月前我還...
    沈念sama閱讀 46,271評論 2 360
  • 正文 我出身青樓榜苫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親媳荒。 傳聞我的和親對象是個殘疾皇子驹饺,可洞房花燭夜當晚...
    茶點故事閱讀 43,452評論 2 348

推薦閱讀更多精彩內(nèi)容