Spring中GET请求参数偶发性丢失问题分析及修复

 更新时间:2025年04月23日 11:02:41   作者:不愿放下技术的小赵  
本文描述了一种在SpringCloud微服务架构下GET接口偶尔出现参数丢失的问题,通过源码分析和复现,发现问题是由于线程中请求对象的生命周期管理导致的,解决方法包括改用POST请求或更换Tomcat为Undertow中间件,需要的朋友可以参考下

一、问题现象

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

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

在这里插入图片描述

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

在这里插入图片描述

二、问题分析

2.1 发生时间

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

在这里插入图片描述

顺着这个思路逐层排查:

  • 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 对象而非创建)。

在这里插入图片描述

2.3 源码解析

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

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

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

在这里插入图片描述

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

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

在这里插入图片描述

在这里插入图片描述

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

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

在这里插入图片描述

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

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

在这里插入图片描述

在这里插入图片描述

2.4 Tomcat机制

Tomcat 机制如下:

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

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

在这里插入图片描述

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 参数值。

在这里插入图片描述

三、问题复现

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

<!-- Hutool -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.23</version>
</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 线程并发请求:

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

四、问题修复

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

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

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

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

<dependency>
    <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
        </exclusion>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

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

在这里插入图片描述

以上就是Spring中GET请求参数偶发性丢失问题分析及修复的详细内容,更多关于Spring GET请求参数丢失的资料请关注脚本之家其它相关文章!

相关文章

  • Spring入门配置和DL依赖注入实现图解

    Spring入门配置和DL依赖注入实现图解

    这篇文章主要介绍了Spring入门配置和DL依赖注入实现图解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-10-10
  • Gradle中Maven仓库配置的实现步骤

    Gradle中Maven仓库配置的实现步骤

    在Java/Kotlin 项目的构建过程中,依赖管理是核心环节之一,而Maven 仓库是最常见的依赖来源,本文就来详细的介绍一下Gradle中Maven仓库配置,感兴趣的可以了解一下
    2025-09-09
  • 利用线程实现动态显示系统时间

    利用线程实现动态显示系统时间

    编写Applet小程序,通过在HTML文档中接收参数,显示当前的系统时间,需要的朋友可以参考下
    2015-10-10
  • 关于ThreadLocal的用法和说明及注意事项

    关于ThreadLocal的用法和说明及注意事项

    这篇文章主要介绍了关于ThreadLocal的用法和说明及注意事项,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • Java使用synchronized修饰方法来同步线程的实例演示

    Java使用synchronized修饰方法来同步线程的实例演示

    synchronized下的方法控制多线程程序中的线程同步非常方便,这里就来看一下Java使用synchronized修饰方法来同步线程的实例演示,需要的朋友可以参考下
    2016-06-06
  • SpringBoot读取properties配置文件中的数据的三种方法

    SpringBoot读取properties配置文件中的数据的三种方法

    本文主要介绍了SpringBoot读取properties配置文件中的数据的三种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-06-06
  • SpringBoot项目使用yml文件链接数据库异常问题解决方案

    SpringBoot项目使用yml文件链接数据库异常问题解决方案

    在使用SpringBoot时,利用yml进行数据库连接配置需小心数据类型区分,如果用户名或密码是数字,必须用双引号包裹以识别为字符串,避免连接错误,特殊字符密码也应用引号包裹
    2024-10-10
  • Mybatis-Plus 多表联查分页的实现代码

    Mybatis-Plus 多表联查分页的实现代码

    本篇文章主要介绍了Mybatis-Plus 多表联查分页的实现代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2018-06-06
  • IDEA创建Java项目保姆级教程(超详细!)

    IDEA创建Java项目保姆级教程(超详细!)

    这篇文章主要给大家介绍了关于IDEA创建Java项目保姆级教程的相关资料,Java是一种广泛使用的编程语言,广泛用于Web应用程序和客户端应用程序的开发,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2023-09-09
  • Spring6.x对调度和异步执行的注解支持示例详解

    Spring6.x对调度和异步执行的注解支持示例详解

    这篇文章主要为大家介绍了Spring6.x对调度和异步执行的注解支持示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-11-11

最新评论