SpringCloud Gateway 作爲新一代網關,在性能上有很大提升,並且附加了諸如限流等實用的功能。本節主要講解 Gateway 的一些實用功能的實例。
限流實戰
開發高併發系統時有三把利器用來保護系統:緩存、降級和限流。API 網關作爲所有請求的入口,請求量大,我們可以通過對併發訪問的請求進行限速來保護系統的可用性。
目前限流提供了基於 Redis 的實現,我們需要增加對應的依賴,代碼如下所示。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
我們可以通過 KeyResolver 來指定限流的 Key,比如我們需要根據用戶來做限流,或是根據 IP 來做限流等。
1. IP 限流
IP 限流的 Key 指定具體代碼如下所示。
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
public static String getIpAddr(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
List<String> ips = headers.get("X-Forwarded-For");
String ip = "192.168.1.1";
if (ips != null && ips.size() > 0) {
ip = ips.get(0);
}
return ip;
}
2. 用戶限流
根據用戶來做限流只需要獲取當前請求的用戶 ID 或者用戶名,代碼如下所示。
@Bean
KeyResolver userKeyResolver() {
return exchange ->
Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
3. 接口限流
獲取請求地址的 uri 作爲限流 Key,代碼如下所示。
@Bean
KeyResolver apiKeyResolver() {
return exchange ->
Mono.just(exchange.getRequest().getPath().value());
}
然後配置限流的過濾器信息:
server:
port: 8084
spring:
redis:
host: 127.0.0.1
port: 6379
cloud:
gateway:
routes:
- id: fsh-house
uri: lb://fsh-house
predicates:
- Path=/house/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@ipKeyResolver}"
filter 名稱必須是 RequestRateLimiter。
redis-rate-limiter.replenishRate:允許用戶每秒處理多少個請求。
redis-rate-limiter.burstCapacity:令牌桶的容量,允許在 1s 內完成的最大請求數。
key-resolver:使用 SpEL 按名稱引用 bean。
可以訪問接口進行測試,這時候 Redis 中會有對應的數據:
127.0.0.1:6379> keys *
1) "request_rate_limiter.{localhost}.timestamp"
2) "request_rate_limiter.{localhost}.tokens"
大括號中就是我們的限流 Key,這裏是 IP,本地的就是 localhost。
- timestamp:存儲的是當前時間的秒數,也就是 System.currentTimeMillis()/1000 或者 Instant.now().getEpochSecond()。
- tokens:存儲的是當前這秒鐘對應的可用令牌數量。
熔斷回退實戰
在 Spring Cloud Gateway 中使用 Hystrix 進行回退需要增加 Hystrix 的依賴,代碼如下所示。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</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 中。
在網關中創建一個回退的接口,用於熔斷時處理返回給調用方的信息,代碼如下所示。
@RestController
public class FallbackController {
@GetMapping("/fallback")
public String fallback() {
return "fallback";
}
}
跨域實戰
推薦電子商務源碼,在 Spring Cloud Gateway 中配置跨域有兩種方式,分別是代碼配置方式和配置文件方式。
代碼配置方式配置跨域,具體代碼如下所示。
@Configuration
public class CorsConfig {
@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (CorsUtils.isCorsRequest(request)) {
HttpHeaders requestHeaders = request.getHeaders();
ServerHttpResponse response = ctx.getResponse();
HttpMethod requestMethod = requestHeaders.getAccessControlRequestMethod();
HttpHeaders headers = response.getHeaders();
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, requestHeaders.getOrigin());
headers.addAll(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
requestHeaders.getAccessControlRequestHeaders());
if (requestMethod != null) {
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, requestMethod.name());
}
headers.add(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
headers.add(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "*");
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}
}
配置文件方式配置跨域:
spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "*"
exposedHeaders:
- content-type
allowedHeaders:
- content-type
allowCredentials: true
allowedMethods:
- GET
- OPTIONS
- PUT
- DELETE
- POST
統一異常處理
Spring Cloud Gateway 中的全局異常處理不能直接使用 @ControllerAdvice,可以通過跟蹤異常信息的拋出,找到對應的源碼,自定義一些處理邏輯來匹配業務的需求。
網關是給接口做代理轉發的,後端對應的是 REST API,返回數據格式是 JSON。如果不做處理,當發生異常時,Gateway 默認給出的錯誤信息是頁面,不方便前端進行異常處理。
所以我們需要對異常信息進行處理,並返回 JSON 格式的數據給客戶端。下面先看實現的代碼,後面再跟大家講一下需要注意的地方。
自定義異常處理邏輯,代碼如下所示。
public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
public JsonExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
ErrorProperties errorProperties, ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, errorProperties, applicationContext);
}
/**
* 獲取異常屬性
*/
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
int code = 500;
Throwable error = super.getError(request);
if (error instanceof org.springframework.cloud.gateway.support.NotFoundException) {
code = 404;
}
return response(code, this.buildMessage(request, error));
}
/**
* 指定響應處理方法爲JSON處理的方法
*
* @param errorAttributes
*/
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
/**
* 根據code獲取對應的HttpStatus
*
* @param errorAttributes
*/
@Override
protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
int statusCode = (int) errorAttributes.get("code");
return HttpStatus.valueOf(statusCode);
}
/**
* 構建異常信息
*
* @param request
* @param ex
* @return
*/
private String buildMessage(ServerRequest request, Throwable ex) {
StringBuilder message = new StringBuilder("Failed to handle request [");
message.append(request.methodName());
message.append(" ");
message.append(request.uri());
message.append("]");
if (ex != null) {
message.append(": ");
message.append(ex.getMessage());
}
return message.toString();
}
/**
* 構建返回的JSON數據格式
*
* @param status 狀態碼
* @param errorMessage 異常信息
* @return
*/
public static Map<String, Object> response(int status, String errorMessage) {
Map<String, Object> map = new HashMap<>();
map.put("code", status);
map.put("message", errorMessage);
map.put("data", null);
return map;
}
}
覆蓋默認的配置,代碼如下所示。
@Configuration
@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class })
public class ErrorHandlerConfiguration {
private final ServerProperties serverProperties;
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public ErrorHandlerConfiguration(ServerProperties serverProperties, ResourceProperties resourceProperties,
ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer,
ApplicationContext applicationContext) {
this.serverProperties = serverProperties;
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(errorAttributes,
this.resourceProperties,this.serverProperties.getError(), this.applicationContext);
exceptionHandler.setViewResolvers(this.viewResolvers);
exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
return exceptionHandler;
}
}
1. 異常時如何返回 JSON 而不是 HTML?
在 org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWeb-Exception-Handler 中的 getRoutingFunction() 方法就是控制返回格式的,源代碼如下所示。
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse);
}
這裏優先是用 HTML 來顯示的,如果想用 JSON 顯示改動就可以了,具體代碼如下所示。
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(),this::renderErrorResponse);
}
2. getHttpStatus 需要重寫
原始的方法是通過 status 來獲取對應的 HttpStatus 的,具體代碼如下所示。
protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
int statusCode = (int) errorAttributes.get("status");
return HttpStatus.valueOf(statusCode);
}
如果我們定義的格式中沒有 status 字段的話,就會報錯,因爲找不到對應的響應碼。要麼返回數據格式中增加 status 子段,要麼重寫,在筆者的操作中返回的是 code,所以要重寫,代碼如下所示。
@Override
protected HttpStatus getHttpStatus(Map<String, Object> errorAttributes) {
int statusCode = (int) errorAttributes.get("code");
return HttpStatus.valueOf(statusCode);
}
重試機制
RetryGatewayFilter 是 Spring Cloud Gateway 對請求重試提供的一個 GatewayFilter Factory。配置方式如下所示。
spring:
cloud:
gateway:
routes:
- id: zuul-encrypt-service
uri: lb://zuul-encrypt-service
predicates:
- Path=/data/**
filters:
- name: Retry
args:
retries: 3
series: SERVER_ERROR
上述代碼中具體參數含義如下所示。
- retries:重試次數,默認值是 3 次。
- series:狀態碼配置(分段),符合某段狀態碼纔會進行重試邏輯,默認值是 SERVER_ERROR,值是 5,也就是 5XX(5 開頭的狀態碼),共有 5 個值,代碼如下所示。
public enum Series {
INFORMATIONAL(1), SUCCESSFUL(2), REDIRECTION(3), CLIENT_ERROR(4), SERVER_ERROR(5);
}
上述代碼中具體參數含義如下所示。
- statuses:狀態碼配置,和 series 不同的是這裏是具體狀態碼的配置,取值請參考 org.springframework.http.HttpStatus。
- methods:指定哪些方法的請求需要進行重試邏輯,默認值是 GET 方法,取值代碼如下所示。
public enum HttpMethod {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
}
上述代碼中具體參數含義如下所示。 exceptions:指定哪些異常需要進行重試邏輯。默認值是 java.io.IOException 和 org.springframework.cloud.gateway.support.TimeoutException。