Netflix-Zuul网关说明文档

Netflix-Zuul

一. 简介

路由是微服务架构中的一个组成部分。
例如【/】可能是你映射到web应用的路径,【/api/user】可能是你映射到user服务的路径,【/api/shop】可能是
映射到shop服务的路径。
Zuul是一款出自于Netflix基于JVM的服务器端的负载均衡器。

Netflix在用Zuul做这些事情:

  • 身份验证(Authentication)
  • Insights
  • 压力测试(Stress Testing)
  • 金丝雀测试(Canary Testing)
  • 动态路由(Dynamic Routing)
  • 服务迁移(Service Migration)
  • 减载(Load Shedding)
  • 安全(Security)
  • 静态响应处理(Static Response handling)
  • 流量控制(Active/Active traffic management)

二. 集成Zuul反向代理

  • Spring Cloud 已经建立了Zuul代理,用来适用前端应用程序想要通过代理来访问一个或者多个后端服务。
    这个特点对于用户接口代理访问到后端服务非常有用。避免了跨域问题并且可以使认证独立于后端服务。

  • 如果要启用Zuul反向代理,可以在Spring Boot应用程序的主类中添加注解@EnableZuulProxy。 这样做
    可以使本地的调用请求路由到相应的服务。按照惯例,一个users服务从代理那里接收/users(使用前缀剥离服务)
    的请求Zuul代理使用Ribbon通过服务发现来定位具体的服务。所有的请求在hystrix command中执行,所以失败
    的请求可以在Hystrix监控中获取到。一旦断路器打开,Zuul代理将不会尝试连接该服务。

    注:Zuul starter 没有包含服务发现客户端,因此,为了实现基于服务id的路由,最好提供服务发现的客户端
    (Eureka是其中之一的选择)

  • 如果服务要跳过Zuul的反向代理,可在属性【zuul.ignored-services】中设置跳过的服务ID列表。
    如果某个服务和需要跳过的服务列表匹配,但是又明确的设置在了路由map中,该服务将不会跳过Zuul的代理。
    下述例子就是这样的效果:
    application.yml

      zuul:
        ignoredServices: '*'
        routes:
          users: /myusers/**
    

    上述例子中,所有的服务都将跳过Zuul的代理,除了users服务。

  • 如果要添加或者改变代理路由,可以将配置信息配置成如下所示:
    application.yml

      zuul:
        routes:
          users: /myusers/**
    

    上述配置意味着如果HTTP请求/myusers将会转发到users服务(例:/myusers/101将会转发到users服务的/101)

  • 如果想要通过路由获取到更好的粒度控制,则可以独立的指定path和serviceId,配置如下:
    application.yml

     zuul:
      routes:
        users:
          path: /myusers/**
          serviceId: users_service
    

    上述配置意味着如果HTTP请求/myusers将会转发到serviceId为users_service的服务中去。
    此场景中必须指定ant-style风格的格式。所以/myusers/*只匹配一级路径,/myusers/**可匹配多级路径。

  • 后台服务的位置可以通过serviceId(通过服务发现获取)指定,也可以通过url指定,配置如下:
    application.yml

      zuul:
        routes:
          users:
            path: /myusers/**
            url: http://example.com/users_service
    
  • 上述所有的这些路由例子都不会以HystrixCommand的方式执行,有多个url的时候也不会使用Ribbon做负载均衡。
    如果要实现这些目标,可以指定一个静态服务的list,配置如下:
    application.yml

      zuul:
        routes:
          echo:
            path: /myusers/**
            serviceId: myusers-service
            stripPrefix: true
      
      hystrix:
        command:
          myusers-service:
            execution:
              isolation:
                thread:
                  timeoutInMilliseconds: ...
      
      myusers-service:
        ribbon:
          NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
          listOfServers: http://example1.com,http://example2.com
          ConnectTimeout: 1000
          ReadTimeout: 3000
          MaxTotalHttpConnections: 500
          MaxConnectionsPerHost: 100
    

    另一种方式是指定一个服务路由并且为serviceId配置一个Ribbon客户端(这样做需要在Ribbon中禁止Eureka的支持),配置如下:
    application.yml

      zuul:
        routes:
          users:
            path: /myusers/**
            serviceId: users
      
      ribbon:
        eureka:
          enabled: false
      
      users:
        ribbon:
          listOfServers: example.com,google.com
    
  • 也可以使用正则表达式在serviceId和如路由之间指定一个规则。从serviceId中提取变量并且将它们注入到路由中,进而使用有规律的表达式来定义groups。例子如下:
    ApplicationConfiguration.java

      @Bean
      public PatternServiceRouteMapper serviceRouteMapper() {
          return new PatternServiceRouteMapper(
              "(?^.+)-(?v.+$)",
              "${version}/${name}");
      }
    

    上述例子意味着serviceId为myusers-v1的服务将指向地址/v1/myusers/**.任何正则表达式都可被接受,但是任何被命名的groups必须存在于servicePattern和routePattern中。如果servicePattern不能匹配serviceId,将使用默认的操作。上述例子中,myusers的serviceId映射到了/myusers/**的路由中(没有检测到版本号)。这个功能默认是禁用的,只有在服务被发现时才有这个功能。

  • 增加一个映射的前缀,可以设置zuul.prefix属性值,例如/api。默认情况下,Zuul在请求转发前会将这个前缀丢弃(可以通过设置zuul.stripPrefix=false来关掉这个功能)。也可以在个别的路由中特定服务中关掉丢弃映射前缀的功能。例如:
    application.yml

     zuul:
      routes:
        users:
          path: /myusers/**
          stripPrefix: false
    

    注:zuul.stripPrefix只有在设置了zuul.prefix时才会起作用。它不会对路由中设置的path产生任何影响。
    在上述例子中,请求/myusers/101将转发到users服务的/myusers/101路径中。

  • 事实上,zuul.routes中的属性会绑定到ZuulProperties的对象中。如果你去看那个对象的属性,里面有一个retryable的标志。如果设置那个标志为true,则Ribbon的客户端会自动重试失败的请求。当你需要修改Ribbon客户端的重试参数配置时你也可以将这个标记设置为true。
    默认情况下,X-Forward-Host头部属性会加到转发的请求中。如果要关闭这个属性,可设置zuul.addProxyHeaders=false.
    默认情况下,前缀路径在转发时将丢弃,并且后台请求中会增加X-Forward-Prefix头部属性(如上述例子中的/myusers)。

  • 如果设置默认的路由路径(/),被@EnableZuulProxy注解的应用程序将类似一个独立的服务器。例如,zuul.route.home:/将会路由home服务的/**路径。

  • 如果需要将忽略路径控制到更好的颗粒度,可以指定一些特别的pattern。这些pattern将会在处理路由位置开始时处理,这意味着前缀应该包含在pattenr中以保证匹配。忽略的模式跨越所有服务并取代任何其他路由规范。下面的例子将展示如何创建一个忽略pattern:
    application.yml

     zuul:
      ignoredPatterns: /**/admin/**
      routes:
        users: /myusers/**
    

    上面的例子意味着请求如/myusers/101将转发到users服务的/101路径中,然而如果请求路径中包含/admin/将不会被转发。

  • 如果你需要保证路由的顺序,你需要使用YAML文件。使用properties文件将丢失顺序访问的功能。下面的例子就是这样一个YAML文件:
    application.yml

       zuul:
        routes:
          users:
            path: /myusers/**
          legacy:
            path: /**  
    

    如果使用properties文件,legacy的路径有可能变成优先于users的路径处理,最后导致users服务不可达。

三. Zuul Http Client

Zuul默认使用的HTTP client是 Apache HTTP client而不是丢弃的Ribbon的RestClient。如果要使用RestClient或者okhttp3.OkHttpClient,分别设置 ribbon.restclient.enabled=true 或者 ribbon.okhttp.enabled=true即可。如果你想自定义Apache HTTP client或者OK HTTP client,则需要提供ClosableHttpClient或者OkHttpClient的bean类型。

四. Cookies和敏感头部信息

  • 在同一个系统的不同服务之间你可以共享头部信息,但是可能你不想将某些敏感头部信息泄露给下游的外部服务器。这种情况下可以指定忽略的头部信息作为路由配置的一部分。Cookies扮演了一个特殊的角色,因为它们能在浏览器中很好的定义语义,并且经常当做处理过的敏感信息使用。如果你的代理的消费者是浏览器,对于用户来说给下游服务使用的Cookies可能是个问题,因为它们都被搞得乱七八糟(所有的下游服务认为Cookies都是来自于一个地方)。

  • 如果你充分的设计了你的服务(比如,只有一个下游服务设置了Cookies),你也许可以使他们从后端服务一直流转到调用者。当然,如果你的代理设置了Cookies并且你所有的后端服务都是同一个系统的一部分,可以很自然和简单的共享他们(例如使用Spring Session让他们维持在共享的状态)。除此之外, 对于调用者来说,下游服务队任何cookie的get和set操作似乎都没有多大用处,所以推荐至少将Set-Cookie和Cookie放入属于域的一部分的路由的敏感头部中。即使该路由属于域的一部分,在让cookie流转与服务和代理之前,请仔细考虑那将意味着什么。

    每个路由的敏感头部可以通过逗号隔开字符串来设置,下面的例子就是这样一个YAML文件:
    application.yml

       zuul:
        routes:
          users:
            path: /myusers/**
            sensitiveHeaders: Cookie,Set-Cookie,Authorization
            url: https://downstream
    

    这个是sensitiveHeaders的默认值,你可以不需要设置它除非你想要一个和默认值不一样的值。
    这个在Spring Cloud Netflix 1.1中修改的。(在1.0中,用户无法控制头部,并且所有的cookies都流向两个方向。).

    sensitiveHeaders是黑名单,并且默认不为空。因此,如果要让Zuul发送所有的头部(除了忽略掉的),你必须指定设置一个空的列表。如果你想传递cookie或者authorization头部给你的后台服务这么做是必要的。下面的例子展示了如何使用sensitiveHeaders:
    application.yml

      zuul:
        routes:
          users:
            path: /myusers/**
            sensitiveHeaders:
            url: https://downstream
    

    你也可以通过zuul.sensitiveHeaders来设置sensitive headers。如果sensitiveHeaders设置在一个路由中,那么它将会覆盖全局的sensitiveHeaders设置。

五. 忽略头部

在附加在路由头部的敏感头部信息中,你可以设置一个全局属性叫zuul.ignoredHeaders,在和下游服务交互的时候为request和response丢弃敏感头部信息。默认情况下,如果spring Security 不在classpath中,这个属性值是空的。否则,会被初始化成大家都知道被spring security指定的安全头部。在某种情况下,假设下游服务也可以添加这些敏感头部,但是我们又需要从代理中获取这些敏感头部;当spring security在classpath中的情况下,为了不丢弃这些安全头部信息,可以设置zuul.ignoredHeadersfalse。如果您在Spring security 中禁用了HTTP安全响应头,并且希望下游服务提供值,那么这样做很有用。

六. 管理节点

默认情况下,如果你使用了@EnableZuulProxy注解,你就启用了以下两个附加的endpoints:

  • Routes
  • Filters

1. 路由节点(Routes Endpoint)

  • 使用/routes(GET)会返回被映射的路由列表:
    GET /routes
{
  /stores/**: "http://localhost:8081"
}
  • 其他的详细路由信息可以通过在GET /routes后增加?fromat=details参数来获取,可以获取到如下的详细信息:
    GET /routes/details
{
  "/stores/**": {
    "id": "stores",
    "fullPath": "/stores/**",
    "location": "http://localhost:8081",
    "path": "/**",
    "prefix": "/stores",
    "retryable": false,
    "customSensitiveHeaders": false,
    "prefixStripped": true
  }
}
  • 使用POST方式提交/routes会强制刷新存在的路由(比如说,当路由被改变的时候)。你可以使用以下配置禁用这项功能。
endpoints.routes.enabled=false

2. 过滤节点 (Filter Endpoint)

  • 用GET方式使用路径/filters请求filters的endpoint将会按类型一直返回带有详细信息的Zuul filters map, 在map中你可以按类型获取每个类型的filters列表。

七. 窒息模式和局部跳转(Strangulation patterns and Local forwards)

  • 当你需要迁移一个已经存在的应用或者API到原有的节点的时候,一般的处理方式是慢慢的用一种不同的实现方式慢慢替换它们。
    这种场景下,Zuul代理是一个很有用的工具,因为你可以使用它将客户端的一部分请求转发到原有的节点,而另外一部分请求转发到新的节点上。
    下面的例子展示了这种“窒息”模式的详细配置:
    application.yml
      zuul:
      routes:
          first:
            path: /first/**
            url: http://first.example.com
          second:
            path: /second/**
            url: forward:/second
          third:
            path: /third/**
            url: forward:/3rd
          legacy:
            path: /**
            url: http://legacy.example.com
    

上面的例子中,我们“窒息”了【legacy】应用,它映射了所有不满足的其他匹配规则的请求。

  • 路径/first/**被提取到了一个外部url的新的服务中。

  • 路径/second/**被转发到了本地服务中(比如,一个普通的Spring @RequestMapping)。

  • 路径/third/**也被转发到了一个有不同前缀的服务中 (例:/third/foo跳转到了/3rd/foo)。

    PS: 上述例子中被忽略的路径不是真的被忽略了,只是没有通过Zuul转发而已。(所以它们仍能跳转到本地服务中)

八. 通过Zuul上传文件

  • 如果你使用了@EnableZuulProxy注解,你也可以使用代理路径来上传文件,只要文件足够的小。
    大文件的话这里有一个可替代的路径可以绕过Spring 的 DispatcherServlet(避免multipart 处理)就是"/zuul/*"。也就是说,如果你配置了路由zuul.routes.customers=/customers/**, 你就可以提交大文件给/zuul/customers/*。servlet路径是取道zuul.servletPath通过的。如果代理如有通过一个Ribbon负载均衡器,非常大的文件必须提高timeout的设置,例子如下:
    application.yml
    hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
    ribbon:
      ConnectTimeout: 3000
      ReadTimeout: 60000
    
  • 注意,流式处理大文件,在请求中你需要块编码(chunked encoding)(有些浏览器不是默认这么处理的),例子如下:
    $ curl -v -H "Transfer-Encoding: chunked" \
      -F "[email protected]" localhost:9999/zuul/simple/file
    

九. 查询字符串编码

  • 当处理正在进来的请求时,查询参数(query params)将被解码,所以在Zuul filters中如果有需要才被修改。然后它们将被重新编码,在路由过滤器(route filters)中重建后端请求。如果是用Javascript的encodeURIComponent方法来编码,结果可能和原始的输入有所不同。在大多数情况下,这没有问题,不过有些web服务器对编码比较复杂的查询参数比较挑剔。

  • 强制执行查询字符串的原始编码,可以往ZuulProperties中设置一个特殊的标记,这样查询字符串就可以与HttpServletRequest::getQueryString方法一起使用。 例子如下:
    application.yml

    zuul:
    forceOriginalQueryStringEncoding: true
    

    PS:这个特殊标记只在SimpleHostRoutingFilter中有效果。因此你也丢失了使用RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)轻松重写查询参数的能力,因为查询字符串现在直接从原始的HttpServletRequest中获取。

十. 请求URI编码(Request URI Encoding)

  • 当处理正在进来的请求时,请求URI将在匹配路由前进行解码。此后将在路由过滤器(route filters)中重建后端请求的时候被重新编码。如果你的URI中包含"/"这将出现一些不可预知的行为。

  • 如果要使用原始的请求URI(request URI),可以往ZuulProperties中设置一个特殊的标记, 这样查询字符串就可以与HttpServletRequest::getRequstURI方法一起使用。 例子如下:
    application.yml

    zuul:
      decodeUrl: false
    

    PS:如果你使用RequestContext属性覆盖了请求URI,并且这个标记被设置成了false,请求内容中的url将不会被编码。确认URL已经被编码了将会是你的责任。

十一.平滑嵌入Zuul

  • 如果你使用@EnableZuulServer(而不是@EnableZuulProxy),你仍然能运行Zuul server,而无需代理或选择性地打开代理平台的某些部分。应用中的任何Zuul filter类型的将被自动装载(与@EnableZuulProxy相同),但没有任何代理过滤器将被自动装载。

  • 在这个例子中,装入Zuul server中欧冠的路由仍然使用"zuul.routes.*"配置指定,但是已经没有服务发现和代理。因此,“serviceId”和“url”配置将被忽略,以下示例将“/api/**”中的所有路径映射到Zuul的过滤器链路中:
    application.yml

     zuul:
      routes:
        api: /api/**
    

十二. 禁用Zuul Filters

  • Spring cloud版本的Zuul在proxy和server模式中自动启用了一些ZuulFilter。可以查看Zuul filters包从而得知你可以启用哪些filter。如果你想禁用某个Zuul filter,设置zuul...disable=true即可。

十三. 为路由提供Hystrix Fallbacks

  • 在Zuul的路由中服务链路被中断,你可以创建一个Fallbackprovider类型的bean来提供一个备用的响应(fallback response)。在这个bean中,你需要指定回退路由的id,并且提供一个ClientHttpResponse作为回退返回。下面的例子是一个比较简单的FallbackProvider的实现:

    class MyFallbackProvider implements FallbackProvider {
    
      @Override
      public String getRoute() {
          return "customers";
      }
    
      @Override
      public ClientHttpResponse fallbackResponse(String route, final Throwable cause) {
          if (cause instanceof HystrixTimeoutException) {
              return response(HttpStatus.GATEWAY_TIMEOUT);
          } else {
              return response(HttpStatus.INTERNAL_SERVER_ERROR);
          }
      }
    
      private ClientHttpResponse response(final HttpStatus status) {
          return new ClientHttpResponse() {
              @Override
              public HttpStatus getStatusCode() throws IOException {
                  return status;
              }
    
              @Override
              public int getRawStatusCode() throws IOException {
                  return status.value();
              }
    
              @Override
              public String getStatusText() throws IOException {
                  return status.getReasonPhrase();
              }
    
              @Override
              public void close() {
              }
    
              @Override
              public InputStream getBody() throws IOException {
                  return new ByteArrayInputStream("fallback".getBytes());
              }
    
              @Override
              public HttpHeaders getHeaders() {
                  HttpHeaders headers = new HttpHeaders();
                  headers.setContentType(MediaType.APPLICATION_JSON);
                  return headers;
              }
          };
      }
    }
    
  • 下面的例子将展示怎样的路由配置可以和上述例子匹配:

    zuul:
    routes:
      customers: /customers/**
    
  • 如果你想为所有的路由提供一个默认的fallback,你可以创建一个FallbackProvider类型的bean,并且在getRoute方法中返回*或者null, 如下所示:

    class MyFallbackProvider implements FallbackProvider {
      @Override
      public String getRoute() {
          return "*";
      }
    
      @Override
      public ClientHttpResponse fallbackResponse(String route, Throwable throwable) {
          return new ClientHttpResponse() {
              @Override
              public HttpStatus getStatusCode() throws IOException {
                  return HttpStatus.OK;
              }
    
              @Override
              public int getRawStatusCode() throws IOException {
                  return 200;
              }
    
              @Override
              public String getStatusText() throws IOException {
                  return "OK";
              }
    
              @Override
              public void close() {
    
              }
    
              @Override
              public InputStream getBody() throws IOException {
                  return new ByteArrayInputStream("fallback".getBytes());
              }
    
              @Override
              public HttpHeaders getHeaders() {
                  HttpHeaders headers = new HttpHeaders();
                  headers.setContentType(MediaType.APPLICATION_JSON);
                  return headers;
              }
          };
      }
    }
    

十四. Zuul 超时

如果你想配置通过Zuul的socket和代理请求的超时时间,你有两个选项可供配置:

  • 如果Zuul使用了服务发现,你需要配置Ribbon的ribbon.ReadTimeoutribbon.SocketTimeout属性。
  • 如果你配置使用Zuul的方式是指定url的方式 ,你需要配置zuul.host.connect-timeout-millisuul.host.socket-timeout-millis

十五. 重写Location头部

如果Zuul处理的是一个web应用程序,当web应用程序通过http状态码3XX跳转时,你可能需要重写Location头部信息。否则,浏览器将重定向到Zuul的Url而不是web应用程序的url了。你可以设置一个LocationRewriteFilter为Zuul的url重写Location头部。当然,它还添加了剥离的全局前缀和路由特定前缀。下面的例子展示了使用一个Spring Configuration文件来增加一个filter:

import org.springframework.cloud.netflix.zuul.filters.post.LocationRewriteFilter;
...

@Configuration
@EnableZuulProxy
public class ZuulConfig {
    @Bean
    public LocationRewriteFilter locationRewriteFilter() {
        return new LocationRewriteFilter();
    }
}

PS:请谨慎使用该filter,此filger作用在所有请求返回状态码为3XX的Location头部中,也许适用于所有的场景,比如重定向到外部的url中。

十六. 启用跨域请求

默认情况下,Zuul路由所有的跨域请求(CORS)到下游服务中。如果你想要Zuul处理特定的跨域请求,你可以使用自定义的WebMvcConfigurerbean:

@Bean
public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/path-1/**")
                 .allowedOrigins("http://allowed-origin.com")
                 .allowedMethods("GET", "POST");
       }
    };
}
  • 在上面的例子中,我们允许http://allowed-origin.com的请求使用GET或者POST方式发送到以/path-1/开头的服务节点中。你可以将你的CORS配置应用到具体的path表达式中,或者是为整个应用程序准备的全局配置,使用/**表达式。
    你可以自定义的属性有:
    • allowedOrigins
    • allowedMethods
    • allowedHeaders
    • exposedHeaders
    • allowCredentials
    • maxAge

十七. 指标监控

Zuul在路由请求时,将会为任何在请求时可能发生的失败提供监控指标。这些指标可以通过请求/actuator/metrics来获取。这些指标将会有名字和这样的格式:ZUUL::EXCEPTION:errorCause:statusCode.

十八. Zuul 开发者攻略

请查看Zuul Wiki

1. Zuul Servlet

Zuul被当做一个servlet来实现。在一般场景下,Zuul被嵌入到Spring的Dispatch机制。这让Spring MVC在路由的控制之下,在这个场景中,Zuul缓冲所有请求。如果需要Zuul不缓冲请求(比如,大文件上传),Servlet也可安装在Spring Dispather之外。默认情况下,selvet的地址是/zuul,这个路径可以使用zuul.servlet-path属性来修改。

2. Zuul RequestContext

Zuul使用RequestContext在过滤器间传递数据。数据保存在为每个request指定的ThreadLocal中。
有关路由请求的位置信息,errors和实际的HttpServletRequest和HttpServletResponse都保存在那里。RequestContext继承自ConCurrentHashMap,所以任何东西都可以保存在那里。FilterContants包含了被安装在SpringCloud Netflix中的所有过滤器的使用的keys(以后再谈这些)。

3. @EnableZuulProxy vs. @EnableZuulServer

Spring Cloud Netflix 安装了一些过滤器,依赖于启用Zuul的注解。@EnableZuulProxy是@EnableZuulServer的超集。也就是说,@EnableZuulProxy包含了所有被@EnableZuulServer安装的过滤器。在“proxy”的额外过滤器开启了路由功能。如果你想要一个“空白”的Zuul,你应该使用@EnableZuulServer。

4. @EnableZuulServer Filters

@EnableZuulServer 创建了一个从Spring Boot配置文件中加载路由定义的SimpleRouteLocator。

下面的过滤器被装载了(作为普通的Spring Beans):

  • 前置过滤器(Pre filters):

    • ServletDetectionFilter: 可检测任何通过Spring Dispatcher的请求。并为其设置一个key为FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY的boolean值。
    • FormBodyWrapperFilter: 为下游请求解析和重编码表单数据。
    • DebugFilter: 如果设置了调试请求的参数,设置equestContext.setDebugRouting()和 RequestContext.setDebugRequest()值为true。
  • 路由过滤器(Route filters):

    • SendForwardFilter: 使用Servlet RequestDispatcher转发请求。转发位置将保存在RequestContext的属性中,key为FilterConstants.FORWARD_TO_KEY。这对于当前应用转发的终端很有用。
  • 后置过滤器(Post filters):

    • SendResponseFilter: 从代理请求中写入响应内容到当前的响应中。
  • Error filters:

    • SendErrorFilter: 如果RequestContext.getThrowable()不为null,默认跳转到/error路由中,可以通过修改error.path属性来修改默认跳转的路径。

5. @EnabelZuulProxy Filters

创建一个DiscoveryClientRouteLocator用来从DiscoveryClient(比如说Eureka)以及properties文件中加载路由定义,这是一个为从DiscoveryClient的每个ServiceId创建的路由。如果新的服务加入进来,路由将会被刷新。

此外,为了让过滤器被更早的发现,装载了以下的过滤器(作为普通的Spring Beans):

  • 前置过滤器(Pre filters):

    • PreDecorationFilter:决定路由到哪里和怎么路由,依赖于被支持的RouteLocator。它当然也为下游的请求设置了各种各样的proxy-related headers。
  • 路由过滤器(Route filters):

    • RibbonRoutingFilter: 使用 Ribbon, Hystrix, 和 pluggable HTTP 客户端发送请求,Service IDs将会放入到RequestContext属性中,key为FilterConstants.SERVICE_ID_KEY。此过滤器可使用不同的HTTP客户端:
      • Apache HttpClient: 默认的客户端。
      • Squareup OkHttpClient v3: 在com.squareup.okhttp3:okhttp library包在classpath的情况下,通过设置ribbon.okhttp.enabled=true启用。
      • Netflix Ribbon HTTP client: 通过设置ribbon.restclient.enabled=true来启用,此client有使用限制,包括不支持PATCH HTTP method, 但是仍然有内置的重试机制。
    • SimpleHostRoutingFilter: 通过Apache HttpClient发送到预定的URLs,URLs可以在RequestContext.getRouteHost()中找到。

6. 自定义Zuul filter 实例

大部分的怎么编写过滤器的实例在Sample Zuul Filters工程中可以找到。
在这个工程中当然也可以找到操纵请求或者响应内容的实例。

这部分包含以下实例:

  • 怎么编写 Pre Filter:
    前置过滤器把数据装载在RequestContext中供下游的过滤器使用。主要的使用场景是为路由过滤器设置必须的信息。下面是一个Zuul前置过滤器的例子:

    public class QueryParamPreFilter extends ZuulFilter {
      @Override
      public int filterOrder() {
      	return PRE_DECORATION_FILTER_ORDER - 1; // run before PreDecoration
      }
    
      @Override
      public String filterType() {
      	return PRE_TYPE;
      }
    
      @Override
      public boolean shouldFilter() {
      	RequestContext ctx = RequestContext.getCurrentContext();
      	return !ctx.containsKey(FORWARD_TO_KEY) // a filter has already forwarded
      			&& !ctx.containsKey(SERVICE_ID_KEY); // a filter has already determined serviceId
      }
      @Override
      public Object run() {
          RequestContext ctx = RequestContext.getCurrentContext();
      	HttpServletRequest request = ctx.getRequest();
      	if (request.getParameter("sample") != null) {
      	    // put the serviceId in `RequestContext`
      		ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
      	}
          return null;
      }
    }
    

    上述过滤器从request参数中填充SERVICE_ID_KEY。在实际应用中,你不应该做这种直接的映射,Service ID应该从sample中查找。
    现在SERVICE_ID_KEY被修改了,PreDecorationFilter将不会执行,RibbonRoutingFilter将会被执行。
    PS:如果你想路由到全路径URL,可以调用ctx.setRouteHost(url)。想要修改路由过滤器的跳转,可以设置REQUEST_URI_KEY的值。

  • 怎么编写 Route Filter:
    路由过滤器在前置过滤器执行之后执行,并且将请求转发到相应的服务中。这里的大部分工作是将请求和响应数据装换为客户端要求的模型。下面是一个Zuul路由过滤器的例子:

    public class OkHttpRoutingFilter extends ZuulFilter {
      @Autowired
      private ProxyRequestHelper helper;
    
      @Override
      public String filterType() {
      	return ROUTE_TYPE;
      }
    
      @Override
      public int filterOrder() {
      	return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;
      }
    
      @Override
      public boolean shouldFilter() {
      	return RequestContext.getCurrentContext().getRouteHost() != null
      			&& RequestContext.getCurrentContext().sendZuulResponse();
      }
    
      @Override
      public Object run() {
      	OkHttpClient httpClient = new OkHttpClient.Builder()
      			// customize
      			.build();
    
      	RequestContext context = RequestContext.getCurrentContext();
      	HttpServletRequest request = context.getRequest();
    
      	String method = request.getMethod();
    
      	String uri = this.helper.buildZuulRequestURI(request);
    
      	Headers.Builder headers = new Headers.Builder();
      	Enumeration<String> headerNames = request.getHeaderNames();
      	while (headerNames.hasMoreElements()) {
      		String name = headerNames.nextElement();
      		Enumeration<String> values = request.getHeaders(name);
    
      		while (values.hasMoreElements()) {
      			String value = values.nextElement();
      			headers.add(name, value);
      		}
      	}
    
      	InputStream inputStream = request.getInputStream();
    
      	RequestBody requestBody = null;
      	if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
      		MediaType mediaType = null;
      		if (headers.get("Content-Type") != null) {
      			mediaType = MediaType.parse(headers.get("Content-Type"));
      		}
      		requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));
      	}
    
      	Request.Builder builder = new Request.Builder()
      			.headers(headers.build())
      			.url(uri)
      			.method(method, requestBody);
    
      	Response response = httpClient.newCall(builder.build()).execute();
    
      	LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();
    
      	for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
      		responseHeaders.put(entry.getKey(), entry.getValue());
      	}
    
      	this.helper.setResponse(response.code(), response.body().byteStream(),
      			responseHeaders);
      	context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
      	return null;
      }
    }
    

    上述过滤器将Servlet请求信息转换成OkHttp3的请求信息,执行HTTP请求,并且将OKHttp3的响应信息转换成Servlet的响应信息。

  • 怎么编写 Post Filter:
    后置过滤器一般操作请求的响应信息。下面的例子在请求头部信息中增加了一个UUID作为X-Sample头部值:

    public class AddResponseHeaderFilter extends ZuulFilter {
      @Override
      public String filterType() {
      	return POST_TYPE;
      }
    
      @Override
      public int filterOrder() {
      	return SEND_RESPONSE_FILTER_ORDER - 1;
      }
    
      @Override
      public boolean shouldFilter() {
      	return true;
      }
    
      @Override
      public Object run() {
      	RequestContext context = RequestContext.getCurrentContext();
      	HttpServletResponse servletResponse = context.getResponse();
      	servletResponse.addHeader("X-Sample", UUID.randomUUID().toString());
      	return null;
      }
    }
    

    PS:其他的操作,比如转换和加工请求响应内容,要复杂的多,计算量也大得多。

7. Zuul的Error是怎么工作的

如果一个异常在Zuul过滤器的任何生命周期内被抛出,error过滤器将被执行。SendErrorFilter只有在RequestContext.getThrowable()不会null的时候才会被执行,这时将会在设置指定的javax.servlet.error.*属性到请求中,并且将请求转发到SpringBoot的error page中。

8. Zuul应用程序上下文加载

Zuul内部使用Ribbon调用远程的URLs。默认情况下,SpringCloud第一次调用的Ribbon客户端的时候使用懒加载的方式。可以通过设置以下的配置来修改这种行为,配置之后的结果就是在程序启动时就会加载相关的Ribbon上下文:

zuul:
  ribbon:
    eager-load:
      enabled: true

参考文档:

  1. 官方文档:
    https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-netflix.html

你可能感兴趣的:(java,spring,spring-cloud,Zuul,网关)