美文网首页
Spring MVC 406

Spring MVC 406

作者: carl_zhao | 来源:发表于2018-04-05 11:55 被阅读0次

    使用Spring MVC返回 JSON 数据有时候会在页面报出以下 406 错误。具体错误信息如下:

    406.jpg

    最常见的问题就是缺少 Jackson 工具包,它的作用是把 Java 对象转换成 JSON 输出到页面。当然这是最常见的情况,下面我就来介绍一下项目中出现的问题。由于项目遗留原因,项目请求中 URI 都是以 .htm 结尾。之前都是使用 HttpServletResponse 操作原生 Servlet 来返回 JSON 数据,而不是使用 Spring MVC 提供的 @ResponseBody 注解。

        public void out(Object obj, HttpServletResponse response) {
            response.setContentType("text/html; charset=utf-8");
    
            PrintWriter out = null;
            try {
                out = response.getWriter();
            } catch (IOException e) {
                e.printStackTrace();
            }
            out.print(JSON.toJSONString(obj));
        }
    

    重复的代码就是不好的

    所以对于新添加的接口我打算使用 Spring MVC 提供的 @ResponseBody来返回 JSON 数据。使用方式很简单,定义 @RequestMapping 方法返回值为任意的 POJO 对象,然后再这个方法上面添加 @ResponseBody 注解就好了。

        @RequestMapping("uri路径")
        @ResponseBody
        public User user(){
            User user = new User();
            user.setId("1");
            user.setName("carl");
            return user;
        }
    

    之前一直使用这个注解都可以解决这个问题,但是公司项目中居然不成功。我检查了一下 pom 文件是引用了 Jackson Jar包,排除这个原因。和之前使用 @ResponseBody 注解的的不同点就是请求 URI 里面包含了 .htm,然后我就做了以下的小实验。

    请求URI 返回
    test 成功返回JSON
    test.htm 406
    test.xxx 成功返回JSON

    从上面的例子中我们可以看到请求 URI 的后缀对于 Spring MVC 的响应生成是有影响的。

    我们知道在 Spring MVC 中 HandlerMethodArgumentResolver接口负责将 HttpServletRequest 里面的请求参数绑定到标注了 @RequestMapping 的@Controller 的方法中;而对于 @RequestMapping 方法的返回值 Spring MVC 通过HandlerMethodReturnValueHandler来处理。Spring MVC 通过 @RequestBody@ResposeBody 支持 restful,其实就是通过实现了以上两个接口的 RequestResponseBodyMethodProcessor 来实现的,而处理 restful 底层是通过 HttpMessageConverters 接口来实现的,对于这个接口这里我们就不过多介绍了。

    下面我们就从源码的角度来分析一下返回 JSON 报406 这个错误的原因。

    在Spring MVC 中处理 @ResponseBody 的入口是RequestResponseBodyMethodProcessor#handleReturnValue,而主要核心处理逻辑是在AbstractMessageConverterMethodProcessor#writeWithMessageConverters

        protected <T> void writeWithMessageConverters(T value, MethodParameter returnType,
                ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
                throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    
            Class<?> valueType = getReturnValueType(value, returnType);
            Type declaredType = getGenericType(returnType);
            HttpServletRequest request = inputMessage.getServletRequest();
            List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request);
            List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType);
    
            if (value != null && producibleMediaTypes.isEmpty()) {
                throw new IllegalArgumentException("No converter found for return value of type: " + valueType);
            }
    
            Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
            for (MediaType requestedType : requestedMediaTypes) {
                for (MediaType producibleType : producibleMediaTypes) {
                    if (requestedType.isCompatibleWith(producibleType)) {
                        compatibleMediaTypes.add(getMostSpecificMediaType(requestedType, producibleType));
                    }
                }
            }
            if (compatibleMediaTypes.isEmpty()) {
                if (value != null) {
                    throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes);
                }
                return;
            }
    
            List<MediaType> mediaTypes = new ArrayList<MediaType>(compatibleMediaTypes);
            MediaType.sortBySpecificityAndQuality(mediaTypes);
    
            MediaType selectedMediaType = null;
            for (MediaType mediaType : mediaTypes) {
                if (mediaType.isConcrete()) {
                    selectedMediaType = mediaType;
                    break;
                }
                else if (mediaType.equals(MediaType.ALL) || mediaType.equals(MEDIA_TYPE_APPLICATION)) {
                    selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
                    break;
                }
            }
    
            if (selectedMediaType != null) {
                selectedMediaType = selectedMediaType.removeQualityValue();
                for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
                    if (messageConverter instanceof GenericHttpMessageConverter) {
                        if (((GenericHttpMessageConverter<T>) messageConverter).canWrite(
                                declaredType, valueType, selectedMediaType)) {
                            value = (T) getAdvice().beforeBodyWrite(value, returnType, selectedMediaType,
                                    (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                                    inputMessage, outputMessage);
                            if (value != null) {
                                addContentDispositionHeader(inputMessage, outputMessage);
                                ((GenericHttpMessageConverter<T>) messageConverter).write(
                                        value, declaredType, selectedMediaType, outputMessage);
                                if (logger.isDebugEnabled()) {
                                    logger.debug("Written [" + value + "] as \"" + selectedMediaType +
                                            "\" using [" + messageConverter + "]");
                                }
                            }
                            return;
                        }
                    }
                    else if (messageConverter.canWrite(valueType, selectedMediaType)) {
                        value = (T) getAdvice().beforeBodyWrite(value, returnType, selectedMediaType,
                                (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
                                inputMessage, outputMessage);
                        if (value != null) {
                            addContentDispositionHeader(inputMessage, outputMessage);
                            ((HttpMessageConverter<T>) messageConverter).write(value, selectedMediaType, outputMessage);
                            if (logger.isDebugEnabled()) {
                                logger.debug("Written [" + value + "] as \"" + selectedMediaType +
                                        "\" using [" + messageConverter + "]");
                            }
                        }
                        return;
                    }
                }
            }
    
            if (value != null) {
                throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
            }
        }
    

    上面的代码看着很复杂其实逻辑很简单。简单来说就是处理 Media Type(互联网媒体类型,也叫做于MIME类型,有时在一些协议的消息头中叫做“Content-Type”。它使用两部分标识符来确定一个类型。
    )

    1. getAcceptableMediaTypes() 通过策略获取到请求可以接受的 MedieType
    2. getProducibleMediaTypes() 根据返回值获取到可产生哪些 MedieType
    3. isCompatibleWith() 匹配请求 MedieType 与 响应产生的 MedieType,如果匹配就添加到匹配的 MedieType 列表当中。
    4. HttpMessageConverter#write() 根据在 Medie 列表中找到的最合适的 MedieType 把它写入 HttpServletResponse 中

    我们可以看到有 3 个地方会影响最终响应的生成:也就是第1、2、4 这 4 个步骤。

    而在Spring MVC 找不到 Jackson 就属于第 4 步,因为处理 JSON 对应的HttpMessageConverterMappingJackson2HttpMessageConverter。而添加这个类的处理逻辑在WebMvcConfigurationSupport#addDefaultHttpMessageConverters

    http-message-converter.png

    它是根据jackson2Present这个参数来添加 JSON 处理器的。

        private static final boolean jackson2Present =
                ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", WebMvcConfigurationSupport.class.getClassLoader()) &&
                        ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", WebMvcConfigurationSupport.class.getClassLoader());
    

    上面这段代码的逻辑是在当前 ClassLoader 加载 ObjectMapper 或者 JsonGenerator,如果加载成功就添加 MappingJackson2HttpMessageConverter,而这两个类属于 Jackson

    下面我们来看第一点:

    其实获取请求可接受的 MedieType 是根据 ContentNegotiationManager#resolveMediaTypes Spring MVC 内容协商来解析的。

    默认有两种策略,也就是 ContentNegotiationManager#strategies

    • ServletPathExtensionContentNegotiationStrategy:根据请求 URI 扩展名来获取 MedieType,它最终会调用javax.servlet.ServletContext#getMimeTypeServletContext 里面获取支持的 URI 请求扩展,包含以下 170 种扩展:
     "css" -> "text/css"
     "ps" -> "application/postscript"
     "movie" -> "video/x-sgi-movie"
     "bin" -> "application/octet-stream"
     "xspf" -> "application/xspf+xml"
     "axa" -> "audio/annodex"
     "jad" -> "text/vnd.sun.j2me.app-descriptor"
     "xul" -> "application/vnd.mozilla.xul+xml"
     "midi" -> "audio/midi"
     "exe" -> "application/octet-stream"
     "java" -> "text/x-java-source"
     "texi" -> "application/x-texinfo"
     "mov" -> "video/quicktime"
     "dvi" -> "application/x-dvi"
     "xml" -> "application/xml"
     "jar" -> "application/java-archive"
     "axv" -> "video/annodex"
     "pict" -> "image/pict"
     "mpa" -> "audio/mpeg"
     "zip" -> "application/zip"
     "oth" -> "application/vnd.oasis.opendocument.text-web"
     "mpe" -> "video/mpeg"
     "otg" -> "application/vnd.oasis.opendocument.graphics-template"
     "qt" -> "video/quicktime"
     "cdf" -> "application/x-cdf"
     "mpg" -> "video/mpeg"
     "ras" -> "image/x-cmu-raster"
     "bcpio" -> "application/x-bcpio"
     "tex" -> "application/x-tex"
     "ai" -> "application/postscript"
     "png" -> "image/png"
     "eps" -> "application/postscript"
     "mathml" -> "application/mathml+xml"
     "otp" -> "application/vnd.oasis.opendocument.presentation-template"
     "odb" -> "application/vnd.oasis.opendocument.database"
     "oda" -> "application/oda"
     "texinfo" -> "application/x-texinfo"
     "ott" -> "application/vnd.oasis.opendocument.text-template"
     "pnm" -> "image/x-portable-anymap"
     "odc" -> "application/vnd.oasis.opendocument.chart"
     "ots" -> "application/vnd.oasis.opendocument.spreadsheet-template "
     "odf" -> "application/vnd.oasis.opendocument.formula"
     "odg" -> "application/vnd.oasis.opendocument.graphics"
     "au" -> "audio/basic"
     "odi" -> "application/vnd.oasis.opendocument.image"
     "pnt" -> "image/x-macpaint"
     "doc" -> "application/msword"
     "odm" -> "application/vnd.oasis.opendocument.text-master"
     "odp" -> "application/vnd.oasis.opendocument.presentation"
     "rm" -> "application/vnd.rn-realmedia"
     "jsf" -> "text/plain"
     "odt" -> "application/vnd.oasis.opendocument.text"
     "aif" -> "audio/x-aiff"
     "ods" -> "application/vnd.oasis.opendocument.spreadsheet"
     "aim" -> "application/x-aim"
     "xwd" -> "image/x-xwindowdump"
     "vsd" -> "application/vnd.visio"
     "flac" -> "audio/flac"
     "mpega" -> "audio/x-mpeg"
     "js" -> "application/javascript"
     "mid" -> "audio/midi"
     "mif" -> "application/x-mif"
     "mac" -> "image/x-macpaint"
     "cer" -> "application/pkix-cert"
     "sh" -> "application/x-sh"
     "pgm" -> "image/x-portable-graymap"
     "wml" -> "text/vnd.wap.wml"
     "jpeg" -> "image/jpeg"
     "man" -> "text/troff"
     "wmv" -> "video/x-ms-wmv"
     "art" -> "image/x-jg"
     "rtf" -> "application/rtf"
     "svg" -> "image/svg+xml"
     "snd" -> "audio/basic"
     "mpv2" -> "video/mpeg2"
     "ppm" -> "image/x-portable-pixmap"
     "txt" -> "text/plain"
     "pps" -> "application/vnd.ms-powerpoint"
     "abs" -> "audio/x-mpeg"
     "shar" -> "application/x-shar"
     "t" -> "text/troff"
     "xpm" -> "image/x-xpixmap"
     "asf" -> "video/x-ms-asf"
     "ppt" -> "application/vnd.ms-powerpoint"
     "rdf" -> "application/rdf+xml"
     "rtx" -> "text/richtext"
     "z" -> "application/x-compress"
     "dib" -> "image/bmp"
     "cpio" -> "application/x-cpio"
     "tr" -> "text/troff"
     "swf" -> "application/x-shockwave-flash"
     "bmp" -> "image/bmp"
     "xht" -> "application/xhtml+xml"
     "asx" -> "video/x-ms-asf"
     "oga" -> "audio/ogg"
     "roff" -> "text/troff"
     "wspolicy" -> "application/wspolicy+xml"
     "pic" -> "image/pict"
     "body" -> "text/html"
     "latex" -> "application/x-latex"
     "hqx" -> "application/mac-binhex40"
     "ogg" -> "audio/ogg"
     "tif" -> "image/tiff"
     "dv" -> "video/x-dv"
     "me" -> "text/troff"
     "wbmp" -> "image/vnd.wap.wbmp"
     "html" -> "text/html"
     "ogv" -> "video/ogg"
     "svgz" -> "image/svg+xml"
     "ogx" -> "application/ogg"
     "tar" -> "application/x-tar"
     "ms" -> "application/x-wais-source"
     "qti" -> "image/x-quicktime"
     "etx" -> "text/x-setext"
     "nc" -> "application/x-netcdf"
     "qtif" -> "image/x-quicktime"
     "mpeg" -> "video/mpeg"
     "spx" -> "audio/ogg"
     "pbm" -> "image/x-portable-bitmap"
     "psd" -> "image/vnd.adobe.photoshop"
     "ulw" -> "audio/basic"
     "xbm" -> "image/x-xbitmap"
     "tiff" -> "image/tiff"
     "aiff" -> "audio/x-aiff"
     "gif" -> "image/gif"
     "aifc" -> "audio/x-aiff"
     "ief" -> "image/ief"
     "rgb" -> "image/x-rgb"
     "jspf" -> "text/plain"
     "m3u" -> "audio/x-mpegurl"
     "xsl" -> "application/xml"
     "avi" -> "video/x-msvideo"
     "dtd" -> "application/xml-dtd"
     "htc" -> "text/x-component"
     "sv4crc" -> "application/x-sv4crc"
     "tsv" -> "text/tab-separated-values"
     "vxml" -> "application/voicexml+xml"
     "sv4cpio" -> "application/x-sv4cpio"
     "json" -> "application/json"
     "tcl" -> "application/x-tcl"
     "class" -> "application/java"
     "kar" -> "audio/midi"
     "jpe" -> "image/jpeg"
     "sit" -> "application/x-stuffit"
     "htm" -> "text/html"
     "jpg" -> "image/jpeg"
     "pct" -> "image/pict"
     "ustar" -> "application/x-ustar"
     "avx" -> "video/x-rad-screenplay"
     "src" -> "application/x-wais-source"
     "anx" -> "application/annodex"
     "wmls" -> "text/vnd.wap.wmlsc"
     "hdf" -> "application/x-hdf"
     "wav" -> "audio/x-wav"
     "gtar" -> "application/x-gtar"
     "mp2" -> "audio/mpeg"
     "mp1" -> "audio/mpeg"
     "xhtml" -> "application/xhtml+xml"
     "mp4" -> "video/mp4"
     "wrl" -> "model/vrml"
     "mp3" -> "audio/mpeg"
     "gz" -> "application/x-gzip"
     "pdf" -> "application/pdf"
     "pls" -> "audio/x-scpls"
     "wmlscriptc" -> "application/vnd.wap.wmlscriptc"
     "csh" -> "application/x-csh"
     "jnlp" -> "application/x-java-jnlp-file"
     "wmlc" -> "application/vnd.wap.wmlc"
     "xslt" -> "application/xslt+xml"
     "xls" -> "application/vnd.ms-excel"
    

    因为 htm后缀 对应 text/html,所以如果请求是 xxx.htm,不管第二步返回什么,服务端最多只能生成 html 页面。而使用test.xxx,并不在支持的扩展参数里面,所以没有影响。

    • HeaderContentNegotiationStrategy请求头策略,根据 http 的请求头来生成请求可接受的 MedieType。

    第二步是获取到服务端支持的可响应的 MedieType,它的规则如下:

    • 获取@RequestMapping 注解的 produces() 标注。
    • 遍历所有的 HttpMessageConverter 获取支持 @RequestMapping 返回值的 MedieType

    因为是 URI 扩展参数惹的祸,所以我首先想到的解决方案就是移除 ServletPathExtensionContentNegotiationStrategy 这个策略。

    因为是 Spring IOC 来创建对象,所以我想根据 Spring IOC 容器扩展 来解决这个问题。

    方法一 : 使用BeanPostProcessor修改 Bean

    因为是WebMvcConfigurationSupport#requestMappingHandlerAdapter来创建 RequestMappingHandlerAdapter并且WebMvcConfigurationSupport#mvcContentNegotiationManager创建的 ContentNegotiationManager。所以从容器中获取到 bean Id 为requestMappingHandlerAdapter的 Bean 对象RequestMappingHandlerAdapter,获取到 ContentNegotiationManager。获取直接根据 mvcContentNegotiationManager获取到 ContentNegotiationManager。 然后通过移除 ContentNegotiationManager.strategies策略列表中的 URI 扩展参数策略就可以了。

    因为 RequestMappingHandlerAdapter 对象里面没有 ContentNegotiationManager 的获取方法 且 ContentNegotiationManager 类中没有 策略列表的操作方法,所以这个方法不可行。

    方法二: 使用BeanFactoryPostProcessor修改 Bean

    可以通过 BeanFactoryPostProcessor#postProcessBeanFactory 来修改 BeanDefinition 的属性来移除 策略列表中的 URI 扩展参数策略。

    因为 @Configuration@Bean 生成的 BeanDefinition 是把这个 BeanDefinition 伪装成一个 Spring Factory Bean。创建实例直接调用这个方法,而不能通过 BeanDefinition 里面的参数来控制对象的创建。所以这个方法也不可行。

    方法三:@EnableMvcConfig

    WebMvcConfigurationSupport 类中调用 mvcContentNegotiationManager方法生成 ContentNegotiationManager 对象的时候,最终会调用 ContentNegotiationManagerFactoryBeanafterPropertiesSet()favorPathExtension 参数可以控制是否添加 PathExtensionContentNegotiationStrategy,如果这个值为 true 就会添加,反之而不会。这个值的默认值是 true,那么我们可以不可修改这个参数的值呢?

    答案是有的,因为在调用ContentNegotiationManagerFactoryBean#afterPropertiesSet方法之前,会调用 WebMvcConfigurationSupport#configureContentNegotiation而我们可以通过继承 WebMvcConfigurerAdapter 类使用 @EnableWebMvc 注解来修改这个值。

    下面就是我的测试代码工程结构:

    spring-boot-demo.png

    Bootstrap.java

    @SpringBootApplication
    public class Bootstrap {
    
        public static void main(String[] args) {
            SpringApplication.run(Bootstrap.class, args);
        }
    
    }
    

    MyMvcConfig.java

    @Configuration
    @EnableWebMvc
    public class MyMvcConfig extends WebMvcConfigurerAdapter {
    
        @Override
        public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
            configurer.favorPathExtension(false);
            super.configureContentNegotiation(configurer);
        }
    }
    

    TestController.java

    @Controller
    public class TestController {
    
        @RequestMapping("URI地址")
        @ResponseBody
        public User user(){
            User user = new User();
            user.setId("1");
            user.setName("carl");
            return user;
        }
    
    }
    

    然后再使用以上的请求 URI 做个实验:

    请求URI 返回
    test 成功返回JSON
    test.htm 成功返回JSON
    test.xxx 成功返回JSON

    并且无论访问哪个 URI 生成的 requestedMediaTypes 都为:

    request-media-types.png

    并且 http 的请求头如下:

    http-header.png

    相关文章

      网友评论

          本文标题:Spring MVC 406

          本文链接:https://www.haomeiwen.com/subject/nwbrhftx.html