接口调用现乱码和415错误,RestTemplate源码分析帮你定位解决
接口调用现乱码和415错误,RestTemplate源码分析帮你定位解决
在项目重构过程中,接口调用出现乱码和415错误怎么办?本文通过对比新旧服务的配置差异,深入分析了RestTemplate的源码,解释了默认Content-Type的选择机制,并提供了明确的配置修改建议。
故事
近期公司对一个基础服务项目进行了重构。新服务上线后,在各上游系统的调用过程中发现了一些问题:
- 经过重构后,新上线的服务在调用A服务接口获取数据时出现了乱码问题;
- B服务在调用时出现了415错误,提示“Content type 'text/plain;charset=UTF-8' not supported”
- 剩余系统调用都正常;
在重构过程中,新旧服务代码几乎保持一致。本次重构的主要目的是针对架构升级,以便无缝支持云原生部署。
分析过程
对比新旧服务接口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
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