問題比較小,重點不在問題的解決,在於碰到問題時對框架的理解和源碼的走讀,而不是一味地依賴搜索引擎
此思路不侷限於我所就任的碼農行業,一樣適用於其餘事業,有問題,請深刻
項目中有一小功能點,獲取用戶頭像。頭像以binary類型存放在mongodb數據庫中,服務端從數據庫中讀取頭像binary數據後,直接寫回response中。spring
一期功能使用NodeJS編寫,線上無問題。二期將該功能重構到Java(spring cloud),並加入請求合併(Hystrix),IOS端請求返回406 Not Acceptable
,服務端報錯HttpMediaTypeNotAcceptableException: Could not find acceptable representation
,WEB及ANDROID端無問題。mongodb
該錯誤已經很明顯,Spring MVC沒法處理該返回類型(MediaType)
抓包調試,IOS在請求頭像時,header中Accept爲image/*
數據庫
使用Spring MVC的童鞋都知道,Spring中處理轉換http請求及響應的基類爲HttpMessageConverter
,而處理ByteArray類型的實現類爲ByteArrayHttpMessageConverter
json
首先跟蹤spring源碼,WebMvcConfigurationSupport
類中getMessageConverters
方法app
protected final List<HttpMessageConverter<?>> getMessageConverters() { if (this.messageConverters == null) { this.messageConverters = new ArrayList<HttpMessageConverter<?>>(); 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<HttpMessageConverter<?>> messageConverters) { StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(); stringConverter.setWriteAcceptCharset(false); // 添加默認 ByteArrayHttpMessageConverter messageConverters.add(new ByteArrayHttpMessageConverter()); messageConverters.add(stringConverter); messageConverters.add(new ResourceHttpMessageConverter()); messageConverters.add(new SourceHttpMessageConverter<Source>()); messageConverters.add(new AllEncompassingFormHttpMessageConverter()); if (romePresent) { messageConverters.add(new AtomFeedHttpMessageConverter()); messageConverters.add(new RssChannelHttpMessageConverter()); } if (jackson2XmlPresent) { messageConverters.add(new MappingJackson2XmlHttpMessageConverter( Jackson2ObjectMapperBuilder.xml().applicationContext(this.applicationContext).build())); } else if (jaxb2Present) { messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); } if (jackson2Present) { messageConverters.add(new MappingJackson2HttpMessageConverter( Jackson2ObjectMapperBuilder.json().applicationContext(this.applicationContext).build())); } else if (gsonPresent) { messageConverters.add(new GsonHttpMessageConverter()); } }
Spring已默認添加ByteArrayHttpMessageConverter
,繼續查看其構造函數函數
public class ByteArrayHttpMessageConverter extends AbstractHttpMessageConverter<byte[]> { public ByteArrayHttpMessageConverter() { // 添加兩條supportedMediaTypes: application/octet-stream & */* super(new MediaType("application", "octet-stream"), MediaType.ALL); } }
該實現類僅添加了 application/octet-stream
及 */*
兩條支持的Accept類型測試
查看AbstractHttpMessageConverter
類中的canWrite
方法ui
// 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
方法(截取片斷)this
protected <T> 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!
重點不在問題的解決,在於碰到問題時對框架的理解和源碼的走讀,而不是一味地依賴搜索引擎!