1 简述
spring cloud 早期api网关组件是zuul,底层基于tomcat,采用传统编程方式,曾广泛应用,性能也不错,目前最新版本是2021年4月,基本不再迭代,新项目就别用了。
spring cloud gateway 采用响应式编程风格,是spring cloud当前主推api网关解决方案,它的框架、流程、类库的设计,都继承了spring一贯的直观、简单的风格。概念理解起来,还是很清晰的。它采用新的异步非阻塞的响应式编程风格,之所以颠覆原来已有成熟方案,个人理解,api网关的上游、下游都采用异步非阻塞方式处理,可以大大降低对线程依赖,提高CPU利用率,比较适合IO处理密集,功能相对单一的场合。
了解响应式编程过程中,刚开始可能会不适应,甚至对它产生怀疑,这个可参考一些它的经典应用场景,响应式编程从推出、到推广,已有很长一段时间,不管是spring,还是redis,mongodb都在积极跟进,足见该框架的优秀。
抛开具体技术,对于api网关框架,路由是它最基本、最核心的能力,也因它的路由网关性质,使他具备统一拦截处理的能力,比较适合处理一些全局、统一性的问题。
api网关除路由外,最常具备的功能,就是认证、鉴权,以及流量保护、重试策略、防刷token、统计日志等,另外像人机识别、灰度发布等机制也比较适合。
另外,既然作为API网关角色,就需要对API制定规则,不管是外部请求的header,url,还是后端服务的响应码、返回值结构,需要有一个统一的外观,可依据不同来源pc、app、h5灵活定义。
2 概念
2.1 术语
以下是spring cloud gateway框架,抽象的核心概念,主要包含3种:
Route(路由): 网关的基本构件。它由一个ID、一个目的地URI、一个谓词(Predicate)集合和一个过滤器(Filter)集合定义。
Predicate(谓词、断言): http请求的匹配规则,以此来匹配唯一的路由节点,已内置丰富配置规则,比如请求url、header、请求时间等。
Filter(过滤器): 它是 GatewayFilter 实例,已由特定工厂构建。在这里,你可以在发送下游请求之前或之后,对请求和响应进行修改。
提示:三者不是并列概念,存在从属关系,Predicate、Filter从属于Route。
2.2 流程
以下是官网Spring Cloud Gateway 工作概述图
从图可看出,跟java中servelt容器处理流程很相似,保留了传统的命名规范,不同的是servelt由进程内Controller处理,而Gateway服务则由外部远程服务处理。
Filter过滤器可以在代理请求发送之前和之后运行,在所有的 "pre"
(前)过滤器逻辑都被执行后,才会向后端服务提交请求,然后"post"
(后)逻辑被执行。
Filter跟早期的zuul网关框架有所不同,zuul中Filter只能在pre(前)、post(后)阶段二选一。
3 准备
3.1 注册中心
注册中心选择nacos,下载、它的安装部署,这里就不介绍,nacos官网文档描述的很详细,配置如下:
spring:
cloud:
nacos:
discovery:
server-addr: 192.168.0.106:8848
3.2 maven依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
3.3 编程风格
Spring Cloud Gateway 建立在 Spring Boot 2.x、 Spring WebFlux 和 Project Reactor之上。
默认基于 Netty,采用响应式编程风格。
其中,Reactor 框架是 Pivotal 公司(开发 Spring 等技术的公司)开发,它符合Reactive Streams 规范(Reactive Streams 是由 Netflix、TypeSafe、Pivotal 等公司发起)。
4 配置
4.1 一般配置
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: hello1
# uri: http://localhost:8081
uri: lb://mtr-cloud-client1
predicates:
- Path=/aaa/demo/**,/ccc/demo/**
- After=2022-05-26T15:15:00.789+08:00
- Header=X-Request-Id, \d+
filters:
- StripPrefix=1
- AddRequestHeader=X-Request-Type, pc
上面是一个完整的route配置,它包含4个属性:id,uri,predicates,filters
id
唯一标记一个路由配置项。
uri
描述后端服务地址,通常按lb://server_id方式配置(server_id,后端服务在注册中心的Id标识 ) ,也可按http://host:port 风格配置 。
predicates(匹配)
负责路由匹配, 就是将一个http请求与一个具体的路由配置项关联。框架内已提供丰富的匹配功能,可以对请求的时间、url、head、method等维度进行匹配,除参考官网的配置介绍,如果有兴趣,可了解下源码,逻辑都比较简单,包路径org.springframework.cloud.gateway.handler.predicate,每个维度的predicate都对应一个单独的类,例如:PathRoutePredicateFactory等。
filters(过滤)
对已匹配的请求,进行请求、响应结果的修改,或增加拦截处理逻辑。基本上,api网关的主要处理逻辑,都在Filter实现。通常都会包含StripPrefix配置,指明后端服务url在路径的起始位置。
框架已内置、实现了丰富的过滤器,不仅包含AddRequestHeader, AddResponseHeader, ModifyRequestBody, ModifyResponseBody等对请求、响应处理的过滤器,还有CircuitBreaker、Retry、SecureHeaders等实用过滤器,同样很有必要了解下源码,包路径org.springframework.cloud.gateway.filter.factory下都有对应的工厂类。
4.2 超时配置
为保证接口可用性,一般都会对后端服务响应时间限制:
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
# 全局超时
httpclient:
connect-timeout: 2000
response-timeout: 2s
routes:
- id: hello1
uri: lb://mtr-cloud-client1
predicates:
- Path=/aaa/demo/**,/ccc/demo/**
- After=2022-05-26T15:15:00.789+08:00
filters:
- StripPrefix=1
metadata:
# 局部超时
response-timeout: 2000
connect-timeout: 2s
4.3 重试配置
为保证接口可用性,一般也会对后端服务响应超时、IO异常,进行重试处理。
routes:
- id: hello1
uri: lb://mtr-cloud-client1
predicates:
- Path=/aaa/demo/**,/ccc/demo/**
- After=2022-05-26T15:15:00.789+08:00
filters:
- StripPrefix=1
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY
methods: GET,POST
backoff:
firstBackoff: 1000ms
maxBackoff: 10000ms
factor: 2
basedOnPreviousValue: false
注:以上仅是常用配置,spring cloud gateway 提供了丰富的内置过滤器,可参考官网了解。
5 自定义
5.1 predicate工厂
新建MyHeader断言工厂,参考内置Header断言工厂实现,注意命名规范,需要以RoutePredicateFactory结尾。
@Component
public class MyHeaderRoutePredicateFactory
extends AbstractRoutePredicateFactory<MyHeaderRoutePredicateFactory.Config> {
public static final String HEADER_KEY = "header";
public static final String REGEXP_KEY = "regexp";
public MyHeaderRoutePredicateFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(HEADER_KEY, REGEXP_KEY);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
boolean hasRegex = !StringUtils.isEmpty(config.regexp);
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
List<String> values = exchange.getRequest().getHeaders().getOrDefault(config.header, Collections.emptyList());
if (values.isEmpty()) {
return false;
}
if (hasRegex) {
return values.stream().anyMatch(value -> value.matches(config.regexp));
}
return true;
}
@Override
public String toString() {
return String.format("Header: %s regexp=%s", config.header,
config.regexp);
}
};
}
@Validated
public static class Config {
@NotEmpty
private String header;
private String regexp;
public String getHeader() {
return header;
}
public Config setHeader(String header) {
this.header = header;
return this;
}
public String getRegexp() {
return regexp;
}
public Config setRegexp(String regexp) {
this.regexp = regexp;
return this;
}
}
}
配置:
routes:
- id: hello1
# uri: http://localhost:8081
uri: lb://mtr-cloud-client1
predicates:
- Path=/aaa/demo/**,/ccc/demo/**
- After=2022-05-26T15:15:00.789+08:00
- Header=X-Request-Id1, \d+
- MyHeader=X-Request-Id2, \d+
filters:
- StripPrefix=1
- AddRequestHeader=X-Request-Type, pc
5.2 filter工厂
新建HelloParameter过滤器工厂,参考内置AddRequestParameter过滤器工厂,注意命名规范,需要以GatewayFilterFactory结尾。
@Component
public class HelloParameterGatewayFilterFactory extends AbstractGatewayFilterFactory<HelloParameterGatewayFilterFactory.Config> {
private final Logger logger = LoggerFactory.getLogger(getClass());
public HelloParameterGatewayFilterFactory() {
super(HelloParameterGatewayFilterFactory.Config.class);
logger.info("Loaded HelloParameterGatewayFilterFactory [HelloParameter]");
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("key", "value");
}
@Override
public GatewayFilter apply(HelloParameterGatewayFilterFactory.Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate()
.header(config.key, config.value).build();
return chain.filter(exchange.mutate().request(request).build());
};
}
public static class Config {
private String key;
private String value;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
配置:
routes:
- id: hello1
# uri: http://localhost:8081
uri: lb://mtr-cloud-client1
predicates:
- Path=/aaa/demo/**,/ccc/demo/**
- After=2022-05-26T15:15:00.789+08:00
filters:
- StripPrefix=1
- AddRequestHeader=X-Request-Type, pc
- HelloParameter=wang, morning
5.3 全局filter
下面是一个模拟验证token的过滤器,如果请求Header中没有传递有效token,就会被拦截。
@Component
public class TokenGlobalFilter implements GlobalFilter, Ordered {
private final Logger logger = LoggerFactory.getLogger(getClass());
private static final String HEADER_KEY_TOKEN = "token";
@Override
public int getOrder() {
return 0;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("TokenGlobalFilter begin, uri:{}", exchange.getRequest().getURI());
HttpHeaders headers = exchange.getRequest().getHeaders();
String token = headers.getFirst(HEADER_KEY_TOKEN);
if (!this.checkToken(token)) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.TEXT_PLAIN);
//return response.writeWith(Flux.just(response.bufferFactory().wrap("token is invalid".getBytes())));
return response.writeAndFlushWith(Flux.just(ByteBufFlux.just(response.bufferFactory().wrap("token is invalid".getBytes()))));
}
return chain.filter(exchange);
}
private boolean checkToken(String value) {
return "wind".equals(value);
}
}
提示:不管是过滤器工厂,还是全局过滤器,都可以通过Ordered接口来调整处理顺序,都是在GatewayFilter实现类添加。