问题比较小,重点不在问题的解决,在于碰到问题时对框架的理解和源码的走读,而不是一味地依赖搜索引擎
此思路不局限于我所就职的码农行业,同样适用于其他事业,有问题,请深入
项目中有一小功能点,获取用户头像。头像以binary类型存放在mongodb数据库中,服务端从数据库中读取头像binary数据后,直接写回response中。
一期功能使用NodeJS编写,线上无问题。二期将该功能重构到Java(spring cloud),并加入请求合并(Hystrix),IOS端请求返回406 Not Acceptable
,服务端报错HttpMediaTypeNotAcceptableException: Could not find acceptable representation
,WEB及ANDROID端无问题。
该错误已经很明显,Spring MVC无法处理该返回类型(MediaType)
抓包调试,IOS在请求头像时,header中Accept为image/*
使用Spring MVC的童鞋都知道,Spring中处理转换http请求及响应的基类为HttpMessageConverter
,而处理ByteArray类型的实现类为ByteArrayHttpMessageConverter
首先跟踪spring源码,WebMvcConfigurationSupport
类中getMessageConverters
方法
protected final List> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList>();
configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
// 设置默认的 HttpMessageConverter
addDefaultHttpMessageConverters(this.messageConverters);
}
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
Spring默认设置了一些HttpMessageConverter,跟进addDefaultHttpMessageConverters
方法
protected final void addDefaultHttpMessageConverters(List> messageConverters) {
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
stringConverter.setWriteAcceptCharset(false);
// 添加默认 ByteArrayHttpMessageConverter
messageConverters.add(new ByteArrayHttpMessageConverter());
messageConverters.add(stringConverter);
messageConverters.add(new ResourceHttpMessageConverter());
messageConverters.add(new SourceHttpMessageConverter
Spring已默认添加ByteArrayHttpMessageConverter
,继续查看其构造函数
public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter {
public ByteArrayHttpMessageConverter() {
// 添加两条supportedMediaTypes: application/octet-stream & */*
super(new MediaType("application", "octet-stream"), MediaType.ALL);
}
}
该实现类仅添加了 application/octet-stream
及 */*
两条支持的Accept类型
查看AbstractHttpMessageConverter
类中的canWrite
方法
// mediaType 为request header中的Accept
protected boolean canWrite(MediaType mediaType) {
if (mediaType == null || MediaType.ALL.equals(mediaType)) {
// 不携带Accept或者Accept为 */* 则返回true
return true;
}
for (MediaType supportedMediaType : getSupportedMediaTypes()) {
if (supportedMediaType.isCompatibleWith(mediaType)) {
// 只有request header中的Accept匹配配置的supportedMediaType时才返回true
return true;
}
}
return false;
}
那么,何处调用canWrite
方法?查看AbstractMessageConverterMethodProcessor
的writeWithMessageConverters
方法(截取片段)
protected void writeWithMessageConverters(T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
// selectedMediaType为request请求header中的Accept
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter> messageConverter : this.messageConverters) {
if (messageConverter instanceof GenericHttpMessageConverter) {
// 给outputValue赋值
return;
}
else if (messageConverter.canWrite(valueType, selectedMediaType)/* 判断是否支持响应的类型及MediaType是否支持 */) {
// 给outputValue赋值
return;
}
}
}
if (outputValue != null) {
// 抛异常
throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
}
}
从以上逻辑可以看出,当配置的supportedMediaType与请求的Accept匹配不上时,会抛出HttpMediaTypeNotAcceptableException
异常,这便解释了为何IOS请求时header中携带Accept: image/*
会返回406 Not Acceptable
解决方法很简单,自行注册ByteArrayHttpMessageConverter
并设置其supportedMediaType
@Bean
fun byteArrayHttpMessageConverter(): ByteArrayHttpMessageConverter {
var byteArrayHttpMessageConverter = ByteArrayHttpMessageConverter()
byteArrayHttpMessageConverter.supportedMediaTypes = arrayListOf(
MediaType.ALL,
MediaType.APPLICATION_OCTET_STREAM,
MediaType.IMAGE_GIF,
MediaType.IMAGE_JPEG,
MediaType.IMAGE_PNG,
MediaType.valueOf("image/*") // 手动将 image/* 加入 supportedMediaTypes
)
return byteArrayHttpMessageConverter
}
重新打包、部署、测试,BINGO!
重点不在问题的解决,在于碰到问题时对框架的理解和源码的走读,而不是一味地依赖搜索引擎!