最近偶遇一诡异棘手问题:一个用于获取 token
的 GET
接口,在生产环境不定期偶发出现 参数不存在 的问题。一度怀疑是前端的锅,虽然前端同学再三以人格担保!经过长时间观察,发现每每出现问题时,“再点一下就好了”!错误信息简单明确,是大家熟知的参数缺失异常:
Required request parameter ‘phone’ for method parameter type String is not present
这是怎么回事呢?这只是再普通不过的一个 GET 接口!
由于项目使用的是 Spring Cloud 微服务框架,当请求从浏览器发送过来后,经过了以下步骤:
顺着这个思路逐层排查:
所以可以得出结论:参数丢失问题发生在 Spring Cloud 微服务内部。
我们进一步分析,在过滤器增加请求参数的打印:
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 参数的获取过程是怎样的?
DispatcherServlet
实现。didQueryParameter=true
标识已解析处理。下面,我们可以看一些源码的片段来验证一下:
源码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()
方法。
Tomcat 机制如下:
Processor 封装了 request
、response
对象,在请求处理开始时进行初始化封装(进封装参数属性,并不创建对象),请求处理完成后,则进行释放重置。(注意:这里的释放仅指重置参数属性,并不销毁对象!)
本次问题的根本原因在于 线程中引用了 request 对象,并在线程中调用了 request.getParameter()
方法使参数属性 didQueryParameter
错误而导致 http 请求无法正确获取参数值。
request.didQueryParameters
值为 false;request.didQueryParameters
值将再次被更新为 true;这里为了方便,我们使用 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 线程并发请求:
压测 http://localhost:8080/demo/test?phone=111111 接口,配置请求信息如下:
成功复现,结果如下所示:
修复这个问题的话有两种方式:
方式一: 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 之后,发现不再出现参数丢失的情况。
整理完毕,完结撒花~