You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 00:00:55