记一次Spring MVC 406 Not Acceptable的问题

问题比较小,重点不在问题的解决,在于碰到问题时对框架的理解和源码的走读,而不是一味地依赖搜索引擎
此思路不局限于我所就职的码农行业,同样适用于其他事业,有问题,请深入

项目中有一小功能点,获取用户头像。头像以binary类型存放在mongodb数据库中,服务端从数据库中读取头像binary数据后,直接写回response中。

一期功能使用NodeJS编写,线上无问题。二期将该功能重构到Java(spring cloud),并加入请求合并(Hystrix),IOS端请求返回406 Not Acceptable,服务端报错HttpMediaTypeNotAcceptableException: Could not find acceptable representationWEB及ANDROID端无问题

该错误已经很明显,Spring MVC无法处理该返回类型(MediaType)
抓包调试,IOS在请求头像时,header中Accept为image/*

使用Spring MVC的童鞋都知道,Spring中处理转换http请求及响应的基类为HttpMessageConverter,而处理ByteArray类型的实现类为ByteArrayHttpMessageConverter

首先跟踪spring源码,WebMvcConfigurationSupport类中getMessageConverters方法

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方法

// 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方法(截取片段)

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!

重点不在问题的解决,在于碰到问题时对框架的理解和源码的走读,而不是一味地依赖搜索引擎!


订阅号

阅读 5.7k

推荐阅读
林中小舍
用户专栏

工作中的坑点及经验

51 人关注
41 篇文章
专栏主页