异步使用 HttpServletRequest 对象的问题

后端上完线之后,api 服务一直在报错 org.springframework.web.bind.MissingServletRequestParameterException: Required String parameter 'projectId' is not present

虽然线上在报错,但是报错并不是很频繁(因为大部分接口都是通过 post json 的方式请求的...),所以一开始没有立即回滚 orz

根据报错找到 controller 的方法代码,通过 @RequestParam 绑定参数 projectId, 前端传参也是正常的,但是就是拿不到参数值,很神奇。

查看链路上的日志,nginx 日志打印的 uri 的 query 里有值,到 spring 的 filter 这一步打印了参数,值已经丢失了。

当时没有头绪,后面还是觉得可能和这次上线有关系,翻了下这次上线的 PR, 新增了一个 filter, 里面异步处理了 request.

public class XxxFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    	// ...
        executors.execute(() -> {
            aService.process(request);
        });
        // ...
    }

}
// 其中 process 逻辑差不多长这样
public void fill(HttpServletRequest request) {
    this.os = request.getParameter("os");
    this.version = request.getParameter("version");
    this.device = request.getParameter("device");
    // ...
}

以前知道 Spring 中如果通过 @Autowired 注入 request 对象的时候,实际获取的只是一个代理工厂对象,底层是通过 RequestContextHolder 的 ThreadLocal 获取到实际的 request 对象。所以这种方式注入的 request 对象无法在异步场景中使用。

@Setter(onMethod_ = {@Autowired})
private HttpServletRequest request;

@RequestMapping(path = "test")
public void demo(HttpServletRequest request2) {
    // class com.sun.proxy.$Proxy146
    System.out.println(request.getClass());
    // class javax.servlet.http.HttpServletRequest
    System.out.println(request2.getClass());
    // test ok
    System.out.println("@Autowired request: " + request.getRequestURI());
    // test ok
    System.out.println("param request: " + request2.getRequestURI());
    new Thread(() -> {
        try {
            System.out.println("Async @Autowired request: " + request.getRequestURI());
        } catch (Exception e) {
            System.out.println("Async @Autowired request error: ");
            // exception here
            e.printStackTrace();
        }
        try {
            // test ok
            System.out.println("Async param request: " + request2.getRequestURI());
        } catch (Exception e) {
            System.out.println("Async param request error: ");
            e.printStackTrace();
        }
    }).start();
}

// Async @Autowired request error:
java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
	at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
	at org.springframework.web.context.support.WebApplicationContextUtils.currentRequestAttributes(WebApplicationContextUtils.java:312)
	at org.springframework.web.context.support.WebApplicationContextUtils.access$400(WebApplicationContextUtils.java:65)
	at org.springframework.web.context.support.WebApplicationContextUtils$RequestObjectFactory.getObject(WebApplicationContextUtils.java:328)
	at org.springframework.web.context.support.WebApplicationContextUtils$RequestObjectFactory.getObject(WebApplicationContextUtils.java:323)
	at org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler.invoke(AutowireUtils.java:301)
	at com.sun.proxy.$Proxy146.getRequestURI(Unknown Source)

那现在这个 case 是直接将 filter 方法中的 request 参数传入给异步线程中使用,并不是一个代理对象,为什么会出现解析参数的问题,难道 request 解析参数的地方有什么特殊操作吗?

...

最后还是求助了搜索引擎,发现了这篇文章 千万不要把Request传递到异步线程里面!有坑!

总结:tomcat 会重用 request 对象,每次请求结束后,会重置对象。 request.getParameter() 的时候会判断该 request 是否已解析,如果没有解析才会去解析参数,并且设置标记位为 true,如果已解析,则直接获取解析好的参数 如果在异步线程里使用了 request 对象获取 parameters,会导致对象重置之后,该 request 对象解析参数的标记位又设置 true,下次新的请求复用这个 request, 不会再去解析 parameter.