服务配置

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

前面的示例意味着HTTP调用将/myusers转发到该users_service服务。路由必须具有path可以指定为Ant风格模式的,因此/myusers/*只能匹配一个级别,但/myusers/**可以分层匹配。

后端的位置可以指定为serviceId(用于发现服务)或url(物理位置),如以下示例所示:

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

简单的url路由不会作为HystrixCommand执行,也不会通过Ribbon来负载均衡多个URL。为了实现这些目标,您可以指定一个serviceId带有静态服务器列表的,如下所示:

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: https://example1.com,http://example2.com
    ConnectTimeout: 1000
    ReadTimeout: 3000
    MaxTotalHttpConnections: 500
    MaxConnectionsPerHost: 100

另一种方法是指定服务路由并配置Ribbon客户端serviceId(这样做需要在Ribbon中禁用Eureka支持-有关更多信息,请参见上文),如以下示例所示:

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

ribbon:
  eureka:
    enabled: false

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

您可以使用regexmapper来提供serviceId和routers之间的约定。它使用正则表达式命名组从中提取变量serviceId并将其注入到路由模式中,如以下示例所示:

ApplicationConfiguration.java

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

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

要为所有映射添加前缀,请设置zuul.prefix一个值,例如/api。默认情况下,代理前缀会从请求中剥离,然后再转发请求(您可以使用来关闭此行为zuul.stripPrefix=false)。您还可以关闭从单个路由中剥离特定于服务的前缀,如以下示例所示:

application.yml

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

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

在前面的例子中,请求到/myusers/101转发到/myusers/101users服务。

这些zuul.routes条目实际上绑定到类型为的对象ZuulProperties。如果查看该对象的属性,则可以看到它也有一个retryable标志。设置该标志以true使功能区客户端自动重试失败的请求。您还可以将该标志设置为何true时需要修改使用功能区客户端配置的重试操作的参数。

默认情况下,X-Forwarded-Host标头被添加到转发的请求中。要关闭它,请设置zuul.addProxyHeaders = false。默认情况下,前缀路径被剥离,并且到后端的请求选择一个X-Forwarded-Prefix标头(/myusers在前面显示的示例中)。

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

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

application.yml

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

前面的示例意味着所有呼叫(例如/myusers/101)都被转发到/101users服务上。但是,包括在内的呼叫/admin/无法解决。

如果您需要保留路线的顺序,则需要使用YAML文件,因为使用属性文件时顺序会丢失。以下示例显示了这样的YAML文件:

application.yml

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

如果要使用属性文件,则该legacy路径可能会终止于该users 路径的前面,从而导致该users路径不可访问。

Http 客户端选择

Zuul的默认HTTP客户端是Apache HTTP客户端而不是已经过时的Ribbon的RestClient。如果要使用RestClient或者okhttp3.OkHttpClient,可以设置ribbon.restclient.enabled=true 或者 ribbon.okhttp.enabled=true。

如果你想自定义Apache HTTP client 或者 OK HTTP client,那么需要提供一个ClosableHttpClient 或者 OkHttpClient类型的bean。

Cookies and Sensitive Headers(cookies和敏感头部)

您可以在同一系统中的服务之间共享标头,但您可能不希望敏感标头泄漏到下游到外部服务器中。您可以在路由配置中指定忽略的标头列表。Cookies发挥着特殊的作用,因为它们在浏览器中具有定义明确的语义,并且始终将它们视为敏感内容。如果代理的使用者是浏览器,那么下游服务的cookie也会给用户带来麻烦,因为它们都混杂在一起(所有下游服务看起来都来自同一位置)。

如果您对服务的设计很谨慎(例如,如果只有一个下游服务设置cookie),则可以让它们从后端一直流到调用者。另外,如果您的代理设置cookie,并且所有后端服务都属于同一系统,则自然可以简单地共享它们(例如,使用Spring Session将它们链接到某些共享状态)。除此之外,由下游服务设置的任何cookie可能对调用者都没有用,因此建议您(至少)将Set-CookieCookie放入不属于您域的路由的敏感标头中。即使对于属于您网域的路由,在让Cookie在它们和代理之间流动之前,也应仔细考虑其含义。

可以将敏感头配置为每个路由的逗号分隔列表,如以下示例所示

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

注意:上面的例子是sensitiveHeaders的默认值,这是在Spring Cloud Netflix 1.1版本新增的功能。(在1.0中,不能设置头部并且所有cookie双向流动)。

sensitiveHeaders是一个黑名单,默认不为空。因此要让Zuul发送所有头部的话,需要明确指定sensitiveHeaders为空。如下:

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

您还可以通过设置设置敏感的标题zuul.sensitiveHeaders。如果sensitiveHeaders在路径上设置了,则它将覆盖全局sensitiveHeaders设置。

忽略头部

除了路由敏感头部以外,你还可以设置zuul.ignoredHeaders成那些在与下游服务交互时应该剔除的值。默认,Spring Security不在classpath上时,这些值是空的。否则他们被Spring Security初始化为一些常见的“安全”头部。

在这种情况下,假设下游服务也可以添加这些标头,但是我们需要来自代理的值。要在Spring Security位于类路径上时不丢弃这些众所周知的安全标头,可以将设置zuul.ignoreSecurityHeadersfalse。如果您在Spring Security中禁用了HTTP Security响应标头并需要下游服务提供的值,则这样做很有用。

管理端点

如果你同时使用@EnableZuulProxy 和Spring Boot Actuator,那么将会开启两个额外的端点:

  • Routes
  • Filters

Routes Endpoint(路由端点)

到路由端点的GET/routes返回已映射路由的列表:

GET /routes

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

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

GET /routes/details

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

/routes的POST方法将会强制刷新路由信息。

可以通过endpoints.routes.enabled=false来禁用这个端点。

注意:路由会自动根据服务目录的改动来更新,但是POST请求/routes是一种立即强制更新的方法。

Filters Endpoint(过滤器端点)

/filters 的GET请求将会返回过滤器类型列表。

压缩模式和本地转发

当迁移一个老的应用或者API时,需要慢慢把它的访问端点替换成新的实现。Zuul会是一个很有用的代理,因为你可以使用它处理所有来自客户端老的端点的流量并且重定向一些请求到新的实现上。如下:

 zuul:
  routes:
    first:
      path: /first/**
      url: https://first.example.com
    second:
      path: /second/**
      url: forward:/second
    third:
      path: /third/**
      url: forward:/3rd
    legacy:
      path: /**
      url: https://legacy.example.com

其中,forward:开头的url将会转发到本地。

通过Zuul上传文件

如果你使用@EnableZuulProxy,那么可以通过代理路径来上传一些小文件,对于大文件有一个可以绕过Spring DispatcherServlet的路径“/zuul/*”,换句话说,如果zuul.routes.customers=/customers/*,那么可以

通过发送POST请求到/zuul/customers/*。servlet路径是通过zuul.servletPath外部化的。如果代理路由使用Ribbon,尤其大文件需要提高超时时间。如下

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

请注意,要使流传输处理大文件,您需要在请求中使用分块编码(某些浏览器默认不这样做),如以下示例所示:

$ curl -v -H "Transfer-Encoding: chunked" \
    -F "file=@mylarge.iso" localhost:9999/zuul/simple/file

查询字段编码

当处理请求时,查询字段将会被解码,这样就可以在Zuul过滤器中修改他们。然后在过滤器中再重新编码后发送请求给后端。如果使用Javascrip的encodeURIComponent()方法,那么结果可能会和原始输入不同。这在大多数情况下不会有问题,但是一些web服务器对于复杂查询字段的编码要求还是很挑剔的。

为了强制查询字符串的原始编码,可以向ZuulProperties传递一个特殊的标志,以便将查询字符串作为HttpServletRequest::getQueryString方法使用。

 zuul:
  forceOriginalQueryStringEncoding: true

注意:这个特殊的标志只对SimpleHostRoutingFilter有效,另外可以通过RequestContext.getCurrentContext().setRequestQueryParams(someOverriddenParameters)来覆盖查询字符串。

请求URI编码

在处理传入请求时,在将请求URI与路由匹配之前,先对请求URI进行解码。然后在路由过滤器中重建后端请求时,将对请求URI进行重新编码。如果您的URI包含编码的“ /”字符,则可能导致某些意外行为。

要使用原始请求URI,可以向’ZuulProperties’传递一个特殊标志,以便该URI与该HttpServletRequest::getRequestURI方法一样被使用,如以下示例所示:

 zuul:
  decodeUrl: false

如果使用requestURIRequestContext属性覆盖请求URI,并且此标志设置为false,则将不对在请求上下文中设置的URL进行编码。确保URL已被编码是您的责任。

禁用Zuul过滤器

在代理和服务器模式,spring cloud Zuul都会默认注册一些ZuulFilter。可以通过zuul...disable=true来禁用指定的过滤器。

按照惯例,包名中filters的后面就是filterType。例如,如果要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter,可以设置zuul.SendResponseFilter.post.disable=true。

为路由提供Hystrix降级服务

当Zuul的路由回路出现问题时,可以通过一个FallbackProvider类型的bean来提供降级服务。在这个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/**

如果要对所有路由提供一个默认降级服务,可以创建一个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超时时间和读取超时时间,你有两个选项,基于配置:

1)如果Zuul使用服务发现,则配置ribbon.ReadTimeout和ribbon.SocketTimeout;

2)如果路由是通过URL指定的,那么需要配置zuul.host.connect-timeout-millis和zuul.host.socket-timeout-millis

重写头部Location字段

如果Zuul在一个web应用前面,那么你需要重写Location头部当你的web应用通过HTTP状态码3XX重定向。否则,浏览器会重定向到web应用的URL而不是Zuul的URL。

可以通过配置一个LocationRewriteFilter类型的Zuul过滤器来重写Location头部到Zuul的URL。它还恢复了删除的全局前缀和特定于路由的前缀。如下

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

注意:要非常小心使用这个过滤器,因为它会作用于所有响应码为3XX的Location头部,这可能在某些场合不适合。比如要重定向到一个外部地址。

跨域请求

默认情况下,Zuul将所有跨源请求(CORS)路由到服务。如果您想让Zuul处理这些请求,可以通过提供自定义WebMvcConfigurerbean来完成

@Bean
public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
        public void addCorsMappings(CorsRegistry registry) {
            registry.addMapping("/path-1/**")
                    .allowedOrigins("https://allowed-origin.com")
                    .allowedMethods("GET", "POST");
        }
    };
}

在上面的示例中,我们允许GETPOST方法从allowed-origin.com将跨域请求发送到以开头的端点path-1。您可以使用/**映射将CORS配置应用于特定的路径模式,也可以将其整体应用于整个应用程序。您可以自定义属性:allowedOriginsallowedMethodsallowedHeadersexposedHeadersallowCredentialsmaxAge通过此配置。

度量指标

Zuul在Actuator metrics端点下提供metrics,当路由请求出现失败时。可以通过/actuator/metrics端点查看。metrics名称的格式为ZUUL::EXCEPTION:errorCause:statusCode。

限流

https://www.baeldung.com/spring-cloud-zuul-rate-limit

<dependency>
    <groupId>com.marcosbarbero.cloud</groupId>
    <artifactId>spring-cloud-zuul-ratelimit</artifactId>
    <version>2.2.0.RELEASE</version>
</dependency>
zuul:
  routes:
    serviceSimple:
      path: /greeting/simple
      url: forward:/
    serviceAdvanced:
      path: /greeting/advanced
      url: forward:/
  ratelimit:
    enabled: true
    repository: JPA
    policy-list:
      serviceSimple:
        - limit: 5
          refresh-interval: 60
          type:
            - origin
      serviceAdvanced:
        - limit: 1
          refresh-interval: 2
          type:
            - origin
  strip-prefix: true

serviceSimple 端点增加了每60秒5个请求的速率限制。相反, serviceAdvanced的速率限制为每2秒1个请求

The type configuration specifies which rate limit approach we want to follow. Here are the possible values:

  • origin – rate limit based on the user origin request
  • url – rate limit based on the request path of the downstream service
  • user – rate limit based on the authenticated username or ‘anonymous’
  • No value – acts as a global configuration per service. To use this approach just don’t set param ‘type’

在ZuulFilter中修改请求和相应

在ZuulFilter中修改请求Resquest

public class CustomZuulFilter extends ZuulFilter {

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        ctx.addZuulRequestHeader("Test", "TestSample");
        return null;
    }

    @Override
    public boolean shouldFilter() {
       return true;
    }
    // ...
}

下游服务取值

@RestController
public class FooController {

    @GetMapping("/foos/{id}")
    public Foo findById(
      @PathVariable long id, HttpServletRequest req, HttpServletResponse res) {
        if (req.getHeader("Test") != null) {
            res.addHeader("Test", req.getHeader("Test"));
        }
		...
    }
}

在ZuulFilter中修改响应Response

public class ResponseFilter extends ZuulFilter {  
    @Override
    public String filterType() {
        return POST_TYPE;
    }
    @Override
    public int filterOrder() {
        return 0;
    }
    @Override
    public boolean shouldFilter() {
        return true;
    }
    @Override
    public Object run() throws ZuulException {
          RequestContext context = RequestContext.getCurrentContext();
  		  try (final InputStream responseDataStream = context.getResponseDataStream()) {
     		   if(responseDataStream == null) {
        		    logger.info("BODY: {}", "");
         		   return null;
       			 }
     		 String responseData = CharStreams.toString(new InputStreamReader(responseDataStream, "UTF-8"));
       		 logger.info("BODY: {}", responseData);
       		 context.setResponseBody(responseData);//重点 将响应主体添加回上下文中
   		  }  catch (Exception e) {
      		  throw new ZuulException(e, INTERNAL_SERVER_ERROR.value(), e.getMessage());
  		  }
        return null;
    }
}

我们使用context.setResponseBody(responseData)将响应主体添加回上下文中进行处理

zuul开发指南

Zuul Servlet

Zuul的实现是一个Servlet。通常情况下,Zuul是嵌入到Spring分发机制中的。Spring MVC会掌控路由。这种情况下,Zuul会缓存请求。如果有一种情况是穿过Zuul但是不要缓存(例如大文件的上传),这时可以使用一种独立于Spring分发器的外部Servlet。默认情况,这个Servlet的地址是/zuul。也可以通过zuul.servlet-path属性来修改。

Zuul RequestContext

Zuul使用RequestContext在不同的过滤器中传递信息。它的数据保存在特定于每个请求的ThreadLocal中.它存储的信息有:路由请求到何处,错误,HttpServletRequest 和 HttpServletResponse。

RequestContext继承ConcurrentHashMap,所以它可以存储任何信息。FilterConstants保存了那些被过滤器使用的key。

@EnableZuulProxy vs. @EnableZuulServer

Spring Cloud Netflix安装了一系列过滤器,安装了哪些过滤器依赖于你使用哪种注解来开启Zuul的。@EnableZuulProxy是@EnableZuulServer的超集。换句话说,@EnableZuulProxy包含了@EnableZuulServer中的过滤器。

在“proxy”模式中的额外过滤器开启了路由功能。所以如果想要一个“空白”的Zuul,就使用@EnableZuulServer。

@EnableZuulServer Filters

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

安装的过滤器(作为普通的spring bean):

1)Pre filters:

ServletDetectionFilter:检测请求是否是通过Spring Dispatcher,设置FilterConstants.IS_DISPATCHER_SERVLET_REQUEST_KEY的布尔值。

FormBodyWrapperFilter:解析表单数据,并且为下游请求重新编码这些数据。

DebugFilter:如果请求参数中设置了debug,则RequestContext.setDebugRouting()和RequestContext.setDebugRequest()都设置为true。

2)Route filters:

SendForwardFilter:使用RequestDispatcher转发请求。转发地址存储在RequestContext的FilterConstants.FORWARD_TO_KEY属性中。

3)Post filters:

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

4)Error filters:

SendErrorFilter:如果RequestContext.getThrowable()不是null,则转发到/error(默认)。也可以error.path设置转发路径。

@EnableZuulProxy Filters

创建一个 DiscoveryClientRouteLocator(从DiscoveryClient(例如Eureka)和配置文件中加载路由定义)。为每个服务发现客户端中的serviceId都会创建一个路由。当有新服务添加时,路由就会刷新。

除了上面的过滤器外,还有额外的过滤器:

1)Pre filters:

PreDecorationFilter:根据提供的RouteLocator来决定如何路由,并且路由到何处。并且为下游服务设置了一些代理相关的头部。

2)Route filters:

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 client:设置ribbon.restclient.enabled=true。但是这个客户端有一些限制,它不支持PATCH方法,但是有内建的重试机制。

SimpleHostRoutingFilter:通过Apache HttpClient向预定的url发送请求。URL在RequestContext.getRouteHost()中。

自定义Zuul过滤器示例

下面的大多数“如何编写”示例都包含在示例Zuul过滤器项目中。在该存储库中也有一些处理请求或响应正文的示例。

本节包括以下示例:

Pre filters为在RequestContext中设置数据,给下游的过滤器使用。主要用途就设置一些route过滤器需要的信息。如下:

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

上面的过滤器使用sample请求参数填充SERVICE_ID_KEY。实际上不应该做这种直接映射,Service ID应该从sample的值中查找。

现在,SERVICE_ID_KEY已经被填充,所以PreDecorationFilter将不会执行,RibbonRoutingFilter会执行。

注意:如果想路由到一个完整的URL,调用ctx.setRouteHost(url)。

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

Route filters在pre filters后执行。它转发请求到其他服务。这里的大部分工作是将请求和响应数据转换到客户机所需的模型。

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 filters主要是用来修改响应。下面的例子中在响应头中添加一个X-Sample头部并且设置为UUID。

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

注意:其他操作,比如转换响应体,则要复杂得多,计算量也大得多。

Zuul错误处理

Zuul过滤器的生命周期的任何阶段出现异常,error过滤器将会执行。当RequestContext.getThrowable()不为null时,SendErrorFilter将会执行。它然后在请求中设置javax.servlet.error.*属性,

然后将请求转发到spring boot的错误页面。

Zuul Eager应用程序上下文加载

Zuul在内部使用Ribbon来调用远程URL。默认情况下,Ribbon客户端在第一次调用时被Spring Cloud延迟加载。通过使用以下配置,可以为Zuul更改此行为,这将导致在应用程序启动时急于加载与子Ribbon相关的应用程序上下文。以下示例显示了如何启用即时加载

zuul:
  ribbon:
    eager-load:
      enabled: true

重试失败请求

Spring Cloud Netflix提供了许多发送HTTP请求的方法。你可以使用RestTemplate, Ribbon, 或者 Feign。无论怎么选择创建HTTP请求,都有可能请求失败。

当请求失败时,你可能想要请求自动重试。当使用Sping Cloud Netflix时,你需要添加Spring Retry到classpath上。这样RestTemplates, Feign, 和 Zuul会在请求失败时

自动重试。

补偿政策

默认,在使用重试机制时是没有补偿政策的。如果你想配置一个补偿政策,则需要创建一个LoadBalancedBackOffPolicyFactory类型的bean。它会为指定的服务创建一个BackOffPolicy。如下:

@Configuration
public class MyConfiguration {
    @Bean
    LoadBalancedRetryFactory retryFactory() {
        return new LoadBalancedRetryFactory() {
            @Override
            public BackOffPolicy createBackOffPolicy(String service) {
                return new ExponentialBackOffPolicy();
            }
        };
    }
}

配置

当将Ribbon与Spring Retry一起使用时,可以通过配置某些Ribbon属性来控制重试功能。要做到这一点,设置client.ribbon.MaxAutoRetriesclient.ribbon.MaxAutoRetriesNextServerclient.ribbon.OkToRetryOnAllOperations性能。有关这些属性的描述,请参见功能区文档

注意:开启client.ribbon.OkToRetryOnAllOperations的话将会包括重试POST请求,这样会对服务器资源有些影响,因为它会缓存请求体数据。

另外,你可能希望对某些响应中的状态码进行重试请求。可通过设置clientName.ribbon.retryableStatusCodes。

clientName:
  ribbon:
    retryableStatusCodes: 404,502

您也可以创建一个类型为bean的bean,LoadBalancedRetryPolicy并实现该retryableStatusCode方法以根据状态码重试请求。

Zuul

可以通过zuul.retryable设置为false来关闭zuul中的重试机制。也可以设置zuul.routes.routename.retryable为false来关闭某个指定路由上的重试机制

Http客户端

Spring Cloud Netflix 会为Ribbon, Feign, 和 Zuul自动创建HTTP客户端。你也可以提供你自己的HTTP客户端。如果您使用的是Apache Http Cient,那么可以创建类型为ClosableHttpClient的bean,或者如果您使用的是OK Http,则可以创建OkHttpClient。

注意:当您创建自己的HTTP客户机时,您还负责为这些客户机实现正确的连接管理策略。如果没做好会导致资源管理问题。

翻译于:

  • https://docs.spring.io/spring-cloud-netflix/docs/2.2.7.RELEASE/reference/html/#router-and-filter-zuul
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐