SpringCloud - 路由器和过滤器:ZUUL

参考:https://cloud.spring.io/spring-cloud-static/Edgware.SR5/multi/multi__router_and_filter_zuul.html

路由是微服务架构的组成部分。 例如,可以映射到Web应用程序,/api/users 映射到用户服务,/api/shop 映射到商店服务。 Zuul(https://github.com/Netflix/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的规则引擎基本上允许以任何JVM语言编写规则和过滤器,内置支持Java和Groovy。

注:

  • zuul.max.host.connections配置属性已取代两个新的属性,zuul.host.maxTotalConnections和zuul.host.maxPerRouteConnections,其缺省值分别200和20。
  • 所有路由的默认Hystrix的隔离模式(ExecutionIsolationStrategy)是SEMAPHORE。如果首选此隔离模式,则可以将zuul.ribbonIsolationStrategy更改为THREAD。

1. 如何添加Zuul
要在项目中添加Zuul,请使用group id为 org.springframework.cloud 和 artifact id为 spring-cloud-starter-netflix-zuul 的starter。

2. 嵌入式Zuul反向代理
Spring Cloud创建了一个嵌入式Zuul代理,以简化常见的用例开发,如UI应用程序想要代理对一个或多个后端服务的调用。此功能对于用户界面代理其所需的后端服务非常有用,从而避免了为所有后端服务独立管理CORS和身份验证问题。

要启用它,使用@EnableZuulProxy注释Spring Boot主类,并将本地调用转发到相应的服务。按照惯例,具有ID“users”的服务将从位于 /users 的代理接收请求(带有前缀剥离)。代理使用Ribbon来定位要通过发现转发的实例,并且所有请求都在hystrix命令中执行,因此故障将显示在Hystrix指标中,并且一旦出现断路,代理将不会尝试联系服务。

注:Zuul starter不包含发现客户端,因此对于基于服务ID的路由,还需要在类路径上提供一个服务发现(例如,Eureka是一种选择)。

要跳过自动添加服务,将 zuul.ignored-services 设置为服务ID模式列表。如果服务匹配被忽略的模式,但也包含在显式配置的路由映射中,那么它将是不被忽略的。例:

application.yml

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

在此示例中,除“users”外,将忽略所有服务。

要扩充或更改代理路由,可以添加如下所示的外部配置:

application.yml

zuul:
  routes:
    users: /myusers/**

这意味着对“/myusers”的http调用被转发到“users”服务(例如“/myusers/101”被转发到“/101”)。

要对路径进行更细粒度的控制,可以单独指定路径和serviceId:

application.yml。

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

这意味着对“/myusers”的http调用将转发到“users_service”服务。 路由必须有一个"path",可以指定为ant风格的模式,因此“/ myusers/*”只匹配一个级别,但“/myusers/**”按层次匹配。

后端的位置可以被指定为“serviceId”(来自发现的服务)或“url”(用于物理位置),例如:application.yml

 zuul:
  routes:
    users:
      path: /myusers/**
      url: http://example.com/users_service

这些简单的url-routes不会作为HystrixCommand执行,也不会使用Ribbon对多个URL进行负载均衡。 要实现此目的,可以使用静态服务器列表指定serviceId:

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

可以使用regexmapper在serviceId和路由之间提供约定。 它使用名为groups的正则表达式从serviceId中提取变量并将它们注入路由模式。

ApplicationConfiguration.java

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

这意味着serviceId “myusers-v1”将映射到路由“/v1/myusers/**”。 接受任何正则表达式,但所有命名组必须同时出现在servicePattern和routePattern中。 如果servicePattern与serviceId不匹配,则使用默认行为。 在上面的示例中,serviceId “myusers”将映射到路由“/myusers/**”(未检测到版本)默认情况下禁用此功能,仅适用于已发现的服务。

要为所有映射添加前缀,设置zuul.prefix,例如 /api 。 在默认情况下转发请求之前,会从请求中删除代理前缀(使用zuul.stripPrefix=false关闭此行为)。 也可以在各个路由中配置不删除代理前缀,例如:

application.yml

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

注:zuul.stripPrefix 仅适用于zuul.prefix中设置的前缀。它对给定路由的path属性定义的前缀没有任何影响。

在此示例中,对“/myusers/101”的请求将转发到“users”服务上的“/myusers/101”。

zuul.routes属性配置实际上绑定到 org.springframework.cloud.netflix.zuul.filters.ZuulProperties 上。如果查看该对象的属性,将看到它还具有"retryable"标志。将该标志设置为“true”以使Ribbon客户端自动重试失败的请求(如果需要,可以使用Ribbon客户端配置修改重试操作的参数)。

默认情况下,X-Forwarded-Host头会添加到转发的请求中。要关闭它,设置zuul.addProxyHeaders=false。默认情况下,前缀路径被剥离,对后端的请求会携带一个“X-Forwarded-Prefix”请求头(例如,上面示例中的“/myusers”)。

如果设置默认路由("/"),那么@EnableZuulProxy注释的应用程序可以充当独立服务器,例如 zuul.route.home: / 会将所有流量(即“/**”)路由到“home”服务。

如果需要更细粒度的忽略,则可以指定要忽略的特定模式。这些模式在路由定位过程开始时进行评估,这意味着前缀应包含在模式中以保证匹配。忽略的模式跨越所有服务并取代任何其他路由规则。

application.yml

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

这意味着所有诸如“/myusers/101”之类的请求将被转发到“users”服务上的“/101”, 但包括“/admin/”在内的请求将无法处理。

注:如果需要路由保留其次序,则需要使用YAML文件,因为使用属性配置文件将丢失次序。 例如:

application.yml

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

如果要使用属性文件,则 legacy 路径可能最终位于 users 路径前面,从而导致 users 路径无法访问。

3. Zuul Http客户端
zuul使用的默认HTTP客户端现在由Apache HTTP Client支持,而不是使用已经废弃的Ribbon RestClient。要使用RestClient或使用okhttp3.OkHttpClient,分别设置ribbon.restclient.enabled=true或ribbon.okhttp.enabled=true。如果要自定义Apache HTTP客户端或OK HTTP客户端,提供ClosableHttpClient或OkHttpClient类型的bean。

4. Cookie和敏感报头
在同一系统中的服务之间共享头部是可以的,但可能不希望敏感报头向下游泄漏到外部服务器。可以在路由配置中指定忽略的报头列表。 Cookie起着特殊的作用,因为它们在浏览器中具有明确定义的语义,并且它们总是被视为敏感的。如果代理的消费者是浏览器,那么下游服务的cookie也会给用户带来问题,因为它们都会混乱(所有下游服务看起来都来自同一个地方)。

如果对服务的设计非常小心,例如,如果只有一个下游服务设置了cookie,那么可以让它们从后端一直流到调用者。此外,如果代理设置了cookie并且所有后端服务都是同一系统的一部分,那么简单地共享它们就很自然(例如使用Spring Session将它们链接到某个共享状态)。除此之外,由下游服务设置的任何cookie对调用者来说可能不是很有用,因此建议(至少)将“Set-Cookie”和“Cookie”放入敏感报头中,这些报头用于不属于domain的路由。即使对于属于domain的路由,在允许cookie在它们与代理之间流动之前,请仔细考虑它的含义。

可以为每个路由配置敏感报头,以逗号分隔,例如,

application.yml

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

注:这是sensitiveHeaders的默认值,因此除非希望它不同,否则无需进行设置。这是Spring Cloud Netflix 1.1中的新功能(在1.0中,用户无法控制报头,所有Cookie都在两个方向上流动)。

sensitiveHeaders是黑名单,默认不为空,因此要使Zuul发送所有报头(“ignored”报头除外),必须将其明确设置为空列表。 如果要将cookie或authorization报头传递给后端,则必须执行此操作。 例:

application.yml

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

也可以通过设置zuul.sensitiveHeaders来全局设置敏感报头。如果在路由上设置了sensitiveHeaders,则会覆盖全局sensitiveHeaders设置。

5. 忽略报头
除了每个路由敏感报头之外,还可以为 zuul.ignoredHeaders 设置全局值,以便在与下游服务交互期间应丢弃的这些值(请求和响应)。默认情况下,如果Spring Security不在类路径上,则它们是空的,否则它们被初始化为Spring Security指定的一组“安全”报头(例如caching)。这种情况下的假设是,下游服务也可能添加这些报头,我们希望从代理获取这些值。如果Spring Security在类路径上,且不需要丢弃这些安全报头,可以将zuul.ignoreSecurityHeaders设置为false。如果在Spring Security中禁用了HTTP Security 响应报头并希望下游服务提供值,则此功能非常有用。

6. 管理端点
如果将@EnableZuulProxy与Spring Boot Actuator一起使用,将启用(默认情况下)两个额外的端点:

  • Routes
  • Filters

6.1 Routes端点
GET请求到 /routes 的路由端点将返回路由映射列表:

GET /routes

{
  /stores/**: "http://localhost:8081"
}

可以通过将 ?format = details 查询字符串添加到 /routes 来请求其他路由详细信息。 这将产生以下输出:

GET /routes?format=details

{
  "/stores/**": {
    "id": "stores",
    "fullPath": "/stores/**",
    "location": "http://localhost:8081",
    "path": "/**",
    "prefix": "/stores",
    "retryable": false,
    "customSensitiveHeaders": false,
    "prefixStripped": true
  }
}

POST请求将强制刷新现有路由(例如,如果服务目录中有更改)。 可以通过将endpoints.routes.enabled设置为false来禁用此端点。

注:路由应自动响应服务目录中的更改,但POST到 /routes 是一种强制立即更改的方法。

6.2 Filters端点
对于以GET方式请求到  /filters 的路由端点将按类型返回Zuul过滤器的映射。 对于映射中的每种过滤器类型,将找到该类型的所有过滤器及其详细信息的列表。

7. 扼杀模式和本地转发
迁移现有应用程序或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)。

注:忽略的模式不会被完全忽略,它们不会被代理处理(因此它们也可以在本地有效转发)。

8. 通过Zuul上传文件
如果使用@EnableZuulProxy,可以使用代理路径上传文件,只要文件很小,它就可以正常工作。对于大型文件,有一个替代路径绕过“/zuul/*”中的Spring DispatcherServlet(以避免多部件处理)。即如果zuul.routes.customers =/customers/**,那么可以将大文件POST到“/zuul/customers/*”。 servlet路径通过zuul.servletPath外部化。如果代理路由引导使用Ribbon负载均衡器,则极大文件也需要设置超时时间。

例如:application.yml

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000

请注意,要使用流式方式处理大型文件,需要在请求中使用分块编码(某些浏览器默认情况下不会这样做)。 例如在命令行上:

$ curl -v -H "Transfer-Encoding: chunked" \
    -F "[email protected]" localhost:9999/zuul/simple/file

9. 查询字符串编码
处理传入请求时,将对查询参数进行解码,以便在Zuul过滤器中进行修改时可以获取到它们。 然后在路由过滤器中构建后端请求时重新编码它们。 如果使用Javascript的encodeURIComponent()方法编码,结果可能与原始输入不同。 虽然这在大多数情况下不会引起任何问题,但某些Web服务器可能会因复杂查询字符串的编码而变得挑剔。

要强制查询字符串的原始编码,可以将特殊标志传递给ZuulProperties,以便使用HttpServletRequest::getQueryString方法获取查询字符串:

application.ym

 zuul:
  forceOriginalQueryStringEncoding: true

注意:此特殊标志仅适用于SimpleHostRoutingFilter,并且无法使用RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)轻松覆盖查询参数,因为查询字符串现在直接在原始HttpServletRequest上获取。

10. 直接嵌入Zuul
如果使用@EnableZuulServer(而不是@EnableZuulProxy),也可以在没有代理的情况下运行Zuul服务器,或者选择性地切换代理平台的各个部分。 与@EnableZuulProxy一样,添加到ZuulFilter类型的应用程序上的任何bean都将自动安装,但不会自动添加任何代理过滤器。

在这种情况下,仍然通过配置“zuul.routes.*”来指定进入Zuul服务器的路由,但是没有服务发现且没有代理,因此忽略“serviceId”和“url”设置。 例如:

application.yml。

zuul:
  routes:
    api: /api/**

将“/api/**”中的所有路径映射到Zuul过滤器链。

11. 禁用Zuul过滤器
Zuul for Spring Cloud在代理和服务器模式下都默认启用了许多ZuulFilter bean。 参考zuul filters包(org.springframework.cloud.netflix.zuul.filters.*)以获取已启用的过滤器。 如果要禁用一个,只需设置 zuul.<SimpleClassName>.<filterType>.disable=true。 按照惯例,filters后的包名是Zuul过滤器类型。 例如,要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter,设置zuul.SendResponseFilter.post.disable=true。

12. 为路由提供Hystrix回退
当Zuul中给定路径的链路中断时,可以通过创建ZuulFallbackProvider类型的bean来提供回退响应。 在此bean中,需要指定回退所针对的路由ID,并提供ClientHttpResponse作为回退返回。 这是一个非常简单的ZuulFallbackProvider实现。

class MyFallbackProvider implements ZuulFallbackProvider {
    @Override
    public String getRoute() {
        return "customers";
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        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:
  routes:
    customers: /customers/**

如果希望为所有路由提供默认回退,则可以创建ZuulFallbackProvider类型的bean并使getRoute方法返回*或null。

class MyFallbackProvider implements ZuulFallbackProvider {
    @Override
    public String getRoute() {
        return "*";
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        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;
            }
        };
    }
}

如果想根据失败原因选择响应,请使用FallbackProvider,它将取代未来版本中的ZuulFallbackProvder。

class MyFallbackProvider implements FallbackProvider {

    @Override
    public String getRoute() {
        return "*";
    }

    @Override
    public ClientHttpResponse fallbackResponse(final Throwable cause) {
        if (cause instanceof HystrixTimeoutException) {
            return response(HttpStatus.GATEWAY_TIMEOUT);
        } else {
            return fallbackResponse();
        }
    }

    @Override
    public ClientHttpResponse fallbackResponse() {
        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;
            }
        };
    }
}

13. Zuul超时
13.1 服务发现配置
如果Zuul正在使用服务发现,则需要关注两个超时,Hystrix超时(因为默认情况下所有路由都包含在Hystrix命令中)和Ribbon超时。 Hystrix超时需要考虑Ribbon读取和连接超时以及将为该服务发生的重试总次数。 默认情况下,Spring Cloud Zuul会尽力计算Hystrix超时,除非明确指定Hystrix超时。

Hystrix超时使用以下公式计算:

(ribbon.ConnectTimeout + ribbon.ReadTimeout) * (ribbon.MaxAutoRetries + 1) * (ribbon.MaxAutoRetriesNextServer + 1)

例如,如果在应用程序属性中设置以下属性

application.yml

ribbon:
  ReadTimeout:100
  ConnectTimeout:500
  MaxAutoRetries:1
  MaxAutoRetriesNextServer:1

然后Hystrix超时(对于这种情况下的所有路由)将设置为2400ms。

注:

  • 可以使用service.ribbon.*属性为各个路由配置Hystrix超时。
  • 如果您选择不配置上述属性,则将使用默认值,因此默认的Hystrix超时将设置为4000毫秒。

如果设置 hystrix.command.commandKey.execution.isolation.thread.timeoutInMilliseconds,其中commandKey是路由ID,或者设置hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds,无论ribbon.*属性怎么设置,这些值都将用于Hystrix超时。如果设置任何一个属性,则确保考虑好了Ribbon连接和读取超时以及可能发生的任何重试。

13.2 URL配置
如果通过指定URL配置了Zuul路由,则需要使用zuul.host.connect-timeout-millis和zuul.host.socket-timeout-millis。

14. 重写Location报头
如果Zuul面向Web应用程序,那么当Web应用程序通过3XX的http状态代码重定向时,可能需要重新编写Location报头,否则浏览器将最终重定向到Web应用程序的url而不是Zuul url 。可以将LocationRewriteFilter Zuul过滤器配置为将Location报头重新写入Zuul的url,它还会添加剥离的全局和路由特定前缀。可以通过Spring配置文件以下面方式添加过滤器:

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

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

但请谨慎使用此过滤器,过滤器作用于所有3XX响应代码的位置标头,这可能不适用于所有情况,例如,如果用户重定向到外部URL。

15. Zuul开发人员指南
有关Zuul如何工作的一般概述,请参阅Zuul Wiki(https://github.com/Netflix/zuul/wiki/How-it-Works)。

Zuul请求生命周期(官方文档):

15.1 Zuul Servlet
Zuul是作为Servlet实现的。对于一般情况,Zuul嵌入到Spring Dispatch机制中。这允许Spring MVC控制路由。在这种情况下,Zuul配置为缓冲请求。如果需要在没有缓冲请求的情况下通过Zuul(例如,上传大型文件),则Servlet也安装在Spring Dispatcher之外。默认情况下,它位于/zuul。可以使用zuul.servlet-path属性更改此路径。

15.2 Zuul RequestContext
为了在过滤器之间传递信息,Zuul使用RequestContext。它的数据保存在特定于每个请求的ThreadLocal中。在何处路由请求,错误以及实际的HttpServletRequest和HttpServletResponse的信息都存储在那里。 RequestContext扩展了ConcurrentHashMap,因此任何东西都可以存储在上下文中。 FilterConstants包含Spring Cloud Netflix安装过滤器使用的key(稍后将详细介绍)。

15.3 @EnableZuulProxy与@EnableZuulServer
Spring Cloud Netflix安装了许多过滤器,根据使用哪个注解来启用Zuul。 @EnableZuulProxy是@EnableZuulServer的超集。换句话说,@EnableZuulProxy包含@EnableZuulServer安装的所有过滤器。 “代理”中的过滤器启用了路由功能。如果你想要一个“空白”Zuul,你应该使用@EnableZuulServer。

15.4 @EnableZuulServer过滤器
创建一个SimpleRouteLocator,用于从Spring Boot配置文件加载路由定义。

安装了以下过滤器(与普通的Spring Bean一样):

pre过滤器:

  • ServletDetectionFilter:检测请求是否通过Spring Dispatcher。使用FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY键设置布尔值。
  • FormBodyWrapperFilter:解析表单数据并为下游请求重新编码。
  • DebugFilter:如果设置了debug request参数,则此过滤器将RequestContext.setDebugRouting()和RequestContext.setDebugRequest()设置为true。

Route过滤器:

  • SendForwardFilter:此过滤器使用Servlet RequestDispatcher转发请求。转发位置存储在RequestContext属性FilterConstants.FORWARD_TO_KEY中。这对于转发到当前应用程序中的端点非常有用。

Post过滤器:

  • SendResponseFilter:将代理请求的响应写入当前响应。

Error过滤器:

  • SendErrorFilter:如果RequestContext.getThrowable()不为null,则转发到/error(默认情况下)。可以通过设置error.path属性来更改默认转发路径(/error)。

15.5 @EnableZuulProxy过滤器
创建DiscoveryClientRouteLocator,用于从DiscoveryClient(如Eureka)以及属性加载路径定义。为DiscoveryClient中的每个serviceId创建一个路由。随着新服务的添加,路由将被刷新。

除了上述过滤器之外,还安装了以下过滤器(与普通的Spring Bean一样):

pre过滤器:

  • PreDecorationFilter:此过滤器根据提供的RouteLocator确定路由的位置和方式。它还为下游请求设置各种与代理相关的报头。

Route过滤器:

  • RibbonRoutingFilter:此过滤器使用Ribbon,Hystrix和可插拔的HTTP客户端发送请求。服务ID可在RequestContext属性FilterConstants.SERVICE_ID_KEY中找到。此过滤器可以使用不同的HTTP客户端。他们是:
  1. Apache HttpClient:这是默认客户端。
  2. Squareup OkHttpClient v3:这可以通过在类路径上使用com.squareup.okhttp3:okhttp库并设置ribbon.okhttp.enabled=true来实现。
  3. Netflix Ribbon HTTP客户端:这可以通过设置ribbon.restclient.enabled=true来启用。此客户端具有限制,例如它不支持PATCH方法,但有内置重试。
  • SimpleHostRoutingFilter:此过滤器通过Apache HttpClient将请求发送到预定的URL。 URL位于RequestContext.getRouteHost()中。

15.6 自定义Zuul过滤器示例
下面的大多数示包含在Sample Zuul Filters项目(https://github.com/spring-cloud-samples/sample-zuul-filters)。还有一些操作该存储库中的请求或响应主体的示例。

15.7 如何编写Pre过滤器
Pre过滤器用于在RequestContext中设置数据,以便在下游过滤器中使用。主要用例是设置路由过滤器所需的信息。

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("foo") != null) {
		    // put the serviceId in `RequestContext`
    		ctx.put(SERVICE_ID_KEY, request.getParameter("foo"));
    	}
        return null;
    }
}

上面的过滤器从foo请求参数填充SERVICE_ID_KEY。 实际上,进行这种直接映射并不是一个好主意,但应该从foo的值中查找服务ID。

现在已填充SERVICE_ID_KEY,PreDecorationFilter和RibbonRoutingFilter将不会运行。 如果想要转而使用完整的URL,请改为调用ctx.setRouteHost(url)。

要修改路由过滤器将转发到的路径,设置REQUEST_URI_KEY。

15.8 如何编写Route过滤器
Route过滤器在Pre过滤器之后运行,用于向其他服务发出请求。 这里的大部分工作是将请求和响应数据转换为客户端所需的模型。

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响应。 注:此过滤器可能存在bug,无法正常运行。

15.9 如何编写Post过滤器
Post过滤器通常会操纵响应。 在下面的过滤器中,我们添加一个随机UUID作为X-Foo标头。 其他操作(例如转换响应主体)要复杂得多且计算密集。

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-Foo", UUID.randomUUID().toString());
		return null;
	}
}

15.10 Zuul错误如何工作
如果在Zuul过滤器生命周期的任何部分期间抛出异常,则执行error过滤器。 仅当RequestContext.getThrowable()不为null时,才会运行SendErrorFilter。 然后,它在请求中设置特定的javax.servlet.error.*属性,并将请求转发到Spring Boot错误页面。

15.11 Zuul Eager应用程序上下文加载
Zuul内部使用Ribbon来调用远程URL,并且在第一次调用时,Spring Cloud会默认懒加载Ribbon客户端。 可以使用以下配置更改Zuul的此行为,将导致在应用程序启动时立即加载子Ribbon相关的应用程序上下文。

application.yml

zuul:
  ribbon:
    eager-load:
      enabled: true

猜你喜欢

转载自blog.csdn.net/mytt_10566/article/details/84780502
今日推荐