記一次Spring MVC 406 Not Acceptable的問題

問題比較小,重點不在問題的解決,在於碰到問題時對框架的理解和源碼的走讀,而不是一味地依賴搜索引擎
此思路不侷限於我所就任的碼農行業,一樣適用於其餘事業,有問題,請深刻

項目中有一小功能點,獲取用戶頭像。頭像以binary類型存放在mongodb數據庫中,服務端從數據庫中讀取頭像binary數據後,直接寫回response中。spring

一期功能使用NodeJS編寫,線上無問題。二期將該功能重構到Java(spring cloud),並加入請求合併(Hystrix),IOS端請求返回406 Not Acceptable,服務端報錯HttpMediaTypeNotAcceptableException: Could not find acceptable representationWEB及ANDROID端無問題mongodb

該錯誤已經很明顯,Spring MVC沒法處理該返回類型(MediaType)
抓包調試,IOS在請求頭像時,header中Accept爲image/*數據庫

使用Spring MVC的童鞋都知道,Spring中處理轉換http請求及響應的基類爲HttpMessageConverter,而處理ByteArray類型的實現類爲ByteArrayHttpMessageConverterjson

首先跟蹤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方法?查看AbstractMessageConverterMethodProcessorwriteWithMessageConverters方法(截取片斷)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!

重點不在問題的解決,在於碰到問題時對框架的理解和源碼的走讀,而不是一味地依賴搜索引擎!

相關文章
相關標籤/搜索