Spring(20) GET请求参数偶发性丢失问题

文章目录

    • 一、问题现象
    • 二、问题分析
      • 2.1 发生时间
      • 2.2 发生位置
      • 2.3 源码解析
      • 2.4 Tomcat机制
      • 2.5 原因总结
    • 三、问题复现
    • 四、问题修复

一、问题现象

最近偶遇一诡异棘手问题:一个用于获取 tokenGET 接口,在生产环境不定期偶发出现 参数不存在 的问题。一度怀疑是前端的锅,虽然前端同学再三以人格担保!经过长时间观察,发现每每出现问题时,“再点一下就好了”!错误信息简单明确,是大家熟知的参数缺失异常:

Required request parameter ‘phone’ for method parameter type String is not present

在这里插入图片描述

这是怎么回事呢?这只是再普通不过的一个 GET 接口!

Spring(20) GET请求参数偶发性丢失问题_第1张图片


二、问题分析

2.1 发生时间

由于项目使用的是 Spring Cloud 微服务框架,当请求从浏览器发送过来后,经过了以下步骤:

Spring(20) GET请求参数偶发性丢失问题_第2张图片

顺着这个思路逐层排查:

  • HTTP请求: F12查看参数正常,排除。
  • Nginx: 日志打印参数正常,排除。
  • Gateway: 日志打印参数正常,排除。
  • Controller: 参数丢失。。。

所以可以得出结论:参数丢失问题发生在 Spring Cloud 微服务内部

2.2 发生位置

我们进一步分析,在过滤器增加请求参数的打印:

LogFilter.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
public class LogFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        log.info(">>>>>>>>>>【INFO】request.getQueryString(): {}", httpServletRequest.getQueryString());
        log.info(">>>>>>>>>>【INFO】request.getParameter(): {}", httpServletRequest.getParameter("phone"));

        filterChain.doFilter(httpServletRequest,httpServletResponse);
    }
}

再次复现问题后,在同一个 traceId 对应的日志中,打印结果如下:

在这里插入图片描述

可以发现在问题请求中 request.queryString() 正常,而 request.getParameter() 值却没有获取到!

众所周知,SpringBoot 默认内置 tomcat 容器,SpringMVC 则通过 request.getParameter() 方法获取并绑定 Controller 接口参数的。因此,初步判断:在 tomcat 获取 parameter 参数的时候出现了问题

那么,parameter 参数的获取过程是怎样的?

  1. SpringMVC 框架通过 DispatcherServlet 实现。
  2. Tomcat 接收到外部请求,将由 connector 通过 Processor 受理 http 请求。
  3. SpringMVC 通过 request.getParameter() 获取并绑定 Controller 接口参数。
  4. request.getParameter() 方法 在请求处理过程中仅在第一次调用 时通过解析 queryString 获取 parameters 参数值,并设置 didQueryParameter=true 标识已解析处理。
  5. Http 请求处理完成,processor 通过 release 方法释放连接重置参数属性,request.recycle 方法重置 request 参数属性(注意:这里 连接器及 request 对象并不会销毁,connector 再次受理新的请求时,将复用连接器、processor 及 request 对象而非创建)。

Spring(20) GET请求参数偶发性丢失问题_第3张图片

2.3 源码解析

下面,我们可以看一些源码的片段来验证一下:

源码1:SpringBoot 从 request 获取 parameter 参数。

RequestParamMethodArgumentResolve 类的 resovleName() 方法,可以看到这里调用了 request.getParameterValue() 方法。

Spring(20) GET请求参数偶发性丢失问题_第4张图片

源码2:tomcat 封装了解析参数。

org.apache.catalina.connector.Request 类的 getParameterValues() 方法,request 通过 Parameters 获取 parameter 参数。

Spring(20) GET请求参数偶发性丢失问题_第5张图片

Spring(20) GET请求参数偶发性丢失问题_第6张图片

源码3:Parameters 从 queryString 解析封装 parameter 参数。

org.apache.tomcat.util.http.Parameters 类的 handleQueryParameters() 方法,可以发现,参数在解析处理后会设置 didQueryParameters 参数为 true。

Spring(20) GET请求参数偶发性丢失问题_第7张图片

源码4:请求处理结束,重置参数属性,并不销毁对象。

org.apache.tomcat.util.http.Parameters 类的 recycle() 方法。

Spring(20) GET请求参数偶发性丢失问题_第8张图片

Spring(20) GET请求参数偶发性丢失问题_第9张图片

2.4 Tomcat机制

Tomcat 机制如下:

  • tomcat 可支持多个 service 示例;
  • 每个 service 实例维护了一个包含多个 connector 的连接池;
  • 当 service 接收到了一个 http 请求时,则从 connector 池中获取一个 connector 连接器进行响应处理。
  • connector 连接器是通过 Processor 对应 HTTP 请求进行响应处理。

Processor 封装了 requestresponse 对象,在请求处理开始时进行初始化封装(进封装参数属性,并不创建对象),请求处理完成后,则进行释放重置。(注意:这里的释放仅指重置参数属性,并不销毁对象!

Spring(20) GET请求参数偶发性丢失问题_第10张图片

2.5 原因总结

本次问题的根本原因在于 线程中引用了 request 对象,并在线程中调用了 request.getParameter() 方法使参数属性 didQueryParameter 错误而导致 http 请求无法正确获取参数值。

  • 假设第一次受理 http 请求的连接器为 connector1;
  • 请求 request 在子线程 thread1 中被引用;
  • connector1 完成 http 请求并执行 release 释放连接,这时 request.didQueryParameters 值为 false;
  • 如果子线程 thread1 处理任务的时间较长,调用了 getParameter() 方法,这时 request.didQueryParameters 值将再次被更新为 true;
  • 当 tomcat 再次通过 connector1 受理新的 http 请求时,由于 request.didQueryParameters=true,这时新请求调用 getParameter() 方法将不会再解析 queryString,因而无法正确获取 parameter 参数值。

Spring(20) GET请求参数偶发性丢失问题_第11张图片


三、问题复现

这里为了方便,我们使用 Hutool 的线程池工具。依赖如下:


<dependency>
    <groupId>cn.hutoolgroupId>
    <artifactId>hutool-allartifactId>
    <version>5.8.23version>
dependency>

复现代码如下:

DemoController.java

import cn.hutool.core.thread.ThreadUtil;
import com.demo.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@RestController
@RequestMapping("/demo")
public class DemoController {

    /**
     * 根据手机号获取token
     */
    @GetMapping("/getToken")
    public Result<Object> getToken(@RequestParam String phone) {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        ThreadUtil.execute(() -> {
            RequestContextHolder.setRequestAttributes(attributes);
            ThreadUtil.safeSleep(1000);
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            System.out.println("********** " + request.getParameter(phone));
        });
        return Result.succeed();
    }

}

使用 Jmeter 压测工具,设置 200 线程并发请求:

Spring(20) GET请求参数偶发性丢失问题_第12张图片

压测 http://localhost:8080/demo/test?phone=111111 接口,配置请求信息如下:

Spring(20) GET请求参数偶发性丢失问题_第13张图片

成功复现,结果如下所示:

Spring(20) GET请求参数偶发性丢失问题_第14张图片


四、问题修复

修复这个问题的话有两种方式:

  • 方式一: GET 请求改为 POST请求,使用 JSON 格式传输数据。

    (经过尝试,即使使用 POST 请求,不使用 JSON 格式传输数据的话,还是会丢失参数。)

  • 方式二: 将 tomcat 中间件替换为 undertow 中间件。修改后如下所示:

<dependency>
    <groupId>org.springframework.bootgroupId>
   <artifactId>spring-boot-starter-webartifactId>
    <exclusions>
        <exclusion>
            <groupId>org.yamlgroupId>
            <artifactId>snakeyamlartifactId>
        exclusion>
        <exclusion>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-tomcatartifactId>
        exclusion>
    exclusions>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-undertowartifactId>
dependency>

将 tomcat 替换为 undertow 之后,发现不再出现参数丢失的情况。

Spring(20) GET请求参数偶发性丢失问题_第15张图片

整理完毕,完结撒花~

你可能感兴趣的:(Spring,spring,java,后端)