1. 问题复现
话不多说,先贴出问题代码:这里的GetUserInfoByAccessToken
是我自定义的一个实体类。
GetUserInfoByAccessToken getUserInfoByAccessTokenString = restTemplate.getForObject(userInfoByAccessCodeURL, GetUserInfoByAccessToken.class);
异常信息:Could not extract response: no suitable HttpMessageConverter
found for response type [class wechat.wxRes.GetUserInfoByAccessToken] and content type [text/plain],很明显这段异常的意思是在指定返回类型为GetUserInfoByAccessToken,并且服务端响应报文的content-type为text/plain的情况下找不到一个合适的HttpMessageConverter
来处理这种情况
2. 处理方法
这里举例两种处理请求
1.首先StringHttpMessageConverter
这个处理器是可以处理content-type为text/plain的响应报文的。但阅读源码知道必须放回类型是String才可以使用它,所有我们需要改写下代码,将放回类型改为String。需要的时候可以利用JSON
工具类将其转为你需要的类型。
GetUserInfoByAccessToken getUserInfoByAccessTokenString = restTemplate.getForObject(userInfoByAccessCodeURL, String.class);
需要注意的是使用StringHttpMessageConverter
容易出现中文乱码的情况,因为它默认支持的字符集是ISO-8859-1
,这种时候可以参考以下代码更改StringHttpMessageConverter
的默认字符集,我这里将其改为utf-8
了。
RestTemplate customRestTemplate = new RestTemplate();
List<HttpMessageConverter<?>> list = customRestTemplate.getMessageConverters();
for (HttpMessageConverter<?> httpMessageConverter : list) {
if(httpMessageConverter instanceof StringHttpMessageConverter) {
((StringHttpMessageConverter) httpMessageConverter).setDefaultCharset(Charset.forName("utf-8"));
break;
}
}
2.往restTemplate
的转换器里再加一个支持JSON
转换的转换器,比如MappingJackson2HttpMessageConverter
。
RestTemplate customRestTemplate = new RestTemplate();
MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
mappingJackson2HttpMessageConverter.setSupportedMediaTypes(Arrays.asList(MediaType.TEXT_HTML,MediaType.TEXT_PLAIN));
restTemplate.getMessageConverters().add(mappingJackson2HttpMessageConverter);
GetUserInfoByAccessToken getUserInfoByAccessTokenString = restTemplate.getForObject(userInfoByAccessCodeURL, GetUserInfoByAccessToken.class);
3. 源码分析问题
3.1 关键代码extractData
方法
extractData
方法将接口请求拿到的响应报文拿来给HttpMessageConverter
解析,这里会找到合适的解析器来解析响应报文,解析成我们指定的返回类型的数据,如果找不到或者处理出现异常就会抛出异常。
// 这里的入参是请求之后的响应体
public T extractData(ClientHttpResponse response) throws IOException {
//创建一个名为responseWrapper的MessageBodyClientHttpResponseWrapper,用于包装响应对象response,方便操作响应数据。
MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response);
// 检查响应是否有消息体,并且消息体不为空。如果不满足条件,则返回null。
if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) {
return null;
}
// 获取响应内容类型contentType。
MediaType contentType = getContentType(responseWrapper);
try {
// 遍历已注册的HttpMessageConverter列表。
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
// 对于实现了GenericHttpMessageConverter接口的转换器,检查是否可以读取responseType对应的类型,并且内容类型匹配。
if (messageConverter instanceof GenericHttpMessageConverter) {
GenericHttpMessageConverter<?> genericMessageConverter =
(GenericHttpMessageConverter<?>) messageConverter;
if (genericMessageConverter.canRead(this.responseType, null, contentType)) {
if (logger.isDebugEnabled()) {
ResolvableType resolvableType = ResolvableType.forType(this.responseType);
logger.debug("Reading to [" + resolvableType + "]");
}
return (T) genericMessageConverter.read(this.responseType, null, responseWrapper);
}
}
// 如果没有找到合适的GenericHttpMessageConverter,则检查是否指定了responseClass。
if (this.responseClass != null) {
// 如果指定了responseClass,则检查是否有转换器可以读取该类型,并且内容类型匹配。见相关代码`canRead`方法中的代码清单1-2
if (messageConverter.canRead(this.responseClass, contentType)) {
if (logger.isDebugEnabled()) {
String className = this.responseClass.getName();
logger.debug("Reading to [" + className + "] as \"" + contentType + "\"");
}
// 如果匹配成功,使用该转换器读取响应数据,并返回结果。
return (T) messageConverter.read((Class) this.responseClass, responseWrapper);
}
}
}
}
catch (IOException | HttpMessageNotReadableException ex) {
throw new RestClientException("Error while extracting response for type [" +
this.responseType + "] and content type [" + contentType + "]", ex);
}
throw new UnknownContentTypeException(this.responseType, contentType,
response.getRawStatusCode(), response.getStatusText(), response.getHeaders(),
getResponseBody(response));
}
3.2 相关代码messageConverter.canRead(this.responseClass, contentType)
方法
canRead(java.lang.Class, org.springframework.http.MediaType)
方法判断当前的HttpMessageConverter
是否可以读取响应报文ContentType
为服务端指定的数据,并且内容和你指定的返回值类型匹配。
// 判断`HttpMessageConverter`转换器是否可以读取该ContentType的数据,并且内容和你指定的返回值类型匹配
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
// supports判断HttpMessageConverter转换器是否支持你指定的返回类型,参考代码清单1-3。canRead
return supports(clazz) && canRead(mediaType);
}
这是StringHttpMessageConverter
的supports方法,可以看出他可以处理返回类型为String的数据。
public boolean supports(Class<?> clazz) {
return String.class == clazz;
}
上面代码supports方法返回true会调用canRead(org.springframework.http.MediaType)
方法,这段代码主要就是判断当前的HttpMessageConverter
是否可以处理content-type为服务端指定类型的响应报文,比如content-type为text/plain。
protected boolean canRead(@Nullable MediaType mediaType) {
if (mediaType == null) {
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.includes(mediaType)) {
return true;
}
}
return false;
}
4.关键点截图
以下是我在调试中截取的一些图片。
这里可以看到响应体的contentType
为text/plain,接下来就要找可以处理这种响应类型的HttpMessageConverter
。
这里可以看到已注册的HttpMessageConverter
列表里面有九个元素,并且通过他们的supportedMediaTypes
属性看到他们可以处理的contentType
。
首先判断HttpMessageConverter
是否可以读取我们指定的返回类,这里我指定的是我自定义的一个返回类GetUserInfoByAccessToken.class
在这里是在判断当前HttpMessageConverter
是否可以处理当前响content-type为text/plain的响应报文。