Buffering包装下Jdk与SimpleClientHttpRequestFactory遇IOException的行为差异
在配置Spring RestClient时,为方便开发阶段记录响应,用BufferingClientHttpRequestFactory包装了请求工厂。但调用返回4XX错误状态码的接口时,JdkClientHttpRequestFactory能正常一次读取响应体,而SimpleClientHttpRequestFactory需要两次调用clientHttpResponse.getBody()(先捕获IOException再读取)才能拿到响应体,以下是该差异的成因解析。
配置代码
@Bean public RestClient restClient() { // 在SimpleClientHttpRequestFactory和JdkClientHttpRequestFactory间切换 final var clientHttpRequestFactory = new SimpleClientHttpRequestFactory(); return RestClient.builder() .requestFactory(new BufferingClientHttpRequestFactory(clientHttpRequestFactory)) .requestInterceptors(list -> list.add(new LoggingClientHttpRequestInterceptor())) .build(); }
日志拦截器代码
@Slf4j private static class LoggingClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { // 执行REST调用 final var clientHttpResponse = execution.execute(request, body); // 复制并记录响应体 try { // 第一次读取InputStream log.info("#1 attempt"); final var response = StreamUtils.copyToString(clientHttpResponse.getBody(), UTF_8); log.info("#1 status={}, response={}", clientHttpResponse.getStatusCode().value(), response); } catch (IOException e) { // 第二次读取InputStream log.info("#2 attempt"); final var response = StreamUtils.copyToString(clientHttpResponse.getBody(), UTF_8); log.info("#2 status={}, response={}", clientHttpResponse.getStatusCode().value(), response); } // 返回客户端响应 return clientHttpResponse; } }
两种工厂的日志输出对比
- JdkClientHttpRequestFactory(预期行为):
2024-01-04T15:25:41.138+07:00 INFO 62777 --- [main] : #1 attempt 2024-01-04T15:25:41.140+07:00 INFO 62777 --- [main] : #1 status=400, response={"foo": "bar"} 2024-01-04T15:25:41.141+07:00 INFO 62777 --- [main] : Done
- SimpleClientHttpRequestFactory(意外行为):
2024-01-04T15:26:41.518+07:00 INFO 62839 --- [main] : #1 attempt 2024-01-04T15:26:41.767+07:00 INFO 62839 --- [main] : #2 attempt 2024-01-04T15:26:41.769+07:00 INFO 62839 --- [main] : #2 status=400, response={"foo": "bar"} 2024-01-04T15:26:41.771+07:00 INFO 62839 --- [main] : Done
行为差异成因
SimpleClientHttpRequestFactory的底层逻辑
它基于JDK的HttpURLConnection实现。当响应状态码为4XX时,HttpURLConnection默认将响应内容存入错误流(error stream),而非普通输入流。第一次调用getBody()时,它尝试读取普通输入流,此时流为空,直接抛出IOException;第二次调用时,底层会自动切换到读取错误流,从而成功获取响应体。JdkClientHttpRequestFactory的处理方式
它基于JDK 11+的HttpClient实现,该客户端不区分普通输入流和错误流——无论状态码是否为4XX,响应内容都通过同一个输入流返回,因此第一次调用getBody()就能正常读取到内容。BufferingClientHttpRequestFactory的作用限制
它仅负责缓存响应体,不会修改底层请求工厂对错误流的处理逻辑,因此两种工厂的原生行为差异依然会暴露出来。
内容的提问来源于stack exchange,提问作者Nikolas

