RestTemplate发起GET请求后挂起问题排查求助
从你描述的现象来看,核心矛盾点非常明确:用String接收响应秒级返回,用自定义POJO就阻塞30-60秒,结合curl的错误提示transfer closed with 79 bytes remaining to read,大概率是HTTP响应的Content-Length与实际响应体大小不匹配,导致客户端的反序列化组件一直在等待“缺失”的字节,最终触发超时。下面给你一步步的排查和解决建议:
1. 先验证Content-Length的准确性
这是最关键的第一步,直接用curl -v http://friend:5000/users请求第三方服务,重点看响应头里的Content-Length值;然后把响应体保存到文件(比如curl http://friend:5000/users > response.json),用wc -c response.json计算实际响应体的字节数。
如果两者数值不一致,问题根源就在第三方服务:它返回了错误的Content-Length,导致RestTemplate的Jackson消息转换器在读取响应流时,一直等待到超时才结束。这种情况下,你需要联系服务方修复这个HTTP头的问题。
2. 绕过RestTemplate自动反序列化,手动处理
如果暂时没法让服务方修复,可以先绕过RestTemplate的自动反序列化流程,先把响应读成String,再手动转成POJO,这样能避免流阻塞的问题:
@GetMapping("/todos/{toDoNoteId}/users") public ResponseEntity getNotesUsersTest(@PathVariable int toDoNoteId) { // 省略前置校验逻辑 RestTemplate restTemplate = new RestTemplate(); final String uri = "http://friend:5000/users"; try { HttpHeaders requestHeaders = new HttpHeaders(); requestHeaders.setContentType(MediaType.APPLICATION_JSON); HttpEntity<String> entity = new HttpEntity<>("parameters", requestHeaders); // 先获取String响应 ResponseEntity<String> stringResult = restTemplate.exchange(uri, HttpMethod.GET, entity, String.class); // 手动反序列化 ObjectMapper objectMapper = new ObjectMapper(); ArrayResponsePojo pojo = objectMapper.readValue(stringResult.getBody(), ArrayResponsePojo.class); return ResponseEntity.ok(pojo); } catch (HttpClientErrorException ex) { // 异常处理逻辑 } catch (JsonProcessingException e) { // 反序列化异常处理 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("反序列化失败"); } }
这种方式如果能快速返回,就进一步验证了是自动反序列化时的流读取阻塞问题。
3. 调整RestTemplate的消息转换器配置
默认的MappingJackson2HttpMessageConverter可能在处理响应流时,因为Content-Length不匹配导致阻塞。你可以尝试修改配置,关闭缓冲:
// 配置请求工厂关闭缓冲 SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setBufferRequestBody(false); RestTemplate restTemplate = new RestTemplate(factory); // 或者自定义Jackson消息转换器,关闭缓冲 MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setBufferRequestBody(false); restTemplate.getMessageConverters().add(0, converter);
4. 深挖线程挂起的具体细节
你提到线程长时间挂起在ThreadPoolExecutor.afterExecute或LockSupport.park,可以用jstack <你的应用PID>导出线程栈,找到挂起的线程查看调用栈:
- 如果停在
InputStream.read()相关方法,那100%是流读取阻塞,等待服务端发送剩余字节; - 如果停在Jackson的反序列化方法里,可能是POJO的字段映射有隐藏问题(比如有未知字段导致Jackson反复尝试解析,但这种情况一般会抛异常而非阻塞)。
5. 检查服务端的传输编码合规性
用curl -v看响应头,如果同时存在Transfer-Encoding: chunked和Content-Length,这是违反HTTP规范的(Chunked编码下不应返回Content-Length),会导致客户端解析混乱出现阻塞。这种情况同样需要服务方修复。
6. 排除POJO反序列化的隐藏问题
虽然你说POJO有正确的getter、setter和无参构造,但可以尝试给POJO加上@JsonIgnoreProperties(ignoreUnknown = true),避免Jackson因为响应体里有未知字段而出现异常(极端场景下可能导致阻塞):
@JsonIgnoreProperties(ignoreUnknown = true) public class ArrayResponsePojo { private User[] data; private String message; // getter、setter及无参构造方法 } @JsonIgnoreProperties(ignoreUnknown = true) public class User { private String email; private String firstName; private String lastName; // getter、setter及无参构造方法 }
补充说明
你提到浏览器请求快而Postman/curl慢,是因为浏览器的HTTP客户端实现比较“宽松”,会在流关闭时就停止读取;而Postman、curl以及RestTemplate的默认实现会严格按照Content-Length来读取,所以会一直等待到超时。
内容的提问来源于stack exchange,提问作者Svajunas Kavaliauskas

