问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

接口调用现乱码和415错误,RestTemplate源码分析帮你定位解决

创作时间:
2025-01-22 00:52:07
作者:
@小白创作中心

接口调用现乱码和415错误,RestTemplate源码分析帮你定位解决

在项目重构过程中,接口调用出现乱码和415错误怎么办?本文通过对比新旧服务的配置差异,深入分析了RestTemplate的源码,解释了默认Content-Type的选择机制,并提供了明确的配置修改建议。

故事

近期公司对一个基础服务项目进行了重构。新服务上线后,在各上游系统的调用过程中发现了一些问题:

  1. 经过重构后,新上线的服务在调用A服务接口获取数据时出现了乱码问题;
  2. B服务在调用时出现了415错误,提示“Content type 'text/plain;charset=UTF-8' not supported”
  3. 剩余系统调用都正常;

在重构过程中,新旧服务代码几乎保持一致。本次重构的主要目的是针对架构升级,以便无缝支持云原生部署。

分析过程

对比新旧服务接口http协议参数

问题1:

  • 老服务的请求头响应Content-Type 多了charset=UTF-8
  • A服务调用接口使用了Apache的工具包,当HTTP响应头没有指定charset时,默认使用ISO_8859_1编码,因此导致了乱码问题

新项目新增配置:

server:
  servlet:
    encoding:
      charset: UTF-8
      enabled: true
      force: true  

至此新项目的接口响应:

在响应头中明确指定charset=UTF-8后,A项目调用接口时的乱码问题得到了有效解决。或者在A项目调用时指定编码也可以避免乱码问题,但由于项目中调用较多,这种方式改动较大。

问题2:

B服务在调用新服务时出现了错误,提示“Content type 'text/plain;charset=UTF-8' not supported”。但是,B服务并没有指定请求头中的Content-Type,在本地测试中也复现了这一问题

当不指定Content-Type时,默认采用的是
application/x-www-form-urlencoded;charset=UTF-8
类型,因此在解析body时出现错误。然而,B项目在调用时却报错显示
Content type 'text/plain;charset=UTF-8' not supported
。通过调查发现,B项目使用的是
RestTemplate

以下是源码分析:

  • RestTemplate默认设置
    RestTemplate
    在没有显式指定Content-Type的情况下,默认会选择某种Content-Type。这可能导致与预期不符的行为。

  • 自动选择Content-Type: 当没有明确设置Content-Type时,
    RestTemplate
    会根据请求的内容自动选择一个适当的Content-Type。如果请求内容是普通字符串或其他简单类型,
    RestTemplate
    可能会选择
    text/plain;charset=UTF-8

  • 解析器配置
    RestTemplate
    内部使用一组消息转换器(MessageConverters)来处理请求和响应。不同的消息转换器会处理不同的Content-Type。默认情况下,
    StringHttpMessageConverter
    会处理
    text/plain
    类型的请求和响应。

  • 解决方法

  • 显式设置Content-Type:在B项目中,明确设置请求头中的Content-Type为所需的类型,例如
    application/json

  • 自定义消息转换器:可以在配置
    RestTemplate
    时,自定义消息转换器,以确保其处理的Content-Type符合预期。

由于StringHttpMessageConverter的MediaType默认是text/plain,因此很容易理解为什么B项目在没有指定Content-Type的情况下报错为text/plain。

为什么B服务调用老项目没有问题,调用新项目确报错呢?

通过以下源码可以看出,该方法通过读取给定的HttpInputMessage来创建所需参数类型的方法参数值。参数包括:inputMessage,即当前请求参数的HTTP输入消息;targetType,即目标类型,不一定与方法参数类型相同,例如HttpEntity。方法返回创建的方法参数值。如果从请求中读取失败会抛出IOException,如果找不到合适的消息转换器会抛出HttpMediaTypeNotSupportedException

httpMessageConverter<?> converter : this.messageConverters 这行代码就是通过所有的Converter来解析body里面的内容,前提是需要匹配MediaType

protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
        MediaType contentType;
        boolean noContentType = false;
        try {
            contentType = inputMessage.getHeaders().getContentType();
        }
        catch (InvalidMediaTypeException ex) {
            throw new HttpMediaTypeNotSupportedException(ex.getMessage());
        }
        if (contentType == null) {
            noContentType = true;
            contentType = MediaType.APPLICATION_OCTET_STREAM;
        }
        Class<?> contextClass = parameter.getContainingClass();
        Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
        if (targetClass == null) {
            ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
            targetClass = (Class<T>) resolvableType.resolve();
        }
        HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
        Object body = NO_VALUE;
        EmptyBodyCheckingHttpInputMessage message;
        try {
            message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
            for (HttpMessageConverter<?> converter : this.messageConverters) {
                Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
                GenericHttpMessageConverter<?> genericConverter =
                        (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
                if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
                        (targetClass != null && converter.canRead(targetClass, contentType))) {
                    if (message.hasBody()) {
                        HttpInputMessage msgToUse =
                                getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
                        body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                                ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
                        body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
                    }
                    else {
                        body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
                    }
                    break;
                }
            }
        }
        catch (IOException ex) {
            throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
        }
        if (body == NO_VALUE) {
            if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
                    (noContentType && !message.hasBody())) {
                return null;
            }
            throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
        }
        MediaType selectedContentType = contentType;
        Object theBody = body;
        LogFormatUtils.traceDebug(logger, traceOn -> {
            String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
            return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
        });
        return body;
    }  

至此可以断定老服务新增自定义了Converter,并且Converter支持
text/plain 类型

通过代码查询发现老服务新增了
FastJsonHttpMessageConverter并支持MediaType.TEXT_PLAIN

至此B服务调用问题已经定位清楚,两种解决方式:

1:新服务新增Converter 来支持
text/plain , 但是这种不合适,应为本身提供跟调用方都是基于json的格式,只不过老服务刚好可以解析text/plain
方式,B服务调用的时候也没在意就没有指定
Content-Type,调用也是没问题。其实这种只是一个巧合上的碰撞,使用方式并不正确。

2:B服务调用的时候指定
Content-Type: application/json;charset=UTF-8"

本文原文来自CSDN

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号