如果想禁用GateWay,可以通过两种方式。
// @Bean
// public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
// return builder.routes()
// .route(r -> r.path("/gateway/**")
// .filters(f -> f.stripPrefix(1)
// .addResponseHeader("X-Response-Default-Foo", "Default-Bar"))
// .uri("lb://ribbon-server")
// .order(0)
// .id("strippath_route")
// )
// .build();
// }
spring:
cloud:
gateway:
enabled: false
前面我们在前面讲Zuul的之后知道Zuul的路由前缀配置方式很简单,只需要
设置zuul.prefix即可。
而在Spring Cloud Getaway 为我们提供了很多原生的过滤器,我们可以通过StripPrefix、RewritePath 在默认过滤器中 进行前缀的设置,可以参照过滤器中 StripPrefix、过滤器 RewritePath、过滤器 Default 进行实现。
前面我们在前面讲解Zuul的时候,路由跳转是通过在url中指定forward:进行跳转。不清楚的可以回头看看。
Spring Cloud Getaway 的路由跳转也可以使用forward:,使用方式一样的。比如下面我们要讲的Gateway FallBack 就是使用forward:处理异常问题
在前面网关扩展之zuul限流中我们介绍了两种限流手段,可以很方便实现。如果忘了的话可以回去看一下。
在 Spring Cloud Gateway 上实现限流是个不错的选择,只需要编写一个过滤器就可以了。有了前边过滤器的基础,写起来很轻松。Spring Cloud Gateway 已经内置了一个RequestRateLimiterGatewayFilterFactory
,我们可以直接使用。
目前RequestRateLimiterGatewayFilterFactory的实现依赖于 Redis,Redis的实现基于Stripe的工作。它需要使用spring-boot-starter-data-redis-reactive Spring Boot启动器。
使用的算法是令牌桶算法。
Getaway实现限流所使用的lua脚本在gateway包下的script目录中,如下:
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)
--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)
local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
new_tokens = filled_tokens - requested
allowed_num = 1
end
--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
return { allowed_num, new_tokens }
新建项目模块cloud-gateways-gateway
,引入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
添加配置
server:
port: 8711
spring:
application:
name: gateways-gateway
profiles:
active: rate_limiter_route
eureka:
client:
service-url:
defaultZone: http://xunfei:12345@127.0.0.1:8761/eureka/
---
spring:
cloud:
gateway:
routes:
- id: rate_limiter_route
uri: lb://ribbon-server
predicates:
- After=2020-05-09T11:30:11.965+08:00[Asia/Shanghai]
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 1
key-resolver: "#{@userKeyResolver}"
redis:
host: ${redis.host}
password: ${redis.pwd}
port: 6379
profiles: rate_limiter_route
限流配置描述
新建启动类
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
限流Bean 配置
下面的配置给出了三种,分别是基于参数、基于ip、基于请求路径。用的时候只能用一个。
@Configuration
public class GatewayConfig {
//基于参数
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("cloud"));
}
// 基于ip
// @Bean
// public KeyResolver ipKeyResolver() {
// return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
// }
//基于请求路径
// @Bean
// KeyResolver apiKeyResolver() {
// return exchange -> Mono.just(exchange.getRequest().getPath().value());
// }
}
启动测试,多次频繁点击发送。可以看到返回429 Too Many Request。
前面我们在将Feign、Ribbon的时候已经提到过重试机制,就是在我们发送Http 请求的时候希望一次失败后可以再尝试发送一次,因为前几次可能因为网络等不确定原因。Spring Cloud Gateway对请求重试提供的一个GatewayFilter Factory
添加并active配置
---
spring:
cloud:
gateway:
routes:
- id: retry_route
uri: lb://ribbon-server
predicates:
- After=2020-05-09T11:30:11.965+08:00[Asia/Shanghai]
filters:
- name: Retry
args:
retries: 3
series:
- SERVER_ERROR
statuses:
- INTERNAL_SERVER_ERROR
- BAD_GATEWAY
methods:
- GET
exceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
profiles: retry_route
Spring Cloud Gateway 通过以下参数来控制重试机制: 。
retries:重试次数,默认值是 3 次
statuses:HTTP 的状态返回码,取值请参考:org.springframework.http.HttpStatus
methods:指定哪些方法的请求需要进行重试逻辑,默认值是 GET 方法,取值参考:org.springframework.http.HttpMethod
series:一些列的状态码配置,取值参考:org.springframework.http.HttpStatus.Series。符合的某段状态码才会进行重试逻辑,默认值是 SERVER_ERROR,值是 5,也就是 5XX(5 开头的状态码),共有5 个值。
exceptions:应重试的引发异常的列表。
backoff:为重试配置的指数补偿。重试在的退避间隔后执行firstBackoff * (factorn),其中n为迭代。如果maxBackoff已配置,则应用的最大退避限制为maxBackoff。如果basedOnPreviousValue为true,则使用计算退避prevBackoff * factor。
源码分析
public static class RetryConfig implements HasRouteId {
private String routeId;
private int retries = 3;
private List<Series> series;
private List<HttpStatus> statuses;
private List<HttpMethod> methods;
private List<Class<? extends Throwable>> exceptions;
private RetryGatewayFilterFactory.BackoffConfig backoff;
public RetryConfig() {
this.series = RetryGatewayFilterFactory.toList(Series.SERVER_ERROR);
this.statuses = new ArrayList();
this.methods = RetryGatewayFilterFactory.toList(HttpMethod.GET);
this.exceptions = RetryGatewayFilterFactory.toList(IOException.class, TimeoutException.class);
}
......
通过分析RetryConfig,我们可以看到重试支持的参数和默认值。
public enum Series {
INFORMATIONAL(1),
SUCCESSFUL(2),
REDIRECTION(3),
CLIENT_ERROR(4),
SERVER_ERROR(5);
}
public enum HttpMethod {
GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
}
exceptions:指定哪些异常需要进行重试逻辑,默认值是IOException、TimeoutException
修改ribbon-server
为了测试重试,我们重新加上随机休眠
int millis = new Random().nextInt(3000);
System.out.println("client线程休眠时间:"+millis);
Thread.sleep(millis);
if(millis>1000){
throw new RuntimeException("error");
}
启动应用,访问127.0.0.1:8711/user/1,如果延迟时间超过1秒钟,就会抛异常,默认重试3次,3次之后仍然没有成功就会返回异常信息。
前面我们前面章节讲到Zuul的FallBack,并且我们在讲Hystrix也说到在一个分布式系统架构中FallBack的作用。服务B出现的问题不应该影响服务A,在服务B可用的时候继续提供服务。这样保证了服务之间是隔离的,不会产生雪崩式的不可用。
Spring Cloud Gateway 提供了Hystrix GatewayFilter Factory,Hystrix GatewayFilter允许你向网关路由引入断路器,保护你的服务不受级联故障的影响,并允许你在下游故障时提供fallback响应。
添加hystrix 依赖
要在项目中启用Hystrix网关过滤器,需要添加对 spring-cloud-starter-netflix-hystrix的依赖 Spring Cloud Netflix.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
应用配置
添加并启用配置
---
spring:
cloud:
gateway:
routes:
- id: hystrix_route
uri: lb://ribbon-server
predicates:
- After=2020-05-09T11:30:11.965+08:00[Asia/Shanghai]
filters:
- name: Hystrix
args:
name: fallback
fallbackUri: forward:/fallback/ribbon-server
profiles: hystrix_route
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
Hystrix GatewayFilter Factory 需要一个name参数,即HystrixCommand的名称,设置过滤器名称为Hystrix,并且设置fallback的地址为重定向到我们自己定义的接口。ribbon-server会作为参数。
FallBack 接收类
控制层接收服务名。这样如果一个服务不可用,会将服务不可用的信息返回。这里只是掩饰,可以用自己的业务去丰富这个功能。
@RestController
public class FallbackController {
@RequestMapping("fallback/{name}")
public Mono<ApiResult> systemFallback(@PathVariable String name) {
String response = String.format("访问%s超时或者服务不可用", name);
return Mono.just(ApiResult.failedMsg(response));
}
}
启动应用,这次我们将ribbon-server 关掉。访问127.0.0.1:8711/user/1可以看到返回服务不可用信息。
跟Zuul一样,Spring Cloud Gateway的全局异常处理也不能直接用@ControllerAdvice来处理,通过自定义异常处理器来实现业务需求。
网关是给接口做代理转发的,后端对应的都是REST API,返回数据格式一般都是JSON。如果不做处理,当发生异常时,Gateway默认给出的错误信息是页面,不方便前端进行异常处理。
需要对异常信息进行处理,返回JSON格式的数据给客户端。
**添加并启用配置
---
spring:
cloud:
gateway:
routes:
- id: exception_route
uri: lb://ribbon-server
predicates:
- After=1999-05-09T11:30:11.965+08:00[Asia/Shanghai]
profiles: exception_route
自定义异常
先看默认异常处理
在org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler
中的getRoutingFunction()
方法就是控制返回格式的,原代码如下,默认异常信息返回的是HTML格式。这里我们自定义的时候需要改变为JSON的。
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(this.acceptsTextHtml(), this::renderErrorView).andRoute(RequestPredicates.all(), this::renderErrorResponse);
}
原始的方法是通过status来获取对应的HttpStatus的,对应的是整形数字,如果你的状态字段不是status,需要重写此方法。
protected int getHttpStatus(Map<String, Object> errorAttributes) {
return (Integer)errorAttributes.get("status");
}
重写后的如下
@Slf4j
public class GatewayExceptionHandler extends DefaultErrorWebExceptionHandler {
public GatewayExceptionHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
ErrorProperties errorProperties, ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, errorProperties, applicationContext);
}
/**
* 异常处理,定义返回报文格式
*/
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
Throwable error = super.getError(request);
log.error(
"请求发生异常,请求URI:{},请求方法:{},异常信息:{}",
request.path(), request.methodName(), error.getMessage()
);
String errorMessage;
if (error instanceof NotFoundException) {
String serverId = StringUtils.substringAfterLast(error.getMessage(), "Unable to find instance for ");
serverId = StringUtils.replace(serverId, "\"", StringUtils.EMPTY);
errorMessage = String.format("无法找到%s服务", serverId);
} else if (StringUtils.containsIgnoreCase(error.getMessage(), "connection refused")) {
errorMessage = "目标服务拒绝连接";
} else if (error instanceof TimeoutException) {
errorMessage = "访问服务超时";
} else if (error instanceof ResponseStatusException
&& StringUtils.containsIgnoreCase(error.getMessage(), HttpStatus.NOT_FOUND.toString())) {
errorMessage = "未找到该资源";
} else {
errorMessage = "网关转发异常";
}
Map<String, Object> errorAttributes = new HashMap<>(2);
errorAttributes.put("msg", errorMessage);
errorAttributes.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
return errorAttributes;
}
// 设置返回内容为JSON
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
// 设置HttpStatus
@Override
protected int getHttpStatus(Map<String, Object> errorAttributes) {
return (int) errorAttributes.get("code");
}
}
覆盖默认的异常处理
重写errorWebExceptionHandler()方法,里面的实现用我们自己定义的异常处理(GatewayExceptionHandler)来实现,否则仍然走默认的。
@Configuration
public class GatewayErrorConfigure {
private final ServerProperties serverProperties;
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayErrorConfigure(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) {
GatewayExceptionHandler exceptionHandler = new GatewayExceptionHandler(
errorAttributes,
this.resourceProperties,
this.serverProperties.getError(),
this.applicationContext);
exceptionHandler.setViewResolvers(this.viewResolvers);
exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
return exceptionHandler;
}
}
启动应用,关闭ribbon-server,访问127.0.0.1:8711/user/1可以看到自定义的异常处理执行了。
相对于Zuul来说,目前Spring Cloud Gateway是Spring Cloud 官方主要推广的,当然如果你已经使用的是Zuul,也可以不用替换,因为一旦集群部署以后,一般企业也达不到这个瓶颈。
本文由 寻非 创作,如果您觉得本文不错,请随意赞赏
采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
原文链接:https://www.zhouning.group/archives/springcloud13网关之gateway下路由配置
最后更新:2020-05-09 11:17:29
Update your browser to view this website correctly. Update my browser now