接受Header使用错Map类型,导致获取到的Header值不全
问题复现
- 在 Spring 中解析 Header 时,我们在多数场合中是直接按需解析的。例如,我们想使用一个名为 myHeaderName 的 Header,我们会书写代码如下:
@RequestMapping(path = "/hi", method = RequestMethod.GET) public String hi(@RequestHeader("myHeaderName") String name){ //省略 body 处理 };
- 定义一个参数,标记上 @RequestHeader,指定要解析的 Header 名即可。但是假设我们需要解析的 Header 很多时,按照上面的方式很明显会使得参数越来越多。在这种情况下,我们一般都会使用 Map 去把所有的 Header 都接收到,然后直接对 Map 进行处理。于是我们可能会写出下面的代码:
@RequestMapping(path = "/hi1", method = RequestMethod.GET) public String hi1(@RequestHeader() Map map){ return map.toString(); };
- 粗略测试程序,你会发现一切都很好。而且上面的代码也符合针对接口编程的范式,即使用了 Map 这个接口类型。但是上面的接口定义在遇到下面的请求时,就会超出预期。请求如下:
GET http://localhost:8080/hi1
myheader: h1
myheader: h2 - 这里存在一个 Header 名为 myHeader,不过这个 Header 有两个值。此时我们执行请求,会发现返回的结果并不能将这两个值如数返回。结果示例如下:
{myheader=h1, host=localhost:8080, connection=Keep-Alive, user-agent=Apache-HttpClient/4.5.12 (Java/11.0.6), accept-encoding=gzip,deflate}
案例解析
-
实际上,当我们看到这个测试结果,大多数同学已经能反应过来了。对于一个多值的 Header,在实践中,通常有两种方式来实现,一种是采用下面的方式:
Key: value1,value2,
-
而另外一种方式就是我们测试请求中的格式:
Key:value1
Key:value2 -
对于方式 1,我们使用 Map 接口自然不成问题。但是如果使用的是方式 2,我们就不能拿到所有的值。这里我们可以翻阅代码查下 Map 是如何接收到所有请求的。
-
对于一个 Header 的解析,主要有两种方式,分别实现在 RequestHeaderMethodArgumentResolver 和 RequestHeaderMapMethodArgumentResolver 中,它们都继承于 AbstractNamedValueMethodArgumentResolver,但是应用的场景不同,我们可以对比下它们的 supportsParameter(),来对比它们适合的场景:
-
在上图中,左边是 RequestHeaderMapMethodArgumentResolver 的方法。通过比较可以发现,对于一个标记了 @RequestHeader 的参数,如果它的类型是 Map,则使用 RequestHeaderMapMethodArgumentResolver,否则一般使用的是 RequestHeaderMethodArgumentResolver。
-
在我们的案例中,很明显,参数类型定义为 Map,所以使用的自然是 RequestHeaderMapMethodArgumentResolver。接下来,我们继续查看它是如何解析 Header 的,关键代码参考 resolveArgument():
@Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { Class<?> paramType = parameter.getParameterType(); if (MultiValueMap.class.isAssignableFrom(paramType)) { MultiValueMap<String, String> result; if (HttpHeaders.class.isAssignableFrom(paramType)) { result = new HttpHeaders(); } else { result = new LinkedMultiValueMap<>(); } for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) { String headerName = iterator.next(); String[] headerValues = webRequest.getHeaderValues(headerName); if (headerValues != null) { for (String headerValue : headerValues) { result.add(headerName, headerValue); } } } return result; } else { Map<String, String> result = new LinkedHashMap<>(); for (Iterator<String> iterator = webRequest.getHeaderNames(); iterator.hasNext();) { String headerName = iterator.next(); //只取了一个“值” String headerValue = webRequest.getHeader(headerName); if (headerValue != null) { result.put(headerName, headerValue); } } return result; } }
-
针对我们的案例,这里并不是 MultiValueMap,所以我们会走入 else 分支。这个分支首先会定义一个 LinkedHashMap,然后将请求一一放置进去,并返回。其中第 29 行是去解析获取 Header 值的实际调用,在不同的容器下实现不同。例如在 Tomcat 容器下,它的执行方法参考 MimeHeaders#getValue:
public MessageBytes getValue(String name) { for (int i = 0; i < count; i++) { if (headers[i].getName().equalsIgnoreCase(name)) { return headers[i].getValue(); } } return null; }
-
当一个请求出现多个同名 Header 时,我们只要匹配上任何一个即立马返回。所以在本案例中,只返回了一个 Header 的值。
-
其实换一个角度思考这个问题,毕竟前面已经定义的接收类型是 LinkedHashMap,它的 Value 的泛型类型是 String,也不适合去组织多个值的情况。综上,不管是结合代码还是常识,本案例的代码都不能获取到 myHeader 的所有值。
问题修正
-
现在我们要修正这个问题。在案例解析部分,其实已经给出了答案。
-
在 RequestHeaderMapMethodArgumentResolver 的 resolveArgument() 中,假设我们的参数类型是 MultiValueMap,我们一般会创建一个 LinkedMultiValueMap,然后使用下面的语句来获取 Header 的值并添加到 Map 中去:
String[] headerValues = webRequest.getHeaderValues(headerName)
-
参考上面的语句,不用细究,我们也能看出,我们是可以获取多个 Header 值的。另外假设我们定义的是 HttpHeaders(也是一种 MultiValueMap),我们会直接创建一个 HttpHeaders 来存储所有的 Header。
-
HttpHeaders 来存储所有的 Header。有了上面的解析,我们可以得出这样一个结论:要完整接收到所有的 Header,不能直接使用 Map 而应该使用 MultiValueMap。我们可以采用以下两种方式来修正这个问题:
//方式 1 @RequestHeader() MultiValueMap map //方式 2 @RequestHeader() HttpHeaders map
-
重新运行测试,你会发现结果符合预期:
[myheader:“h1”, “h2”, host:“localhost:8080”, connection:“Keep-Alive”, user-agent:“Apache-HttpClient/4.5.12 (Java/11.0.6)”, accept-encoding:“gzip,deflate”]
-
对比来说,方式 2 更值得推荐,因为它使用了大多数人常用的 Header 获取方法,例如获取 Content-Type 直接调用它的 getContentType() 即可,诸如此类,非常好用。