近期有一个项目更换了中间件,在测试的时候功能不行了。

这时候经过逐步排查,发现了是某个请求响应http status code为406,而且只有某些用户才会出现这种情况。

查看前台response的响应头是Content-Type:text/plain;charset=UTF-8

正常的话应该是响应json的Content-Type: application/json;charset=UTF-8

查看后台日志发现

2020-12-16 15:50:17,377 DEBUG - Resolving exception from *** handler org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation
2020-12-16 15:50:17,378 DEBUG - Null ModelAndView returned to DispatcherServlet with name '**-app': assuming HandlerAdapter completed request handling
2020-12-16 15:50:17,378 DEBUG - Successfully completed request

这种用户共同点是用户名都是邮箱,以.com为结尾。

跟踪后台代码发现,这个接口是以用户名拼在url作为入参的,也就导致了这一次的请求路径是domain/add/test@abc.com

找了半天原来是因为Spring内容协商机制的原因。

由于后缀文件名.com的结尾,spring的内容协商机制会通过扩展名结尾解析,不识别.com的扩展名,最终导致spring无法完成内容协商,http status code406

故增加配置,关闭通过扩展名为结尾的解析:

<mvc:annotation-driven content-negotiation-manager="contentNegotiationManager"></mvc:annotation-driven>
<bean id= "contentNegotiationManager" class= "org.springframework.web.accept.ContentNegotiationManagerFactoryBean" >
    <property name ="favorPathExtension" value= "false" />
</bean>

参考文章:https://www.cnblogs.com/c04s31602/p/11257293.html

HTTP内容协商

要了解Spring MVC的内容协商机制,先要了解HTTP的内容协商机制,SpringMVC实现了HTTP内容协商的同时,又进行了扩展。

一个URL的资源服务端可以有多种响应形式,即MIME(Media Type)媒体类型。但客户端只需要一种,这就要求客户端和服务端之间有一种机制,能确保服务端响应的是客户端想要的,这就是内容协商。

内容协商通常有两种方式,第一是服务端将可用列表发给客户端,客户端选择之后服务端再发送过来,这种方式会多一次网络交互,而且普通用户不太可能了解技术性的选项,所以这种方式一般不用。第二种方式是常用的,客户端发送请求时指明需要的MIME,比如HTTP首部的Accept;服务端根据客户端的要求返回对应的内容形式,并在响应头中说明,比如Content-Type。详见下表:

请求头 请求头说明 响应头 响应头说明
Accept 告诉服务端需要的MIME Content-Type 告诉客户端响应的媒体类型
Accept-Language 告诉服务端需要的语言 Content-Language 告诉客户端响应的语言
Accept-Charset 告诉服务端需要的字符集 Content-Charset 告诉客户端响应的字符集
Accept-Encoding 告诉服务端需要的压缩方式 Content-Encoding 告诉客户端响应的压缩方式

先看个请求首部的例子:

首先解释一下q,权重的意思,最高为1,最低为0,默认是1。

Accept:*/*表示可以是任何MIME资源,其他的比如text/plain,text/html等。

Accept-Encoding:压缩方式可以是gzip,deflate,br。服务端向客户端发送的资源可通过压缩减少传输量。

Accept-Language:中文的权重最高。这里浏览器可以根据操作系统的语言或者浏览器本身的语言设置来选择,但能否协商成功还要看服务端是否支持多语言。

再看个响应首部的例子:

Content-Encoding:说明压缩的方式是gzip。

Content-Type:表示MIME是html文本,字符集是utf-8

Spring MVC的内容协商

Spring MVC支持4种内容协商方式:HTTP首部Accept,扩展名,请求参数,或者固定类型。我们通过一个例子来分别验证。

@RestController
@RequestMapping("/users")
public class UserController {

    @RequestMapping("{id}")
    public User get(@PathVariable  Integer id) {
        return new User(id, "呵呵");
    }

}
public class User {

    private Integer id;

    private String name;

    User(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

 

 

为了验证json和xml两种MIME,我们需要用到下面两个jar包,SpringMVC在转换json和xml时MessageConvertor默认的两个jar依赖

        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.6.4</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
            <version>2.9.9</version>
        </dependency>

 

Accept

在浏览器中输入http://localhost:8080/mvc/users/1,响应结果是:

<User><id>1</id><name>呵呵</name></User>

可以看到返回了xml格式的数据

再用Postman测试

返回了json格式的数据,为什么和浏览器不一样呢?其实就是权重q的设置问题。

浏览器的Accept设置:

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Postman的Accept设置:

由于我们用了@RestController注解,不会返回一个View,在浏览器请求的所有Accept中不能返回text/html,application/xhtml+xml格式,就取了权重是0.9的application/xml返回了。

而在Postman的请求中,Accept为任意格式,Spring MVC返回了json格式数据,由此可验证json的优先级比xml高。

那么怎么在Postman中返回xml格式的数据呢,修改Accept值就可以了:

我们已经验证了Spring MVC完全支持基于HTTP Accept首部的内容协商机制了。

扩展名

我们可以通过设置url的扩展名来指定需要的MIME,如果加了Spring MVC可以识别的扩展名将会忽略Accept的值。

在http://localhost:8080/mvc/users/1后面分别加上.json和.xml可得到对应格式的返回数据,就不贴图了。重点看下扩展名和Accept的优先级:

由此可验证扩展名优先级比Accept要高。

请求参数

请求参数内容协商机制默认是关闭的,我们手动打开:

@Configuration
@EnableWebMvc
@ComponentScan("com.acwei.spring.mvc")
public class MvcConfiguration extends WebMvcConfigurerAdapter {

    @Bean
    public LogInterceptor logInterceptor() {
        return new LogInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logInterceptor()).addPathPatterns("/**");
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.favorParameter(true);
    }
}

 

在configureContentNegotiation方法中打开请求参数内容协商设置,注意:请求参数机制优先级低于扩展名,所以我们验证时候先把url后缀去掉:

可以看到请求参数的优先级高于Accept。综合上面的实验可以得出几种机制的优先级:后缀 > 请求参数 > HTTP首部Accept

固定类型

最后一种就是@RequestMapping注解属性produces:

响应的MIME在这里指定,需要说明的是,这里指定的类型不能和后缀、请求参数、Accept冲突。比如这里指定了json格式,那么后缀如果不是json,或者format不是json,或者Accept不是application/json、*/*,将无法完成内容协商,http status code为406。


啦啦啦!