Gateway学习笔记一 responseBody和resquestBody的获取
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后二次封装。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通