1、简介 {#1简介}
Spring Cloud Gateway 的常见用例是作为一个网关,代理一个或多个服务,从而为客户端提供更简单的消费方式。
本文将带你了解如何在将请求发送到后端之前,通过重写 URL 来自定义暴露的 API 的不同方式。
2、Spring Cloud Gateway 快速回顾 {#2spring-cloud-gateway-快速回顾}
Spring Cloud Gateway 项目是在流行的 Spring Boot 2 和 Project Reactor 的基础上构建的,因此继承了其主要特性:
- 响应式,资源占用低
- 支持 Spring Cloud 生态系统的所有功能(服务发现、配置等)
- 使用标准 Spring 模式轻松扩展和/或定制
这里只列出它的主要概念,更多详细信息请参阅 中文文档:
Route
:路由,在网关中,匹配的传入请求会经历一系列的处理步骤。Predicate
:针对ServerWebExchange
进行评估的 Java 8 Predicate。Filters
:可以检查、更改ServerWebExchange
的GatewayFilter
实例。网关支持全局 Filter 和按路由的 Filter。
简而言之,接收请求的处理顺序如下:
- 网关使用与每条路由相关的
Predicate
来查找哪条路由可以处理请求。 - 一旦找到路由,请求(
ServerWebExchange
实例)就会通过每个配置的 Filter,直到最终发送到后端。 - 当后端发送回响应或出现错误(例如超时或连接重置)时,Filter 可以再次处理响应,然后再将其发送回客户端。
3、基于配置的 URL 重写 {#3基于配置的-url-重写}
回到本文的主题,让我们看看如何定义一个路由,在将请求发送到后端之前重写传入的 URL。例如,假设输入的请求格式为 /api/v1/customer/*
,后端 URL 应为 http://v1.customers/api/*
。这里,使用 "*" 来表示 "在此之后的任何内容"。
只需在应用的配置中添加几个属性,就可以创建基于配置的重写。为了更好的可读性,使用基于 YAML 的配置,这些信息可以来自任何受支持的 PropertySource
:
spring:
cloud:
gateway:
routes:
- id: rewrite_v1
uri: ${rewrite.backend.uri:http://example.com}
predicates:
- Path=/v1/customer/**
filters:
- RewritePath=/v1/customer/(?<segment>.*),/api/$\{segment}
分析一下这个配置。首先,路由有一个 id
,这只是它的标识符。其次,uri
属性给出了后端 URI。注意,这里只考虑了主机名/端口,因为最终路径来自重写逻辑。
predicates
属性定义了激活此路由必须满足的条件。在本例中,我们使用了 Path
predicate,它使用类似于 Ant 的路径表达式来匹配传入请求的路径。
最后,filters
属性具有实际的重写逻辑。RewritePath
Filter 需要两个参数:正则表达式和替换字符串。Filter 的实现方式是,使用提供的参数作为参数,在请求的 URI 上执行 replaceAll()
方法。
Spring 处理配置文件的方式有一个注意事项,那就是不能使用标准的 ${group}
替换表达式,因为 Spring 会认为这是一个属性引用,并尝试替换其值。为了避免这种情况,需要在 $
和 {
字符之间添加反斜杠,Filter 会在使用它作为实际替换表达式之前移除反斜杠。
4、基于 DSL 的 URL 重写 {#4基于-dsl-的-url-重写}
虽然 RewritePath
非常强大且易于使用,但在重写规则具有某些动态特性的情况下,它就显得力不从心了。根据情况,可以使用基于 DSL 的方法创建路由。我们需要做的就是创建一个 RouteLocator
Bean 来实现路由的逻辑。
举个例子,创建一个简单的路由,和上面一样,使用正则表达式重写传入的 URI。但这次,替换字符串将在每次请求时动态生成:
@Configuration
public class DynamicRewriteRoute {
@Value("${rewrite.backend.uri}")
private String backendUri;
private static Random rnd = new Random();
@Bean
public RouteLocator dynamicZipCodeRoute(RouteLocatorBuilder builder) {
return builder.routes()
.route("dynamicRewrite", r ->
r.path("/v2/zip/**")
.filters(f -> f.filter((exchange, chain) -> {
ServerHttpRequest req = exchange.getRequest();
addOriginalRequestUrl(exchange, req.getURI());
String path = req.getURI().getRawPath();
String newPath = path.replaceAll(
"/v2/zip/(?<zipcode>.*)",
"/api/zip/${zipcode}-" + String.format("%03d", rnd.nextInt(1000)));
ServerHttpRequest request = req.mutate().path(newPath).build();
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, request.getURI());
return chain.filter(exchange.mutate().request(request).build());
}))
.uri(backendUri))
.build();
}
}
在这里,动态部分只是将一个随机数添加到替换字符串中。在实际应用中可能会有更复杂的逻辑,但基本机制如下所示。
首先,它调用了 addOriginalRequestUrl()
方法,该方法来自 ServerWebExchangeUtils
类,用于将原始 URL 存储在 exchange attribute GATEWAY_ORIGINAL_REQUEST_URL_ATTR
下。该属性的值是一个 List,我们将在进行任何修改之前将接收到的 URL 追加到该 List 中,并且网关在处理 X-Forwarded-For
Header 时会内部使用该 List。
其次,应用重写逻辑后,必须将修改后的 URL 保存在 GATEWAY_REQUEST_URL_ATTR
exchange attribute 中。这一步在文档中没有直接提及,但可以确保我们的自定义 Filter 与其他可用 Filter 良好地协同工作。
5、测试 {#5测试}
使用标准的 JUnit 5 来测试我们的重写规则。
稍加改动:使用基于 Java SDK 的 com.sun.net.httpserver.HttpServer
类启动一个简单的服务器。使用随机端口,从而避免端口冲突。
不过,这种方法的缺点是,必须找出实际分配给服务器的端口,并将其传递给 Spring,以便使用它来设置路由的 uri
属性。幸运的是,Spring 为我们提供了一个优雅的解决方案: @DynamicPropertySource
,在此,使用它启动服务器,并使用绑定端口的值注册一个属性:
@DynamicPropertySource
static void registerBackendServer(DynamicPropertyRegistry registry) {
registry.add("rewrite.backend.uri", () -> {
HttpServer s = startTestServer();
return "http://localhost:" + s.getAddress().getPort();
});
}
测试 Handler 只需在响应体中回传接收到的 URI 即可。这样就能验证重写规则是否按预期运行。
@Test
void testWhenApiCall_thenRewriteSuccess(@Autowired WebTestClient webClient) {
webClient.get()
.uri("http://localhost:" + localPort + "/v1/customer/customer1")
.exchange()
.expectBody()
.consumeWith((result) -> {
String body = new String(result.getResponseBody());
assertEquals("/api/customer1", body);
});
}
6、总结 {#6总结}
本文介绍了在 Spring Cloud Gateway 中如何通过配置文件和 DSL 来重写路由 URL。
参考:https://www.baeldung.com/spring-cloud-gateway-url-rewriting