HttpServletRequest 是什么
从名字就可以猜出八九不离十,HttpServletRequest 首先是 Http,就表明了其身份为 Http 协议,其次的 Servlet 就说明具有相关封装的信息,Request 就很明显是说请求相关的了。
HttpServletRequest 有什么
获取请求头
1 2 3 4 5 6 7 8 9 10 11
| String getHeader(String var1);
Enumeration<String> getHeaders(String var1);
Enumeration<String> getHeaderNames();
int getIntHeader(String var1);
|
获取请求参数
1 2 3 4 5 6 7 8 9 10 11
| String getParameter(String var1);
Enumeration<String> getParameterNames();
String[] getParameterValues(String var1);
Map<String, String[]> getParameterMap();
|
请求域相关
1 2 3 4 5 6 7 8 9 10 11
| Object getAttribute(String var1);
Enumeration<String> getAttributeNames();
void setAttribute(String var1, Object var2);
void removeAttribute(String var1);
|
URL 相关
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| String getCharacterEncoding();
void setCharacterEncoding(String var1) throws UnsupportedEncodingException;
int getContentLength();
String getContentType();
String getScheme();
String getServerName();
int getServerPort();
String getRemoteAddr();
String getRemoteHost();
Locale getLocale();
String getContextPath();
String getQueryString();
String getRequestURI();
StringBuffer getRequestURL();
String getServletPath();
|
HttpServletRequest 能做什么
请求转发
通过 HttpServletRequest#getRequestDispatcher
获取到 RequestDispatcher
然后通过 RequestDispatcher#forward
实现请求转发
1 2 3 4 5 6 7 8 9 10
| @GetMapping(value = "/test01") public String test01() { return "this is test01"; }
@GetMapping(value = "/test02") public void test02(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { RequestDispatcher dispatcher = request.getRequestDispatcher("/test01?method=forward"); dispatcher.forward(request, response); }
|

浏览器并无感知,直接返回了 test01
的结果
请求重定向
1 2 3 4
| @GetMapping(value = "/test03") public void test03(HttpServletRequest request, HttpServletResponse response) throws IOException { response.sendRedirect(request.getContextPath() + "/test01?method=redirect"); }
|


第一个请求的状态为 302,并且重新请求了 test01
鉴权
HttpServletRequest 可以获取到请求头,因此也能获取到 token,拿到 token 后便能知晓请求者的身份,从而实现鉴权的目的。通常是配置 Filter 食用。
如何获取 HttpServletRequest
Controller 参数接收
1 2 3 4
| @GetMapping(value = "/test01") public String test01(HttpServletRequest request) { return "this is test01"; }
|
从 RequestContextHolder 获取
1 2
| ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = requestAttributes.getRequest();
|
依赖注入
1 2
| @Autowired HttpServletRequest request;
|
HttpServletRequest 为何会变
Controller 是单例的,但为什么每次请求的 HttpServletRequest 却不是相同的?
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Autowired HttpServletRequest request;
@GetMapping("/test") public void test() throws InterruptedException { TimeUnit.SECONDS.sleep(5L); System.out.println("test's head:" + request.getHeader("a")); }
@GetMapping("/test1") public void test1() throws InterruptedException { TimeUnit.SECONDS.sleep(3L); System.out.println("test1's head:" + request.getHeader("a")); }
|
同时请求这两个接口:
1 2
| curl --request GET --url 'http://localhost:8080/test' --header 'a: 123' curl --request GET --url 'http://localhost:8080/test1' --header 'a: 456'
|
可以看到同一个 HttpServletRequest,输出了不同的 header:
1 2
| test's head:123 test1's head:456
|
debug 可以发现:

传到内接口内的是一个代理对象 WebApplicationContextUtils$RequestObjectFactory
代理对象是由 AutowireUtils$ObjectFactoryDelegatingInvocationHandler
生成的

ObjectFactoryDelegatingInvocationHandler
实现了 InvocationHandler
接口,invoke
方法看出执行的是:
this.objectFactory.getObject()
所以查看 WebApplicationContextUtils$RequestObjectFactory

然后查看 WebApplicationContextUtils.currentRequestAttributes()

再查看 RequestContextHolder.currentRequestAttributes()

可以看出是 getRequestAttributes()
,所以接着查看

发现是从两个 ThreadLocal 属性中获取的

既然有 get 那必定会有 set,找到 setRequestAttributes

到这里基本可知,之所以 HttpServletRequest 作为属性仍然可变是因为代理+本地线程变量
HttPServletRequest 为何会消失
多模块间调用时,经常会将 HttpServletRequest 异步传入其他服务,这时就可能会出现 HttpServletRequest 里面的参数都消失的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @GetMapping("/test2") public void test2(HttpServletRequest httpServletRequest) { String aHeader = httpServletRequest.getHeader("a"); System.out.println("主线程获取参数:" + aHeader); CompletableFuture.runAsync(() -> { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { throw new RuntimeException(e); } String header = httpServletRequest.getHeader("a"); System.out.println("子线程获取参数:" + header); }); }
|
console:
1 2
| 主线程获取参数:test 子线程获取参数:null
|
在获取参数的这一行 String aHeader = httpServletRequest.getHeader("a");
打上断点,一直步入这个方法内

发现最终是走到了 MimeHeaders.getHeader

既然有 get 那么也会有 set,面的消失也会有 move 或者 clear,所以可以找到这个方法,接着打上断点

查看栈帧

往前找,可以找出是在 Request.recycle
中调用的

这里的 Rqeuest 是 org.apache.coyote.Request
并不是另一个 org.apache.catalina.connector.Request
org.apache.catalina.connector.Request
主要用于表示和处理 HTTP 请求的高级信息,它包含了 Servlet 容器中用于处理请求的各种信息,例如请求头、请求参数等。通常由 Servlet 容器创建和管理,供 Servlet 代码使用
org.apache.coyote.Request
主要用于底层的 HTTP 请求处理,它处理 HTTP 请求的原始字节数据,负责连接器和客户端之间的数据传输。通常由 Tomcat 连接器(如 HTTP 连接器)创建和管理,不会直接暴露给 Servlet 代码
到这里基本可知,HttpServletRequest 的参数之所以消失,是因为回收导致
HttpServletRequest 参数丢失
上面的例子改为获取参数后,重复请求会发现参数丢失
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @GetMapping("/test2") public void test2(HttpServletRequest httpServletRequest) { String aHeader = httpServletRequest.getHeader("a"); String aParam = httpServletRequest.getParameter("s"); System.out.println("主线程获取参数:" + aHeader + "," + aParam); CompletableFuture.runAsync(() -> { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { throw new RuntimeException(e); } String header = httpServletRequest.getHeader("a"); String param = httpServletRequest.getParameter("s"); System.out.println("子线程获取参数:" + header + "," + param); }); }
|
第一次请求:
1
| curl --request GET --url 'http://localhost:8080/test2?s=aaa' --header 'a: 123'
|
输出:
1 2
| 主线程获取参数:123,aaa 子线程获取参数:null,null
|
此时子线程的 null 很好理解,就是因为上面提到的 Request 的回收导致
当再一次请求:
1
| curl --request GET --url 'http://localhost:8080/test2?s=aaa' --header 'a: 123'
|
输出:
1 2
| 主线程获取参数:123,null 子线程获取参数:null,null
|
这是就出现了奇怪的现象,主线程的参数丢失了,s 变成了 null
对主线程的 httpServletRequest.getParameter("s")
打上断点
首先步入到的是 RequestFacade.getParameter

接着步入

步入参数解析方法 Request.getParameter

这里是解析参数的核心方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
| protected void parseParameters() { this.parametersParsed = true; Parameters parameters = this.coyoteRequest.getParameters(); boolean success = false;
try { parameters.setLimit(this.getConnector().getMaxParameterCount()); Charset charset = this.getCharset(); boolean useBodyEncodingForURI = this.connector.getUseBodyEncodingForURI(); parameters.setCharset(charset); if (useBodyEncodingForURI) { parameters.setQueryStringCharset(charset); }
parameters.handleQueryParameters(); if (this.usingInputStream || this.usingReader) { success = true; } else if (!this.getConnector().isParseBodyMethod(this.getMethod())) { success = true; } else { String contentType = this.getContentType(); if (contentType == null) { contentType = ""; }
int semicolon = contentType.indexOf(59); if (semicolon >= 0) { contentType = contentType.substring(0, semicolon).trim(); } else { contentType = contentType.trim(); }
if ("multipart/form-data".equals(contentType)) { this.parseParts(false); success = true; } else if (!"application/x-www-form-urlencoded".equals(contentType)) { success = true; } else { int len = this.getContentLength(); if (len <= 0) { if ("chunked".equalsIgnoreCase(this.coyoteRequest.getHeader("transfer-encoding"))) { byte[] formData = null;
Context context; byte[] formData; try { formData = this.readChunkedPostBody(); } catch (IllegalStateException var17) { parameters.setParseFailedReason(FailReason.POST_TOO_LARGE); context = this.getContext(); if (context != null && context.getLogger().isDebugEnabled()) { context.getLogger().debug(sm.getString("coyoteRequest.parseParameters"), var17); }
return; } catch (IOException var18) { parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT); context = this.getContext(); if (context != null && context.getLogger().isDebugEnabled()) { context.getLogger().debug(sm.getString("coyoteRequest.parseParameters"), var18); }
return; }
if (formData != null) { parameters.processParameters(formData, 0, formData.length); } } } else { int maxPostSize = this.connector.getMaxPostSize(); Context context; if (maxPostSize >= 0 && len > maxPostSize) { context = this.getContext(); if (context != null && context.getLogger().isDebugEnabled()) { context.getLogger().debug(sm.getString("coyoteRequest.postTooLarge")); }
this.checkSwallowInput(); parameters.setParseFailedReason(FailReason.POST_TOO_LARGE); return; }
context = null; byte[] formData; if (len < 8192) { if (this.postData == null) { this.postData = new byte[8192]; }
formData = this.postData; } else { formData = new byte[len]; }
try { if (this.readPostBody(formData, len) != len) { parameters.setParseFailedReason(FailReason.REQUEST_BODY_INCOMPLETE); return; } } catch (IOException var19) { Context context = this.getContext(); if (context != null && context.getLogger().isDebugEnabled()) { context.getLogger().debug(sm.getString("coyoteRequest.parseParameters"), var19); }
parameters.setParseFailedReason(FailReason.CLIENT_DISCONNECT); return; }
parameters.processParameters(formData, 0, len); }
success = true; } } } finally { if (!success) { parameters.setParseFailedReason(FailReason.UNKNOWN); }
} }
|
解析完后,步入 CoyoteRequest.getParameter

步入 CoyoteRequest.handleQueryParameters

可以发现在 Request.getParameter
中会调用该方法,所以这里也会有一个类似 parametersParsed
的 didQueryParameters
,他们都是为了避免重复解析参数
这里的 didQueryParameters: true
,所以直接返回到了前面的 coyoteRequest.getParameter

最后就在这里获取到了参数,然后返回
然后进行第二次请求,与上面相同的方式走一遍
这里很快就发现了不同
Request.getParameter
第一次执行时 this.parametersParsed
为 false,而第二次执行 this.parametersParsed
则为 true

然后到了 CoyoteRequest.getParameter
这里时就没了参数

返回为 null
所以重点就是找出 this.didQueryParameter
赋值的地方

直接搜索关键字就可以找到 Parameters.recycle
这个方法,现在大致也能知晓参数丢失的原因:
- 第一次请求:主线程进入,解析参数,解析完成后将
this.parametersParsed
和 this.didQueryParameter
设置为 true
- 第一次请求:子线程进入,还在进行业务处理
- 第一次请求:主线程结束,request 生命周期结束,进行回收,并将
this.parametersParsed
和 this.didQueryParameter
设置为 false
- 第一次请求:子线程业务逻辑结束,开始获取参数,因为主线程的回收,所以这里会再次解析参数,但是由于主线程已回收 request 所以这里并不能解析到参数,参数已被回收,解析完后将
this.parametersParsed
和 this.didQueryParameter
设置为 true
- 第二次请求:主线程进入,此时
this.parametersParsed
和 this.didQueryParameter
设置为 true,所以并不会解析参数,直接返回 null
- 第二次请求:子线程进入,先处理业务逻辑
- 第二次请求:主线程结束,回收 request ,并将
this.parametersParsed
和 this.didQueryParameter
设置为 false
- 第二次请求:子线程结束业务逻辑,开始获取参数,解析参数(空参),并将
this.parametersParsed
和 this.didQueryParameter
设置为 true
- 第三次请求:….
- 第四次请求:….
HttpServletRequest 的异步获取
根据前面所知,request 会在请求结束后进行回收,回收后的参数将会为 null
如果是单线程则不用担心 request 的回收,因为只要不 true,就不会回收 request
如果是多线程,并且需要异步获取 request 就需要注意了,在子线程获取到 request 前,主线程不能返回,否则可能出现参数为 null,或者使用 Servlet 3.0 的新特性,异步响应
主线程等待子线程
当使用的是 Controller 参数接收 或者 依赖注入 保证主线程在子线程获取 request 后结束即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @GetMapping("/test2") public void test2(HttpServletRequest httpServletRequest) { String aHeader = httpServletRequest.getHeader("a"); String aParam = httpServletRequest.getParameter("s"); System.out.println("主线程获取参数:" + aHeader + "," + aParam); CompletableFuture.runAsync(() -> { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { throw new RuntimeException(e); } String header = httpServletRequest.getHeader("a"); String param = httpServletRequest.getParameter("s"); System.out.println("子线程获取参数:" + header + "," + param); }).join(); }
|
增加 .join()
,等待子线程执行完毕
当使用的是 从 RequestContextHolder 获取,因为 SpringMVC 是将 RequestAttributes 放到 ThreadLocal 里的,所以子线程直接获取是获取不到的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @GetMapping("/test2") public void test2(HttpServletRequest httpServletRequest) { String aHeader = httpServletRequest.getHeader("a"); String aParam = httpServletRequest.getParameter("s"); System.out.println("主线程获取参数:" + aHeader + "," + aParam); ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); RequestContextHolder.setRequestAttributes(sra, true); CompletableFuture.runAsync(() -> { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { throw new RuntimeException(e); } HttpServletRequest subRequest = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); String header = subRequest.getHeader("a"); String param = subRequest.getParameter("s"); System.out.println("子线程获取参数:" + header + "," + param); }).join(); }
|
通过 RequestContextHolder.setRequestAttributes(sra, true);
将 RequestAttributes 放入 InheritableThreadLocal 中,使子线程也可以获取
实际使用中,常使用的方式是,先获取 request,然后执行业务逻辑,这时主线程等待就失去了异步的意义,所以也可用 CountDownLatch
来确保子线程的获取是在主线程结束之前的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @GetMapping("/test2") public void test2(HttpServletRequest httpServletRequest) throws InterruptedException { String aHeader = httpServletRequest.getHeader("a"); String aParam = httpServletRequest.getParameter("s"); System.out.println("主线程获取参数:" + aHeader + "," + aParam); CountDownLatch latch = new CountDownLatch(1); CompletableFuture.runAsync(() -> { String header = httpServletRequest.getHeader("a"); String param = httpServletRequest.getParameter("s"); System.out.println("子线程获取参数:" + header + "," + param); latch.countDown(); try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { throw new RuntimeException(e); } }); latch.await(); }
|
子线程异步回收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @GetMapping("/test2") public void test2(HttpServletRequest httpServletRequest) { String aHeader = httpServletRequest.getHeader("a"); String aParam = httpServletRequest.getParameter("s"); System.out.println("主线程获取参数:" + aHeader + "," + aParam); AsyncContext asyncContext = httpServletRequest.startAsync(); CompletableFuture.runAsync(() -> { try { TimeUnit.SECONDS.sleep(3L); } catch (InterruptedException e) { throw new RuntimeException(e); } String header = httpServletRequest.getHeader("a"); String param = httpServletRequest.getParameter("s"); System.out.println("子线程获取参数:" + header + "," + param); asyncContext.complete(); }); }
|
这里将 request 的回收放到了子线程中,所以主线程不必等待子线程获取 request。这一特性得益于 Servlet 3.0 对请求异步处理的支持,如果有自定义的 Filter 或者 Servlet,使用前需将 @WebFilter 和 @WebServlet 中的 asyncSupported 属性设置为 true
HttpServletRequest 从何而来
接着前面的测试接口,分别在 setRequestAttributes
和 test2
的第一行打上断点


可以发现符合之前的预期,set 的对象与传入的对象是同一个,都是 RequestFacade@19115
这个对象
然后到 RequestFacade
的构造方法第一行打上断点

查看栈帧发现,在 Request.getRequest
中调用的

然后按照栈帧接着往前找,直到 Http11Processor
这个类

这里是给后面传入 request
的起点,而这里的 request
是其父类 AbstractProcessor
的属性,通过构造方法注入的,所以在构造方法加上断点

加上断点后重新进入,查看调用的栈帧就可以找到这里

这是 AbstractProcessor 的构造器,也就是在这里 new Request()
new Response()
接着往前找

到了 Http11Processor 的构造方法里,说明前面还有地方进行 new Http11Processor
接着往前找

符合预料,这里 new 出 Http11Processor
再往前看看

这里有两个判断,先判断 null,进行 pop,再判断 null,进行 create,在 pop 和 create 的地方加上断点
再次调用可以发现停在了 pop 这里,步入 this.recycledProcessors.pop()

这是 AbstractProtocol 的一个继承了 SynchronizedStack 的内部类
里面有 push,pop,clear,给他们都打上断点,然后接着往下走,可以发现这次并没有进入 create,因为这次 processor != null

接着往下,就发现停在了刚刚打的 push 断点上

根据栈帧,往前可以发现,就是在前面 process pop 或者 create 不久之后调用的,还是同一个方法内

查看这个 release 方法

先将 processor 回收,然后判断是否为 isUpgrade,如果为 true,则说明请求正在切换到其他协议(例如 WebSocket)。在这种情况下,需要将 processor 从等待列表中移除,因为 upgrade 通常不需要继续通过传统的 HTTP 处理流程。现在在理当然就为 false,所以将 processor push 到前面的栈中。
在此可以看出,除了第一次请求,pop 为 null,然后进入 create,创建一个 processor,创建的过程如上面分析一样,new Request 然后也 new RequestFacade,最后 release 将回收了的 processor push 到栈中,后面请求就能直接 pop 出前面 push 的 processor。
在 request 的创建回收过程中,有两个重要的参数对整个过程起到了关键的作用
RECYCLE_FACADES
在 org.apache.catalina.connector.Request.recycle
方法中进行回收 facade 前有个参数的判断

这个参数的判断来自另两个参数



他们决定着是否回收 facade
org.apache.catalina.connector.RECYCLE_FACADES
默认为 false,如果将其配置为 true
重新调用会发现,此时无论 processor 是否是 pop 还是 create,都会创建 facade

这里调用 processor.process
,会执行到 Request.getReqeust

默认情况下,第一次执行到这里 facade 是为 null 的,因为 processor 是 create 的,然后第二次执行时 facade 就不为 null 了,因为前面一次 request 回收时 RECYCLE_FACADES
参数默认为 false,是不会回收 facade
但现在不同了,因为将 RECYCLE_FACADES
改为了 true,所以每次都会回收 facade,然后不管 processor 是不是 create 都会创建新的 ReqeustFacade
processor-cache
在前面提到 processor 的 push 时,有个参数值对 push 的成功和失败起到了关键的作用

cacheSize 默认为 200,与 Tomcat 默认的最大线程数相同
如果 cacheSize 达到 200,此时 offer 就会为 false,便会执行到下面的 this.handler.unregister(processor)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| protected void unregister(Processor processor) { if (this.getProtocol().getDomain() != null) { synchronized(this) { try { Request r = processor.getRequest(); if (r != null) { RequestInfo rp = r.getRequestProcessor(); rp.setGlobalProcessor((RequestGroupInfo)null); ObjectName rpName = rp.getRpName(); if (this.getLog().isDebugEnabled()) { this.getLog().debug("Unregister [" + rpName + "]"); }
Registry.getRegistry((Object)null, (Object)null).unregisterComponent(rpName); rp.setRpName((ObjectName)null); return; } } catch (Exception var7) { this.getLog().warn(AbstractProtocol.sm.getString("abstractProtocol.processorUnregisterError"), var7); return; }
} } }
|
与之向对应的,每次 processor create 后都会调用 register
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| protected void register(Processor processor) { if (this.getProtocol().getDomain() != null) { synchronized(this) { try { long count = this.registerCount.incrementAndGet(); RequestInfo rp = processor.getRequest().getRequestProcessor(); rp.setGlobalProcessor(this.global); ObjectName rpName = new ObjectName(this.getProtocol().getDomain() + ":type=RequestProcessor,worker=" + this.getProtocol().getName() + ",name=" + this.getProtocol().getProtocolName() + "Request" + count); if (this.getLog().isDebugEnabled()) { this.getLog().debug("Register [" + processor + "] as [" + rpName + "]"); }
Registry.getRegistry((Object)null, (Object)null).registerComponent(rp, rpName, (String)null); rp.setRpName(rpName); } catch (Exception var8) { this.getLog().warn(AbstractProtocol.sm.getString("abstractProtocol.processorRegisterError"), var8); } } }
}
|
Thanks