1 引言
筆者在實現開發者服務網關模塊的任務過程中,遇到下列需求(有關requestBody和responseBody部分):
- 對所有的請求,取出requestBody作為參數,調用鑒權接口
- 不影響requsetBody前提下,路由轉發
- 從路由轉發的回復中取出responseBody,作為參數調用統計接口
gateway的工作流程如圖,filter的傳遞中,我們通常用ServerWebExchange來獲取請求、回復、相關參數等。
我最初的思路,想當然地希望從ServerWebExchange中直接使用exchange.getXXX()方法獲取body,
2 原因分析
2.1 ServerWebExchange
ServerWebExchange命名為服務網絡交換器,存放着重要的請求-響應屬性、請求實例和響應實例等等,有點像Context的角色
ServerWebExchange實例可以理解為不可變實例,如果我們想要修改它,需要通過mutate()方法生成一個新的實例
2.2 ServerHttpRequest
ServerHttpRequest實例是用於承載請求相關的屬性和請求體。
Spring Cloud Gateway中底層使用Netty處理網絡請求,通過追溯源碼,可以從ReactorHttpHandlerAdapter中得知ServerWebExchange實例中持有的ServerHttpRequest實例的具體實現是ReactorServerHttpRequest
ReactorServerHttpRequest的父類AbstractServerHttpRequest中初始化內部屬性headers的時候把請求的HTTP頭部封裝為只讀的實例,所以不能直接從ServerHttpRequest實例中直接獲取請求實例並且進行修改。
3 解決方案
3.1 基於ReadBodyPredicateFactory的實現
ReadBodyPredicateFactory源碼指出,body只允許從request中讀取一次,再次讀取時會拋異常。因此對於已經讀取過的requestBody,為了不影響后期,需要對請求體內容進行二次包裝,即第一次讀取內容進行緩存,后面對同個請求體的讀取則直接返回緩存內容。
1 public final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders(); 2 3 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { 4 log.info("仿照ReadBodyPredicateFactory的方式獲取body---------成功"); 5 return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, 6 (serverHttpRequest) -> ServerRequest 7 //用mutate()方法重新封裝 8 .create(exchange.mutate().request(serverHttpRequest) 9 .build(), messageReaders) 10 //獲取完整的body內容,轉成string 11 .bodyToMono(String.class) 12 .doOnNext(bodyString -> { 13 //以下是業務邏輯,把bodyString去除空格換行,再放入attributes中 14 bodyString = bodyString.replaceAll("\r\n", ""); 15 bodyString = bodyString.replaceAll(" ", ""); 16 exchange.getAttributes().put( 17 Constants.AUTH_SIGN_VO_REQUEST_BODY, bodyString); 18 log.info("放入參數的body為:{}", bodyString); 19 }) 20 .then(chain.filter(exchange))); 21 }
用cacheRequestBodyAndRequest方法緩存了requestBody,本質上還是用ServerHttpRequestDecorator重新封裝了requestBody,覆蓋對應獲取請求體數據緩沖區,以達到多次讀取的目的。
缺陷就是,這里的bodyToMono的class寫死為string,不夠靈活。
3.2 基於ModifyRequestBodyGatewayFilterFactory的實現
官網提供了ModifyRequestBodyFilter和ModifyResponseBodyFilter來獲取修改body,但僅支持以Java DSL的方式來配置。
@Bean public RouteLocator routes(RouteLocatorBuilder builder) { return builder.routes() .route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org") .filters(f -> f.prefixPath("/httpbin") .modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE, (exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri)) .build(); } @Bean public RouteLocator routes(RouteLocatorBuilder builder) { return builder.routes() .route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org") .filters(f -> f.prefixPath("/httpbin") .modifyResponseBody(String.class, String.class, (exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri)) .build(); }
而在開發中,我們更傾向於使用yml配置的方式來配置filter,為了靈活開發,我在這里仿照ModifyRequestBodyFilter來實現了自己的全局過濾器
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders); Mono<String> modifiedBody = serverRequest.bodyToMono(String.class) //bodyToMono獲取body的內容 .flatMap( data -> { try { byte[] body = data.getBytes(StandardCharsets.UTF_8); //以下是具體業務邏輯 String bodyString = new String(body); //刪除所有換行和空格 bodyString = bodyString.replaceAll("\r\n", ""); bodyString = bodyString.replaceAll(" ", ""); log.debug("request body string is :{}", bodyString); //把bodyString放入attribute供后續使用 exchange.getAttributes().put( Constants.AUTH_SIGN_VO_REQUEST_BODY, bodyString); } catch (ExceptionWithErrorCode e) { return Mono.error(e); } return Mono.just(data); }); //重新封裝修改后的body,本業務中實際無修改,但也必須重新封裝body BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class); HttpHeaders headers = new HttpHeaders(); headers.putAll(exchange.getRequest().getHeaders()); headers.remove(HttpHeaders.CONTENT_LENGTH); CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers); return bodyInserter.insert(outputMessage, new BodyInserterContext()) // .log("modify_request", Level.INFO) .then(Mono.defer(() -> { ServerHttpRequestDecorator decorator = CommonUtils.decorate(exchange, headers, outputMessage); //decorate重新封裝 return chain.filter(exchange.mutate().request(decorator).build()); })); }
- 失敗嘗試
用正確實現取body,但不使用bodyInserter重新封裝request
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders); return serverRequest.bodyToMono(String.class) .flatMap( //同上實現,省略 …… }).then(chain.filter(exchange)); }
結果,每兩次調用,第二次都會報錯
3.3 思路三
筆者在查詢資料過程中,看到網上博文提供了第三種解決思路:
由於從exchange.getRequest中獲取的是FluxMap類型,因此可以通過重寫Flux<DataBuffer> getBody()方法,包裝后的請求放到過濾器鏈中傳遞下去。這樣后面的過濾器中再使用exchange.getRequest().getBody()來獲取body時,實際上就是調用的重載后的getBody方法,獲取的最先已經緩存了的body數據。這樣就能夠實現body的多次讀取了。
不過這種思路同樣繞不過要使用ServerHttpRequestDecorator這個請求裝飾器對request進行重新包裝。
3.4 獲取responseBody
responseBody的獲取方法也是同樣的思路,可以參找ModifyResponseBodyGatewayFilterFactory的實現方式
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpResponse originalResponse = exchange.getResponse(); originalResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON); DataBufferFactory bufferFactory = originalResponse.bufferFactory(); ServerHttpResponseDecorator response = new ServerHttpResponseDecorator(originalResponse) { @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { if (getStatusCode().equals(HttpStatus.OK) && body instanceof Flux) { Flux<? extends DataBuffer> fluxBody = Flux.from(body); return super.writeWith(fluxBody.buffer().map(dataBuffers -> { DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); //dataBuffer合並成一個,解決獲取結果不全問題 DataBuffer join = dataBufferFactory.join(dataBuffers); byte[] content = new byte[join.readableByteCount()]; join.read(content); DataBufferUtils.release(join); // 轉為字符串 String responseData = new String(content, Charsets.UTF_8); /** 業務邏輯 */ //去除所有換行和空格 responseData = responseData.replaceAll("\r\n", ""); responseData = responseData.replaceAll(" ", ""); log.debug("responseBody string is:{}", responseData); exchange.getAttributes().put(Constants.AUTH_SIGN_VO_RESPONSE_BODY, responseData); byte[] uppedContent = responseData.getBytes(Charsets.UTF_8); originalResponse.getHeaders().setContentLength(uppedContent.length); return bufferFactory.wrap(uppedContent); })); } return super.writeWith(body); } @Override public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) { return writeWith(Flux.from(body).flatMapSequential(p -> p)); } }; return chain.filter(exchange.mutate().response(response).build()); }
4 總結
- ServerWebExchange中存放着重要的請求-響應屬性、請求實例和響應實例,類似於上下文
- ReadBodyPredicateFactory源碼指出,body只允許從request中讀取一次,再次讀取時會拋異常。因此需要二次封裝
- bodyToMono()方法用於獲取body內容,ServerHttpResponseDecorator用於重新封裝請求,ServerWebExchange.mutate()方法用於重新生成實例
- 可以參照ModifyRequestBodyGatewayFilterFactory 和ReadBodyPredicateFactory實現上述需求,本質都是獲取並緩存body后二次封裝。