1. 概述
攔截器(也稱為過濾器)是 Spring 中的一項特性,它允許我們攔截客户端請求。 這使我們能夠檢查和在控制器處理請求或將響應返回給客户端之前對其進行轉換。
在本教程中,我們將討論各種攔截客户端請求和使用 WebFlux 框架添加自定義標頭的方法。 我們首先將探索針對特定端點的實現方法。 然後,我們將確定攔截所有傳入請求的方法。
2. Maven 依賴
我們將使用以下 Maven 依賴項:spring-boot-starter-webflux,用於 Spring Framework 的 Reactive Web 支持。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>3.1.5</version>
</dependency>
3. 服務器請求攔截與轉換
Spring WebFlux 過濾器可以分為 WebFilter 和 HandlerFilterFunction 兩種類型。 我們將使用這些過濾器來攔截服務器的 Web 請求,並添加自定義頭部或修改現有頭部。
3.1. 使用 WebFilter
WebFilter 是一個用於以鏈式、攔截式的方式處理服務器 Web 請求的合約。WebFilter 全局生效,一旦啓用,便會攔截所有請求和響應。
首先,我們應該定義基於註解的控制器:
@GetMapping(value= "/trace-annotated")
public Mono<String> trace(@RequestHeader(name = "traceId") final String traceId) {
return Mono.just("TraceId: ".concat(traceId));
}
然後,我們攔截服務器的 Web 請求,並添加一個新的 HTTP 頭部 traceId,使用 TraceWebFilter 實現:
@Component
public class TraceWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
exchange.getRequest().mutate()
.header("traceId", "ANNOTATED-TRACE-ID");
return chain.filter(exchange);
}
}現在我們可以使用 WebTestClient 發送一個 GET 請求到 trace-annotated 端點,並驗證響應是否包含我們添加的 traceId 頭部值,該值是 “TraceId: ANNOTATED-TRACE-ID”:
@Test
void whenCallAnnotatedTraceEndpoint_thenResponseContainsTraceId() {
EntityExchangeResult<String> result = webTestClient.get()
.uri("/trace-annotated")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.returnResult();
String body = "TraceId: ANNOTATED-TRACE-ID";
assertEquals(result.getResponseBody(), body);
}關鍵在於,由於請求頭映射是隻讀的,我們無法直接修改響應頭部的請求頭:
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (exchange.getRequest().getPath().toString().equals("/trace-exceptional")) {
exchange.getRequest().getHeaders().add("traceId", "TRACE-ID");
}
return chain.filter(exchange);
}此實現會拋出 UnsupportedOperationException。
讓我們使用 WebTestClient 來驗證過濾器是否拋出異常,從而在向 trace-exceptional 端點發送 GET 請求後導致服務器錯誤。
@GetMapping(value = "/trace-exceptional")
public Mono<String> traceExceptional() {
return Mono.just("Traced");
}@Test
void whenCallTraceExceptionalEndpoint_thenThrowsException() {
EntityExchangeResult<Map> result = webTestClient.get()
.uri("/trace-exceptional")
.exchange()
.expectStatus()
.is5xxServerError()
.expectBody(Map.class)
.returnResult();
assertNotNull(result.getResponseBody());
}3.2. 使用 HandlerFilterFunction
在函數式風格中,一個路由器函數會攔截請求並調用相應的處理函數。
我們可以啓用零個或多個 HandlerFilterFunction,它們作為函數過濾 HandlerFunction。 這些 HandlerFilterFunction 實現僅適用於基於路由的方案。
對於函數式端點,我們首先需要創建一個處理函數:
@Component
public class TraceRouterHandler {
public Mono<ServerResponse> handle(final ServerRequest serverRequest) {
String traceId = serverRequest.headers().firstHeader("traceId");
assert traceId != null;
Mono<String> body = Mono.just("TraceId: ".concat(traceId));
return ok().body(body, String.class);
}
}
在配置了處理程序並使用路由器配置後,我們攔截了服務器的 Web 請求,並使用 TraceHandlerFilterFunction 實現,添加了新的 traceId 標頭。
public RouterFunction<ServerResponse> routes(TraceRouterHandler routerHandler) {
return RouterFunctions
.route(GET("/trace-functional-filter"), routerHandler::handle)
.filter(new TraceHandlerFilterFunction());
}public class TraceHandlerFilterFunction implements HandlerFilterFunction<ServerResponse, ServerResponse> {
@Override
public Mono<ServerResponse> filter(ServerRequest request, HandlerFunction<ServerResponse> handlerFunction) {
ServerRequest serverRequest = ServerRequest.from(request)
.header("traceId", "FUNCTIONAL-TRACE-ID")
.build();
return handlerFunction.handle(serverRequest);
}
}
我們現在可以驗證響應是否包含我們添加的 traceId 標頭值,即在觸發對 trace-functional-filter 終點的 GET 調用後,該值是 "TraceId: FUNCTIONAL-TRACE-ID"。
@Test
void whenCallTraceFunctionalEndpoint_thenResponseContainsTraceId() {
EntityExchangeResult<String> result = webTestClient.get()
.uri("/trace-functional-filter")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.returnResult();
String body = "TraceId: FUNCTIONAL-TRACE-ID";
assertEquals(result.getResponseBody(), body);
}
3.3. 使用自定義處理器Function
處理器函數類似於路由器函數,它會攔截請求並調用相應的處理函數。
功能路由 API 允許我們添加零個或多個自定義Function 實例,這些實例在調用HandlerFunction之前應用。
此過濾器函數攔截由構建器創建的服務器 Web 請求,並添加新的標頭 traceId:
public RouterFunction<ServerResponse> routes(TraceRouterHandler routerHandler) {
return route()
.GET("/trace-functional-before", routerHandler::handle)
.before(request -> ServerRequest.from(request)
.header("traceId", "FUNCTIONAL-TRACE-ID")
.build())
.build());
}在向 trace-functional-before 端點發送 GET 請求後,讓我們驗證響應是否包含我們添加的 traceId 標頭的值,即 “TraceId: FUNCTIONAL-TRACE-ID“:
@Test
void whenCallTraceFunctionalBeforeEndpoint_thenResponseContainsTraceId() {
EntityExchangeResult<String> result = webTestClient.get()
.uri("/trace-functional-before")
.exchange()
.expectStatus()
.isOk()
.expectBody(String.class)
.returnResult();
String body = "TraceId: FUNCTIONAL-TRACE-ID";
assertEquals(result.getResponseBody(), body);
}4. 客户端請求攔截與轉換
我們將使用 ExchangeFilterFunctions 來攔截使用 Spring WebClient 的客户端請求。
4.1. 使用 ExchangeFilterFunction
ExchangeFilterFunction 是與 Spring WebClient 相關的術語。我們使用它來攔截帶有 WebFlux WebClient 的客户端請求。ExchangeFilterFunction 用於在發送請求之前或接收請求之後轉換請求或響應。
讓我們定義一個交換過濾器函數來攔截 WebClient 請求並添加新的標頭 traceId。我們將跟蹤所有請求標頭以驗證 ExchangeFilterFunction:
public ExchangeFilterFunction modifyRequestHeaders(MultiValueMap<String, String> changedMap) {
return (request, next) -> {
ClientRequest clientRequest = ClientRequest.from(request)
.header("traceId", "TRACE-ID")
.build();
changedMap.addAll(clientRequest.headers());
return next.exchange(clientRequest);
};
}由於我們已經定義了過濾器函數,因此我們可以將其附加到 WebClient 實例上。這隻能在創建 WebClient 時完成。
public WebClient webclient() {
return WebClient.builder()
.filter(modifyRequestHeaders(new LinkedMultiValueMap<>()))
.build();
}我們現在可以使用 Wiremock 來測試自定義的 ExchangeFilterFunction:。
@RegisterExtension
static WireMockExtension extension = WireMockExtension.newInstance()
.options(wireMockConfig().dynamicPort().dynamicHttpsPort())
.build();@Test
void whenCallEndpoint_thenRequestHeadersModified() {
extension.stubFor(get("/test").willReturn(aResponse().withStatus(200)
.withBody("SUCCESS")));
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
WebClient webClient = WebClient.builder()
.filter(modifyRequestHeaders(map))
.build();
String receivedResponse = triggerGetRequest(webClient);
String body = "SUCCESS";
Assertions.assertEquals(receivedResponse, body);
Assertions.assertEquals("TRACE-ID", map.getFirst("traceId"));
}最後,藉助 Wiremock,我們驗證了 ExchangeFilterFunction,通過檢查新頭 traceId 是否存在於 MultivalueMap 實例中。
5. 結論
在本文中,我們探討了攔截和為服務器 Web 請求和 Web 客户端請求添加自定義標頭的不同方法。
首先,我們研究瞭如何使用 WebFilter 和 HandlerFilterFunction 為服務器 Web 請求添加自定義標頭。隨後,我們討論瞭如何使用 ExchangeFilterFunction 為 WebClient 請求執行相同的操作。