当前位置: 首页 > article >正文

接受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() 即可,诸如此类,非常好用。


http://www.kler.cn/a/466401.html

相关文章:

  • xml格式化(1):使用python的xml库实现自闭合标签
  • 【Android项目学习】3. MVVMHabit
  • Rabbitmq 具体怎么做到削峰的,是丢弃部分消费吗,有的实际场景是不允许丢弃
  • trendFinder - 利用 AI 掌握社交媒体上的热门话题
  • 『SQLite』详解运算符
  • Linux驱动开发(18):linux驱动并发与竞态
  • 黑马Java面试教程_P10_设计模式
  • [每周一更]-(第130期):微服务-Go语言服务注册中心的中间件对比
  • Vue 项目中实现打印功能:基于目标 ID 的便捷打印方案
  • LeetCode 142:环形链表入口
  • qt的utc时间转本地时间
  • Java基本数据类型与字节数组的相互转换
  • JAVA复习题
  • 使用docker desktop提示 需要更新WSL
  • 深入理解 Android 中的 ApplicationInfo
  • 深入Android架构(从线程到AIDL)_07 线程(Thread) 概念
  • 利用Claude3.5点评学习LightRAG源码
  • css中的渐变
  • 学技术学英文:Tomcat的线程模型调优
  • 基于 GitHub API 的 Issue 和 PR 自动化解决方案
  • ArcGIS API for JavaScript 缓冲区分析、河涌关联分析、集中连片分析
  • 高速网络数据包处理中的内核旁路技术
  • Ae 效果详解:漩涡条纹
  • .NET 8 + Ocelot + Consul 实现代理网关、服务发现
  • 365天深度学习训练营:第N1周:one-hot编码案例
  • 【Unity3D】LOD Group 多细节层次(CrossFade淡出淡入效果)