# 网关:Gateway
Gateway 是 Java 微服务体系中的第二代服务网关,它是 Zuul 的替代品。
API 网关是一个服务,是系统的唯一入口。从面向对象设计的角度看,它与外观模式类似。API 网关封装了系统内部架构,为每个客户端提供一个定制的 API 。
# 0. 关于 Spring Cloud Netflix Zuul
Zuul 作为第一代网关,它相较于第二代网关 Gateway 而言,它最大的优势在于:它是基于 Servlet 的,因此学习曲线几乎为零。在并发量不高的情况下(仅在乎功能,而不在乎性能), Zuul 仍然是可选方案。
# 1. 关于 Spring Cloud Gateway
Spring Cloud Gateway 基于 Spring Boot 2 ,是 Spring Cloud 的全新项目。Gateway 旨在提供一种简单而有效的途径来转发请求,并为它们提供横切关注点。
Spring Cloud Gateway 中最重要的几个概念:
路由 Route:路由是网关最基础的部分,路由信息由一个 ID 、一个目的 URL 、一组断言工厂和一组 Filter 组成。如果路由断言为真,则说明请求的 URL 和配置的路由匹配。
断言 Predicate:Java 8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是 Spring 5.0 框架中的 ServerWebExchange 。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自 Http Request 中的任何信息,比如请求头和参数等。
过滤器 Filter:一个标准的 Spring Web Filter 。Spring Cloud Gateway 中的 Filter 分为两种类型:Gateway Filter 和 Global Filter 。过滤器 Filter 将会对请求和响应进行修改处理。
# 2. 入门案例
作为网关来说,网关最重要的功能就是协议适配和协议转发,协议转发也就是最基本的路由信息转发。
创建项目 gateway-server
,演示 Gateway 的基本路由转发功能,也就是通过 Gateway 的 Path 路由断言工厂实现 url 直接转发。
引入 Spring Cloud Gateway:Spring Cloud Routing > Gateway :
注意
Gateway 自己使用了 netty 实现了 Web 服务,此处『不需要引入 Spring Web』,如果引入了,反而还会报冲突错误,无法启动。
编写主入口程序代码,如下:
@SpringBootApplication public class GatewayServerApplication { public static void main(String[] args) { SpringApplication.run(GatewayServerApplication.class, args); } /** * 配置 */ @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(r -> r .path("/jd") .uri("http://www.jd.com:80/") .id("jd_route") ).build(); } }
上述代码配置了一个路由规则:当用户输入 /jd 路径时,Gateway 会将请求导向到 http://www.jd.com:80 (opens new window) 网址。
除了这种编码方式配置外,Gateway 还支持通过项目配置文件配置。例如:
当用户输入 /163 路径时,Gateway 将会导向到 http://www.163.com (opens new window) 网址。
两种配置方式可以同时使用。
# 3. Gateway 内置 Predicate
Spring Cloud Gateway 是由很多的路由断言工厂组成。当 HTTP Request 请求进入 Spring Cloud Gateway 的时候,网关中的路由断言工厂就会根据配置的路由规则,对 HTTP Request 请求进行断言匹配。匹配成功则进行下一步处理,否则,断言失败直接返回错误信息。
TIP
早期的 Gateway 断言的配置是通过代码中的 @Bean 进行配置,后来才推出配置文件配置。
前面 163
的示例中,我们使用的就是 Path 路由断言。
::: .yml 配置文件
spring:
cloud:
gateway:
routes:
- id: <id> # 路由 ID,唯一
uri: <目标 URL> # 目标 URI,路由到微服务的地址
predicates:
- Path=<匹配规则> # 支持通配符
- id: <id>
uri: <目标 URL>
predicates:
- Path=<匹配规则>
- id: <id>
uri: <目标 URL>
predicates:
- Path=<匹配规则>
- ...
:::
::: .properties 配置文件
spring.cloud.gateway.routes[0].id=<id>
spring.cloud.gateway.routes[0].uri=<目标 URL>
spring.cloud.gateway.routes[0].predicates[0]=Path=<匹配规则>
spring.cloud.gateway.routes[1].id=<id>
spring.cloud.gateway.routes[1].uri=<目标 URL>
spring.cloud.gateway.routes[1].predicates[0]=Path=<匹配规则>
spring.cloud.gateway.routes[2].id=<id>
spring.cloud.gateway.routes[2].uri=<目标 URL>
spring.cloud.gateway.routes[2].predicates[0]=Path=<匹配规则>
:::
例如:
Path 断言不会改变请求的 URI ,即,Gateway 收到的 URI 是什么样的,那么它将请求转给目标服务的时候,URI 仍然是什么。整个过程中只有 IP、端口部分会被『替换』掉 。
再重复一遍
Path 断言不会改变请求的 URI ,整个过程中只有 IP、端口部分会被『替换』掉 。
# 4. Gateway 整合 Nacos 注册中心实现路由
Gateway 整合 Nacos Sever(注册中心)之后,会以微服务的 name 和 URI 的对应关系为依据(利用 Path 路由断言),改变 url 访问路径(使用 RewritePath 过滤器),将访问请求 URL 的访问请求转给对应的微服务。
首先将 Gateway 视作普通的 Nacos Client 进行配置、启动。让其『连上』注册中心,从注册中心拉去各个微服务的信息(网址、端口等)。
略。
配置若干与 Gateway 相关的配置:
spring.cloud.gateway.discovery.locator.enabled=true spring.cloud.gateway.discovery.locator.lower-case-service-id=true # 降低日志级别,验证配置 logging.level.org.springframework.cloud.gateway=DEBUG
.locator.enabled :
该配置是 Gateway 与注册中心整合的开关项。必然要赋值为 true 。
.locator.lower-case-service-id :
true 表示出现在 url 中的服务名为全小写;false 表示出现在 url 中的服务名为全大写。
相较而言,大家看全小写英文会更为习惯一些。另外,一旦开启 lower-case,那么就不能用全大写了,而且大小写不能混用。
logging.level.org.springframework.cloud.gateway :
日志是非必要配置,这里配置成 DEBUG 级别是为了验证 Gateway 自动生成了 Path 断言规则。
先后启动『注册中心』、『服务提供者』和『Gateway』,访问 Gateway,并在访问路径中加上 /服务提供者的标识
,例如:/microservice-department/hello
,你会发现这个请求会被 Gateway 转给 microservice-department 的 /hello
。
并且,日志中会有类似如下一条信息:
Mapping [Exchange: GET http://localhost:9527/microservice-department/] to Route{id='ReactiveCompositeDiscoveryClient_MICROSERVICE-DEPARTMENT', uri=lb://MICROSERVICE-DEPARTMENT, order=0, predicate=Paths: [/microservice-department/**], match trailing slash: true, gatewayFilters=[[[RewritePath /microservice-department/(?<remaining>.*) = '/${remaining}'], order = 1]], metadata={management.port=8080}}
补充
如果你启动了多个服务提供者实例,Gateway 会自动实现基于轮循的负载均衡路由。
# 5. RewritePath 过滤器
RewritePath 过滤器可以重写 URI,去掉 URI 中的前缀。例如,下面的自己中就是去掉所有 URI 中的 /xxx/yyy/zzz 部分,只留之后的内容,再进行转发。
以上 Java 代码配置等同于 .yml 配置:
spring:
cloud:
gateway:
routes:
- id: 163_route
uri: http://localhost:8081
predicates:
- Path=/xxx/yyy/zzz/**
filters:
- RewritePath=/xxx/yyy/zzz/(?<segment>.*), /$\{segment}
对于请求路径 /xxx/yyy/zzz/hello ,当前的配置在请求到到达前会被重写为 /hello ,
命名分组:
(?<name>正则表达式)
与普通分组一样的功能,并且将匹配的子字符串捕获到一个组名称或编号名称中。在获得匹配结果时,可通过分组名进行获取。
(?<segment>.*)
:匹配 0 个或多个任意字符,并将匹配的结果捕获到名称为 segment 的组中。引用捕获文本:
${name}
将名称为 name 的命名分组所匹配到的文本内容替换到此处。
$\{segment}
:将前面捕获到 segment 中的文本置换到此处,注意,\ 的出现是由于避免 YAML 认为这是一个变量而使用的转义字符。
# 6. 自定义全局 Filter
自定义全局过滤器要实现 GlobalFilter 接口。全局过滤器不需要指定对哪个路由生效,它对所有路由都生效。
public class XxxGlobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 逻辑代码 ...
if (...) {
// 流程继续向下,走到下一个过滤器,直至路由目标。
return chain.filter(exchange);
} else {
// 否则流程终止,拒绝路由。
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
};
}
}
本质上,全局过滤器就是 GlobalFilter 接口的实现类的实例。你配置一个(或多个)GlobalFilter 类型的 @Bean,它(们)就是全局过滤器。他们只要存在,就会被 Gateway 使用到。
@Bean
@Order(-100) // 注解是为了去控制过滤器的先后顺序,值越小,优先级越高。
public GlobalFilter xxxGlobalFilter() {
return ...;
}
上面的全局过滤器的 filter 的逻辑结构所实现的功能:当条件成立时,允许路由;否则,直接返回。
TIP
在这种行驶中路由器的所有代码逻辑都是在『路由前』执行。
当然,这种形式的过滤器的更简单的情况是:执行某些代码,然后始终是放行。
这种逻辑结构的过滤器可以实现认证功能。
在过滤器中,你可以获得与当前请求相关的一些信息:
ServerHttpRequest request = exchange.getRequest();
log.info("{}", request.getMethod());
log.info("{}", request.getURI());
log.info("{}", request.getPath());
log.info("{}", request.getQueryParams()); // Get 请求参数
request.getHeaders().keySet().forEach(key -> {
log.info("{}: {}", key, request.getHeaders().get(key))
});
当然,你也可以单独地将自定义的 GlobalFilter 定义出来,然后在 @Bean 中进行配置:
@Bean
public GlobalFilter xxxGlobalFilter() {
return new XxxGlobalFilter();
}
# 7. JSON 形式的错误返回
上述的『拒绝』是以 HTTP 的错误形式返回,即 4xx、5xx 的错误。
有时,我们的返回方案是以 200 形式的『成功』返回,然后再在返回的信息中以自定义的错误码和错误信息的形式告知请求发起者请求失败。
此时,就需要 过滤器『成功』返回 JSON 格式的字符串:
String jsonStr = "{\"status\":\"-1\", \"msg\":\"error\"}";
byte[] bytes = jsonStr.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return exchange.getResponse().writeWith(Flux.just(buffer));
# 8. 获取 Body 中的请求参数(了解、自学)
由于 Gateway 是基于 Spring 5 的 WebFlux 实现的(采用的是 Reactor 编程模式),因此,从请求体中获取参数信息是一件挺麻烦的事情。
有一些简单的方案可以从 Request 的请求体中获取请求参数,不过都有些隐患和缺陷。
最稳妥的方案是模仿 Gateway 中内置的 ModifyRequestBodyGatewayFilterFactory,不过,这个代码写起来很啰嗦。
具体内容可参考这篇文章:Spring Cloud Gateway(读取、修改 Request Body) (opens new window)
不过考虑到 Gateway 只是做请求的『转发』,而不会承担业务责任,因此,是否真的需要在 Gateway 中从请求的 Body 中获取请求数据,这个问题可以斟酌。
# 9. 过滤器的另一种逻辑形式(了解、自学)
有时你对过滤器的运用并非是为了决定是否继续路由,为了在整个流程中『嵌入』额外的代码、逻辑:在路由之前和之后执行某些代码。
如果仅仅是在路由至目标微服务之前执行某些代码逻辑,那么 Filter 的形式比较简单:
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 逻辑代码 ...
// 流程继续向下,走到下一个过滤器,直至路由目标。
return chain.filter(exchange);
}
如果,你想在路由之前和之后(即,目标微服务返回之后)都『嵌入』代码,那么其形式就是:
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("目标微服务【执行前】执行");
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
log.info("目标微服务【执行后】执行");
}));
}
例如,显示一个用于统计微服务调用时长的过滤器:
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
long before = System.currentTimeMillis();
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
long after = System.currentTimeMillis();
System.out.println("请求耗时: " + (after - before) / 1000.0 + " 秒");
}));
}