Spring Cloud Gateway 作为新一代网关,在性能上有很大提升,并且附加了诸如限流等实用的功能。本节主要讲解 Gateway 的一些实用功能的实例。

限流实战

开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。API 网关作为所有请求的入口,请求量大,我们可以通过对并发访问的请求进行限速来保护系统的可用性。

目前限流提供了基于 Redis 的实现,我们需要增加对应的依赖,代码如下所示。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
  4. </dependency>

我们可以通过 KeyResolver 来指定限流的 Key,比如我们需要根据用户来做限流,或是根据 IP 来做限流等。

1. IP 限流

IP 限流的 Key 指定具体代码如下所示。

  1. @Bean
  2. public KeyResolver ipKeyResolver() {
  3. return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
  4. }
  5. public static String getIpAddr(ServerHttpRequest request) {
  6. HttpHeaders headers = request.getHeaders();
  7. List<String> ips = headers.get("X-Forwarded-For");
  8. String ip = "192.168.1.1";
  9. if (ips != null && ips.size() > 0) {
  10. ip = ips.get(0);
  11. }
  12. return ip;
  13. }

2. 用户限流

根据用户来做限流只需要获取当前请求的用户 ID 或者用户名,代码如下所示。

  1. @Bean
  2. KeyResolver userKeyResolver() {
  3. return exchange ->
  4. Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
  5. }

3. 接口限流

获取请求地址的 uri 作为限流 Key,代码如下所示。

  1. @Bean
  2. KeyResolver apiKeyResolver() {
  3. return exchange ->
  4. Mono.just(exchange.getRequest().getPath().value());
  5. }

然后配置限流的过滤器信息:

  1. server:
  2. port: 8084
  3. spring:
  4. redis:
  5. host: 127.0.0.1
  6. port: 6379
  7. cloud:
  8. gateway:
  9. routes:
  10. - id: fsh-house
  11. uri: lb://fsh-house
  12. predicates:
  13. - Path=/house/**
  14. filters:
  15. - name: RequestRateLimiter
  16. args:
  17. redis-rate-limiter.replenishRate: 10
  18. redis-rate-limiter.burstCapacity: 20
  19. key-resolver: "#{@ipKeyResolver}"
  • filter 名称必须是 RequestRateLimiter。
  • redis-rate-limiter.replenishRate:允许用户每秒处理多少个请求。
  • redis-rate-limiter.burstCapacity:令牌桶的容量,允许在 1s 内完成的最大请求数。
  • key-resolver:使用 SpEL 按名称引用 bean。


可以访问接口进行测试,这时候 Redis 中会有对应的数据:

  1. 127.0.0.1:6379> keys *
  2. 1) "request_rate_limiter.{localhost}.timestamp"
  3. 2) "request_rate_limiter.{localhost}.tokens"

大括号中就是我们的限流 Key,这里是 IP,本地的就是 localhost。

  • timestamp:存储的是当前时间的秒数,也就是 System.currentTimeMillis()/1000 或者 Instant.now().getEpochSecond()。
  • tokens:存储的是当前这秒钟对应的可用令牌数量。

    熔断回退实战

    Spring Cloud Gateway 中使用 Hystrix 进行回退需要增加 Hystrix 的依赖,代码如下所示。
    1. <dependency>
    2. <groupId>org.springframework.cloud</groupId>
    3. <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
    4. </dependency>
    内置了 HystrixGatewayFilterFactory 来实现路由级别的熔断,只需要配置即可实现熔断回退功能。配置方式如下所示。 ```
  • id: user-service uri: lb://user-service predicates:
    • Path=/user-service/** filters:
    • name: Hystrix args: name: fallbackcmd fallbackUri: forward:/fallback ``` 上面配置了一个 Hystrix 过滤器,该过滤器会使用 Hystrix 熔断与回退,原理是将请求包装成 RouteHystrixCommand 执行,RouteHystrixCommand 继承于 com.netflix.hystrix.HystrixObservableCommand。

fallbackUri 是发生熔断时回退的 URI 地址,目前只支持 forward 模式的 URI。如果服务被降级,该请求会被转发到该 URI 中。

在网关中创建一个回退的接口,用于熔断时处理返回给调用方的信息,代码如下所示。

  1. @RestController
  2. public class FallbackController {
  3. @GetMapping("/fallback")
  4. public String fallback() {
  5. return "fallback";
  6. }
  7. }

跨域实战

在 Spring Cloud Gateway 中配置跨域有两种方式,分别是代码配置方式和配置文件方式。

代码配置方式配置跨域,具体代码如下所示。

  1. @Configuration
  2. public class CorsConfig {
  3. @Bean
  4. public WebFilter corsFilter() {
  5. return (ServerWebExchange ctx, WebFilterChain chain) -> {
  6. ServerHttpRequest request = ctx.getRequest();
  7. if (CorsUtils.isCorsRequest(request)) {
  8. HttpHeaders requestHeaders = request.getHeaders();
  9. ServerHttpResponse response = ctx.getResponse();
  10. HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
  11. HttpHeaders headers = response.getHeaders();
  12. headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
  13. headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
  14. requestHeaders.getAccessControlRequestHeaders());
  15. if (requestMethod != null) {
  16. headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
  17. }
  18. headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
  19. headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
  20. if (request.getMethod() == HttpMethod.OPTIONS) {
  21. response.setStatusCode(HttpStatus.OK);
  22. return Mono.empty();
  23. }
  24. }
  25. return chain.filter(ctx);
  26. };
  27. }
  28. }

配置文件方式配置跨域:

  1. spring:
  2. cloud:
  3. gateway:
  4. globalcors:
  5. corsConfigurations:
  6. '[/**]':
  7. allowedOrigins: "*"
  8. exposedHeaders:
  9. - content-type
  10. allowedHeaders:
  11. - content-type
  12. allowCredentials: true
  13. allowedMethods:
  14. - GET
  15. - OPTIONS
  16. - PUT
  17. - DELETE
  18. - POST

统一异常处理

Spring Cloud Gateway 中的全局异常处理不能直接使用 @ControllerAdvice,可以通过跟踪异常信息的抛出,找到对应的源码,自定义一些处理逻辑来匹配业务的需求。

网关是给接口做代理转发的,后端对应的是 REST API,返回数据格式是 JSON。如果不做处理,当发生异常时,Gateway 默认给出的错误信息是页面,不方便前端进行异常处理。

所以我们需要对异常信息进行处理,并返回 JSON 格式的数据给客户端。下面先看实现的代码,后面再跟大家讲一下需要注意的地方。

自定义异常处理逻辑,代码如下所示。

  1. public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
  2. public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
  3. ErrorProperties errorProperties, ApplicationContext applicationContext) {
  4. super(errorAttributes, resourceProperties, errorProperties, applicationContext);
  5. }
  6. /**
  7. * 获取异常属性
  8. */
  9. @Override
  10. protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
  11. int code = 500;
  12. Throwable error = super.getError(request);
  13. if (error instanceof org.springframework.cloud.gateway.support.NotFoundException) {
  14. code = 404;
  15. }
  16. return response(code, this.buildMessage(request, error));
  17. }
  18. /**
  19. * 指定响应处理方法为JSON处理的方法
  20. *
  21. * @param errorAttributes
  22. */
  23. @Override
  24. protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
  25. return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
  26. }
  27. /**
  28. * 根据code获取对应的HttpStatus
  29. *
  30. * @param errorAttributes
  31. */
  32. @Override
  33. protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
  34. int statusCode = (int) errorAttributes.get("code");
  35. return HttpStatus.valueOf(statusCode);
  36. }
  37. /**
  38. * 构建异常信息
  39. *
  40. * @param request
  41. * @param ex
  42. * @return
  43. */
  44. private String buildMessage(ServerRequest request, Throwable ex) {
  45. StringBuilder message = new StringBuilder("Failed to handle request [");
  46. message.append(request.methodName());
  47. message.append(" ");
  48. message.append(request.uri());
  49. message.append("]");
  50. if (ex != null) {
  51. message.append(": ");
  52. message.append(ex.getMessage());
  53. }
  54. return message.toString();
  55. }
  56. /**
  57. * 构建返回的JSON数据格式
  58. *
  59. * @param status 状态码
  60. * @param errorMessage 异常信息
  61. * @return
  62. */
  63. public static Map<String, Object> response(int status, String errorMessage) {
  64. Map<String, Object> map = new HashMap<>();
  65. map.put("code", status);
  66. map.put("message", errorMessage);
  67. map.put("data", null);
  68. return map;
  69. }
  70. }

覆盖默认的配置,代码如下所示。

  1. @Configuration
  2. @EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class })
  3. public class ErrorHandlerConfiguration {
  4. private final ServerProperties serverProperties;
  5. private final ApplicationContext applicationContext;
  6. private final ResourceProperties resourceProperties;
  7. private final List<ViewResolver> viewResolvers;
  8. private final ServerCodecConfigurer serverCodecConfigurer;
  9. public ErrorHandlerConfiguration(ServerProperties serverProperties, ResourceProperties resourceProperties,
  10. ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer,
  11. ApplicationContext applicationContext) {
  12. this.serverProperties = serverProperties;
  13. this.applicationContext = applicationContext;
  14. this.resourceProperties = resourceProperties;
  15. this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
  16. this.serverCodecConfigurer = serverCodecConfigurer;
  17. }
  18. @Bean
  19. @Order(Ordered.HIGHEST_PRECEDENCE)
  20. public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
  21. JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(errorAttributes,
  22. this.resourceProperties,this.serverProperties.getError(), this.applicationContext);
  23. exceptionHandler.setViewResolvers(this.viewResolvers);
  24. exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
  25. exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
  26. return exceptionHandler;
  27. }
  28. }

1. 异常时如何返回 JSON 而不是 HTML?

在 org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWeb-Exception-Handler 中的 getRoutingFunction() 方法就是控制返回格式的,源代码如下所示。

  1. @Override
  2. protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
  3. return RouterFunctions.route(acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse);
  4. }

这里优先是用 HTML 来显示的,如果想用 JSON 显示改动就可以了,具体代码如下所示。

  1. protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
  2. return RouterFunctions.route(RequestPredicates.all(),this::renderErrorResponse);
  3. }

2. getHttpStatus 需要重写

原始的方法是通过 status 来获取对应的 HttpStatus 的,具体代码如下所示。

  1. protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
  2. int statusCode = (int) errorAttributes.get("status");
  3. return HttpStatus.valueOf(statusCode);
  4. }

如果我们定义的格式中没有 status 字段的话,就会报错,因为找不到对应的响应码。要么返回数据格式中增加 status 子段,要么重写,在笔者的操作中返回的是 code,所以要重写,代码如下所示。

  1. @Override
  2. protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
  3. int statusCode = (int) errorAttributes.get("code");
  4. return HttpStatus.valueOf(statusCode);
  5. }

重试机制

RetryGatewayFilter 是 Spring Cloud Gateway 对请求重试提供的一个 GatewayFilter Factory。配置方式如下所示。

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: zuul-encrypt-service
  6. uri: lb://zuul-encrypt-service
  7. predicates:
  8. - Path=/data/**
  9. filters:
  10. - name: Retry
  11. args:
  12. retries: 3
  13. series: SERVER_ERROR

上述代码中具体参数含义如下所示。

  • retries:重试次数,默认值是 3 次。
  • series:状态码配置(分段),符合某段状态码才会进行重试逻辑,默认值是 SERVER_ERROR,值是 5,也就是 5XX(5 开头的状态码),共有 5 个值,代码如下所示。

    1. public enum Series {
    2. INFORMATIONAL(1), SUCCESSFUL(2), REDIRECTION(3), CLIENT_ERROR(4), SERVER_ERROR(5);
    3. }

    上述代码中具体参数含义如下所示。

  • statuses:状态码配置,和 series 不同的是这里是具体状态码的配置,取值请参考 org.springframework.http.HttpStatus。

  • methods:指定哪些方法的请求需要进行重试逻辑,默认值是 GET 方法,取值代码如下所示。
    1. public enum HttpMethod {
    2. GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
    3. }
    上述代码中具体参数含义如下所示。 exceptions:指定哪些异常需要进行重试逻辑。默认值是 java.io.IOException 和 org.springframework.cloud.gateway.support.TimeoutException。