Spring Boot升级至3.2.5后RestTemplate POST请求报JSON解析错误
我们的微服务原本基于Spring Boot 3.1.0版本,升级至3.2.5版本后,使用RestTemplate发起的POST请求失败,抛出异常:
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Unexpected end-of-input in VALUE_STRING
对比升级前后的JSON数据与HttpHeaders未发现差异,但仍无法定位问题。当前技术栈为:Java 17、Spring Boot 3.2.5、httpclient5 5.3.1、Netty-handler 4.1.109.Final,请求返回400 Bad Request,需解决该问题。
相关代码
RestTemplate配置类
package com.ual.xclr.common.config; import com.ual.xclr.common.interceptor.RestTemplateReqResLoggingInterceptor; import com.ual.xclr.common.utility.XclrUtils; import lombok.extern.slf4j.Slf4j; import org.apache.http.conn.ssl.NoopHostnameVerifier; import org.apache.http.conn.ssl.TrustStrategy; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.io.HttpClientConnectionManager; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.core5.util.Timeout; //import org.apache.http.impl.client.HttpClients; //import org.apache.http.impl.client.HttpClientBuilder; //import org.apache.http.impl.client.CloseableHttpClient; //import org.apache.http.conn.ssl.SSLConnectionSocketFactory; //import org.apache.http.client.config.RequestConfig; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Scope; import org.springframework.http.client.BufferingClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import javax.net.ssl.SSLContext; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Collections; @Configuration @Slf4j public class RestTemplateConfig { @Value("${restTemplateDefaultConnectionTimeOut:600000}") // 10 Min private int restTemplateDefaultConnectionTimeOut; @Value("${restTemplateDefaultReadTimeOut:600000}") private int restTemplateDefaultReadTimeOut; @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) @Primary public RestTemplate getRestTemplate(RestTemplateBuilder builder) { RestTemplate restTemplate = builder.requestFactory(this::getClientHttpRequestFactory) .interceptors(new RestTemplateReqResLoggingInterceptor()) .build(); return restTemplate; } private BufferingClientHttpRequestFactory getClientHttpRequestFactory() { try { TrustStrategy acceptingTrustStrategy = (X509Certificate[] x509Certificates, String s) -> true; SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy) .build(); SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier()); HttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() .setSSLSocketFactory(csf) .build(); CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(connectionManager).build(); HttpComponentsClientHttpRequestFactory httpFactory = new HttpComponentsClientHttpRequestFactory(); httpFactory.setConnectTimeout(restTemplateDefaultConnectionTimeOut); httpFactory.setConnectionRequestTimeout(restTemplateDefaultConnectionTimeOut); httpFactory.setHttpClient(httpClient); return new BufferingClientHttpRequestFactory(httpFactory); } catch (Exception e) { log.error("There is an exception in creating RestTemplate i.e. -{}", XclrUtils.crlfInjectionNeutralizer(e, true, true)); return null; // You might want to handle this differently based on your use case } } }
RestTemplate请求代码
try { httpHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML)); httpHeaders.setContentType(MediaType.valueOf("application/json;charset=UTF-8")); String xclradaptercargomailpublishurl = adapterMSKUrl + "/publishCargoMailEvent"; String compositeKey = null; if (httpHeaders.containsKey(AppConstants.OPS_FLT_NBR)) { StringBuilder stringBuilder = new StringBuilder(); compositeKey = stringBuilder.append(UAX_CHECKOUT_MAIL_ACK).append(AppConstants.DoubleColon) .append(httpHeaders.get(AppConstants.OPS_CARR_IATA_CD).get(0)).append(AppConstants.DoubleColon) .append(httpHeaders.get(AppConstants.OPS_FLT_NBR).get(0)).append(AppConstants.DoubleColon) .append(httpHeaders.get(AppConstants.FLT_LEG_DEP_DT).get(0)).append(AppConstants.DoubleColon) .append(httpHeaders.get(AppConstants.OPS_LEG_DEP_ARPT_IATA_CD).get(0)) .append(AppConstants.DoubleColon) .append(httpHeaders.get(AppConstants.OPS_LEG_ARR_ARPT_IATA_CD).get(0)) .append(AppConstants.DoubleColon).append(httpHeaders.get(AppConstants.ORIG_OCCUR_NBR).get(0)) .toString(); } XCLRMsg xclrMsg = new XCLRMsg(); xclrMsg.setKey(compositeKey); String message = XclrUtils.convertJavaObjectToJson(cargoMailAckMessage); log.info("Safwan Testing cargomailmsg {}",message); xclrMsg.setMessage(message); HttpEntity<XCLRMsg> entity = new HttpEntity<>(xclrMsg, httpHeaders); log.info("Safwan Testing xclrmsgobj {}",xclrMsg); log.info("Safwan Testing HttpEntity {}",entity); restTemplate.exchange(xclradaptercargomailpublishurl, HttpMethod.POST, entity, String.class); } catch (Exception e) { // NOSONAR log.error("Could not publish acknowledgment to CargoMail topic - {} for headers {}", XclrUtils.crlfInjectionNeutralizer(e,true,true),XclrUtils.sanitizeHttpHeadersForLogging(httpHeaders)); }
排查与解决方案
1. 检查日志拦截器的流处理
你使用了BufferingClientHttpRequestFactory配合RestTemplateReqResLoggingInterceptor,如果拦截器中直接读取请求体的输入流但未重置,会导致实际发送的请求体为空或不完整,触发服务端JSON解析错误。
解决方式:
确保拦截器使用传入的byte[] body参数打印请求内容,而非读取request.getBody()流。示例拦截器实现:
public class RestTemplateReqResLoggingInterceptor implements ClientHttpRequestInterceptor { private static final Logger log = LoggerFactory.getLogger(RestTemplateReqResLoggingInterceptor.class); @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { // 打印请求体 if (body.length > 0) { log.info("Request Body: {}", new String(body, StandardCharsets.UTF_8)); } // 执行请求 ClientHttpResponse response = execution.execute(request, body); // 打印响应体 String responseBody = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8); log.info("Response Body: {}", responseBody); // 重置响应流,避免后续解析失败 response.getBody().reset(); return response; } }
2. 避免JSON二次序列化
请求代码中手动将cargoMailAckMessage转成JSON字符串,再放入XCLRMsg对象中由RestTemplate再次序列化,会导致JSON字符串被转义(比如{"a":1}变成"{\"a\":1}"),如果服务端期望message字段是JSON对象而非字符串,就会触发解析错误。
解决方式:
- 修改
XCLRMsg的message字段类型为Object或对应的实体类(比如CargoMailAckMessage) - 直接设置对象而非手动转JSON:
// 去掉手动转JSON的代码 // String message = XclrUtils.convertJavaObjectToJson(cargoMailAckMessage); xclrMsg.setMessage(cargoMailAckMessage);
3. 验证手动序列化的JSON有效性
如果必须保留手动转JSON的逻辑,先校验日志中cargoMailmsg对应的字符串是否为合法JSON:
- 复制日志中的JSON字符串到JSON校验工具检查格式是否完整,是否存在未闭合的引号、缺失逗号等问题
- 排查
XclrUtils.convertJavaObjectToJson方法是否存在序列化bug,比如处理特殊字符(如换行、引号)时出错
4. 检查Jackson配置变化
Spring Boot 3.2.x升级了Jackson版本(从2.15.x到2.16.x),默认配置可能发生变化:
- 检查是否开启了严格的JSON校验,比如
DeserializationFeature.FAIL_ON_TRAILING_TOKENS - 可以显式配置Jackson的序列化规则,避免默认行为影响:
@Bean public ObjectMapper objectMapper() { return new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true); }
5. 简化ContentType设置
原代码中使用MediaType.valueOf("application/json;charset=UTF-8"),Spring 6+中推荐直接使用MediaType.APPLICATION_JSON(UTF-8是默认编码),避免编码格式设置异常:
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
内容的提问来源于stack exchange,提问作者Safwan Modak

