HttpServletRequest 不完全指北
2025-01-19 09:46:49 # Technical # SpringMVC

HttpServletRequest 是什么

从名字就可以猜出八九不离十,HttpServletRequest 首先是 Http,就表明了其身份为 Http 协议,其次的 Servlet 就说明具有相关封装的信息,Request 就很明显是说请求相关的了。

HttpServletRequest 有什么

获取请求头

1
2
3
4
5
6
7
8
9
10
11
// 获取指定请求头
String getHeader(String var1);

// 获取指定请求头枚举,如:Accept-Language,可以具有多个值
Enumeration<String> getHeaders(String var1);

// 获取所有请求头枚举
Enumeration<String> getHeaderNames();

// 获取指定请求头整数值,如果没有返回-1,无法转换则抛出 NumberFormatException
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
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
// 获取对客户端请求和数据库取值时的编码,如果没有setCharacterEncoding(),那么返回null,表示使用ISO-8859-1编码
String getCharacterEncoding();

// 设置对客户端请求的编码,只对请求体有效!注意,对于GET而言,没有请求体!!!所以此方法只能对POST请求中的参数有效
void setCharacterEncoding(String var1) throws UnsupportedEncodingException;

// 获取请求体的字节数,GET请求没有请求体,没有请求体返回-1
int getContentLength();

// 获取请求类型,如果请求是GET,那么这个方法返回null;如果是POST请求,那么默认为application/x-www-form-urlencoded,表示请求体内容使用了URL编码
String getContentType();

// 返回请求协议,例如:http
String getScheme();

// 返回主机名,例如:localhost
String getServerName();

// 返回主机端口:例如:8080
int getServerPort();

// 返回请求端ip
String getRemoteAddr();

// 返回请求端主机名称
String getRemoteHost();

// 返回请求端浏览器的Locale,java.util.Locale表示国家和言语
Locale getLocale();

// 返回上下文路径,获取的是项目名称。
String getContextPath();

// 返回请求URL中的参数
String getQueryString();

// 返回请求URI路径
String getRequestURI();

// 返回请求URL路径
StringBuffer getRequestURL();

// 返回Servlet路径
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);
}

test02 转发到 test01

浏览器并无感知,直接返回了 test01 的结果

请求重定向

1
2
3
4
@GetMapping(value = "/test03")
public void test03(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.sendRedirect(request.getContextPath() + "/test01?method=redirect");
}

test03 请求被重定向-302

重定向到 test01

第一个请求的状态为 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 可以发现:

request的代理对象

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

ObjectFactoryDelegatingInvocationHandler

ObjectFactoryDelegatingInvocationHandler 实现了 InvocationHandler 接口,invoke 方法看出执行的是:

this.objectFactory.getObject()

所以查看 WebApplicationContextUtils$RequestObjectFactory

RequestObjectFactory

然后查看 WebApplicationContextUtils.currentRequestAttributes()

currentRequestAttributes

再查看 RequestContextHolder.currentRequestAttributes()

RequestContextHolder#currentRequestAttributes()

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

getRequestAttributes

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

RequestContextHolder#ThreadLocal

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

RequestContextHolder#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

发现最终是走到了 MimeHeaders.getHeader

Stack frame

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

MimeHeaders#clear

查看栈帧

Stack frame

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

org.apache.coyote.RequestRequest#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

RequestFacade#getParameter

接着步入

Request#getParameter

步入参数解析方法 Request.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() {
// 判断是否已解析,已解析为 true,用于避免重复解析
this.parametersParsed = true;
// 从 this.coyoteRequest 中获取一个 Parameters 对象,该对象用于存储请求参数
Parameters parameters = this.coyoteRequest.getParameters();
boolean success = false;

try {
// 设置请求参数的数量限制,通常受连接器(connector)的最大参数数量限制的影响
parameters.setLimit(this.getConnector().getMaxParameterCount());
// 获取请求中的字符编码
Charset charset = this.getCharset();
// 获取是否应该使用请求体的编码(body encoding)来解析 URI
boolean useBodyEncodingForURI = this.connector.getUseBodyEncodingForURI();
// 设置参数对象的字符编码
parameters.setCharset(charset);
if (useBodyEncodingForURI) {
parameters.setQueryStringCharset(charset);
}

// 处理查询字符串中的参数
parameters.handleQueryParameters();
// 检查是否使用了输入流或读取器(Reader),如果使用了,则设置 success 为 true
if (this.usingInputStream || this.usingReader) {
success = true;
} else if (!this.getConnector().isParseBodyMethod(this.getMethod())) {
// 如果没有使用输入流或读取器,并且当前的 HTTP 方法不需要解析请求体,也设置 success 为 true
success = true;
} else {
// 根据请求的 Content-Type 头部字段的值,判断如何处理请求体
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)) {
// 如果是 multipart/form-data,则调用 this.parseParts(false) 方法来解析多部分请求体
this.parseParts(false);
success = true;
} else if (!"application/x-www-form-urlencoded".equals(contentType)) {
success = true;
} else {
// 对于 application/x-www-form-urlencoded 类型的请求体
// 首先检查请求体的长度 len
int len = this.getContentLength();
// 如果长度小于等于 0
if (len <= 0) {
// 检查是否使用了分块传输编码(chunked transfer encoding),如果是,就尝试读取分块传输编码的请求体数据
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 {
// 如果请求体长度大于 0,就继续处理请求体的内容。如果请求体长度小于 8192 字节,会创建一个大小为 8192 字节的字节数组 formData,否则会根据请求体长度创建合适大小的 formData 数组
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 {
// 使用 this.readPostBody(formData, len) 方法从请求体中读取数据,并将数据存储到 formData 数组中
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) 方法,将解析出来的请求参数存储到 Parameters 对象中
parameters.processParameters(formData, 0, len);
}

// success 最终会被设置为 true,表示参数解析成功
success = true;
}
}
} finally {
// 在 finally 块中,如果 success 为 false,则设置解析失败的原因为 FailReason.UNKNOWN
if (!success) {
parameters.setParseFailedReason(FailReason.UNKNOWN);
}

}
}

解析完后,步入 CoyoteRequest.getParameter

CoyoteRequest#getParameter

步入 CoyoteRequest.handleQueryParameters

CoyoteRequest#handleQueryParameters

可以发现在 Request.getParameter 中会调用该方法,所以这里也会有一个类似 parametersParseddidQueryParameters,他们都是为了避免重复解析参数

这里的 didQueryParameters: true,所以直接返回到了前面的 coyoteRequest.getParameter

CoyoteRequest.getParameter

最后就在这里获取到了参数,然后返回

然后进行第二次请求,与上面相同的方式走一遍

这里很快就发现了不同

Request.getParameter 第一次执行时 this.parametersParsed 为 false,而第二次执行 this.parametersParsed 则为 true

Request#getParameter

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

CoyoteRequest#getParameter

返回为 null

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

Parameters.recycle

直接搜索关键字就可以找到 Parameters.recycle 这个方法,现在大致也能知晓参数丢失的原因:

  1. 第一次请求:主线程进入,解析参数,解析完成后将 this.parametersParsedthis.didQueryParameter 设置为 true
  2. 第一次请求:子线程进入,还在进行业务处理
  3. 第一次请求:主线程结束,request 生命周期结束,进行回收,并将 this.parametersParsedthis.didQueryParameter 设置为 false
  4. 第一次请求:子线程业务逻辑结束,开始获取参数,因为主线程的回收,所以这里会再次解析参数,但是由于主线程已回收 request 所以这里并不能解析到参数,参数已被回收,解析完后将 this.parametersParsedthis.didQueryParameter 设置为 true
  5. 第二次请求:主线程进入,此时 this.parametersParsedthis.didQueryParameter 设置为 true,所以并不会解析参数,直接返回 null
  6. 第二次请求:子线程进入,先处理业务逻辑
  7. 第二次请求:主线程结束,回收 request ,并将 this.parametersParsedthis.didQueryParameter 设置为 false
  8. 第二次请求:子线程结束业务逻辑,开始获取参数,解析参数(空参),并将 this.parametersParsedthis.didQueryParameter 设置为 true
  9. 第三次请求:….
  10. 第四次请求:….

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);
// 设置RequestContext 子线程共享
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);
// 设置 countDownLcatch
CountDownLatch latch = new CountDownLatch(1);
CompletableFuture.runAsync(() -> {
String header = httpServletRequest.getHeader("a");
String param = httpServletRequest.getParameter("s");
System.out.println("子线程获取参数:" + header + "," + param);
// countDown
latch.countDown();
try {
// 模拟业务场景
TimeUnit.SECONDS.sleep(3L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
// 等待 countDown
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 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);
// 请求结束,回收 request
asyncContext.complete();
});
}

这里将 request 的回收放到了子线程中,所以主线程不必等待子线程获取 request。这一特性得益于 Servlet 3.0 对请求异步处理的支持,如果有自定义的 Filter 或者 Servlet,使用前需将 @WebFilter 和 @WebServlet 中的 asyncSupported 属性设置为 true

HttpServletRequest 从何而来

接着前面的测试接口,分别在 setRequestAttributestest2 的第一行打上断点

test2

RequestContextHolder.setRequestAttributes

可以发现符合之前的预期,set 的对象与传入的对象是同一个,都是 RequestFacade@19115 这个对象

然后到 RequestFacade 的构造方法第一行打上断点

RequestFacade

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

Request#getRequest

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

Http11Processor

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

AbstractProcessor

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

AbstractProcessor construct

这是 AbstractProcessor 的构造器,也就是在这里 new Request() new Response()

接着往前找

Http11Processor construct

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

接着往前找

AbstractHttp11Protocol#createProcessor

符合预料,这里 new 出 Http11Processor

再往前看看

AbstractProtocol$ConnectionHandler#process

这里有两个判断,先判断 null,进行 pop,再判断 null,进行 create,在 pop 和 create 的地方加上断点

再次调用可以发现停在了 pop 这里,步入 this.recycledProcessors.pop()

AbstractProtocol$RecycledProcessors#pop

这是 AbstractProtocol 的一个继承了 SynchronizedStack 的内部类

里面有 push,pop,clear,给他们都打上断点,然后接着往下走,可以发现这次并没有进入 create,因为这次 processor != null

processor != null

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

AbstractProtocol$RecycledProcessors#push

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

AbstractProtocol$ConnectionHandler#process

查看这个 release 方法

AbstractProtocol$ConnectionHandler#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 前有个参数的判断

getDiscardFacades

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

Connector#getDiscardFacades

discardFacades

Globals#IS_SECURITY_ENABLED

他们决定着是否回收 facade

org.apache.catalina.connector.RECYCLE_FACADES 默认为 false,如果将其配置为 true

重新调用会发现,此时无论 processor 是否是 pop 还是 create,都会创建 facade

AbstractProtocol$ConnectionHandler#process

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

Request#getRequest

默认情况下,第一次执行到这里 facade 是为 null 的,因为 processor 是 create 的,然后第二次执行时 facade 就不为 null 了,因为前面一次 request 回收时 RECYCLE_FACADES 参数默认为 false,是不会回收 facade

但现在不同了,因为将 RECYCLE_FACADES 改为了 true,所以每次都会回收 facade,然后不管 processor 是不是 create 都会创建新的 ReqeustFacade

processor-cache

在前面提到 processor 的 push 时,有个参数值对 push 的成功和失败起到了关键的作用

AbstractProtocol$RecycledProcessors#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) {
// 检查是否设置了 Tomcat 的 JMX 域名
if (this.getProtocol().getDomain() != null) {
// 上锁,保证多线程下的安全
synchronized(this) {
try {
// 获取 processor 中的 Request
Request r = processor.getRequest();
if (r != null) {
// 获取 Request 中的 RequestInfo
RequestInfo rp = r.getRequestProcessor();
// 将 RequestInfo 的全局处理器设置为 null,取消全局处理器的管理
rp.setGlobalProcessor((RequestGroupInfo)null);
// 获取之前注册时设置的 JMX 对象名称
ObjectName rpName = rp.getRpName();
if (this.getLog().isDebugEnabled()) {
this.getLog().debug("Unregister [" + rpName + "]");
}

// 从 Tomcat 的 JMX 注册表中取消注册请求信息,使用之前设置的 JMX 对象名称 (ObjectName) 进行取消注册
Registry.getRegistry((Object)null, (Object)null).unregisterComponent(rpName);
// 最后,将请求信息的 JMX 对象名称设置为 null,以确保不再与 JMX 管理相关联
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) {
// 检查是否设置了 Tomcat 的 JMX 域名
if (this.getProtocol().getDomain() != null) {
// 上锁,保证多线程下的安全
synchronized(this) {
try {
// 生成一个唯一的计数值 count,每次调用 register 方法都会递增,以确保每个注册的处理器都有唯一的标识
long count = this.registerCount.incrementAndGet();
// 获取请求处理器 (Processor) 中的请求信息 (RequestInfo),以便将它注册到 JMX 中
RequestInfo rp = processor.getRequest().getRequestProcessor();
// 设置请求信息的全局处理器 (global),通常用于标识该请求处理器属于哪个全局处理器
rp.setGlobalProcessor(this.global);
// 构建了一个 JMX 对象名称 (ObjectName),用于唯一标识请求处理器。JMX 对象名称通常包括域名 (domain)、类型 (type)、工作器 (worker)、名称 (name) 等信息。在这里,它们是通过组合协议的域名、工作器名称、协议名称和计数值生成的
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 + "]");
}

// 将请求信息 (RequestInfo) 注册到 Tomcat 的 JMX 注册表中。它使用 JMX 注册表的 registerComponent 方法进行注册。rp 是要注册的组件,rpName 是注册时使用的 JMX 对象名称,第三个参数为描述信息,这里为空
Registry.getRegistry((Object)null, (Object)null).registerComponent(rp, rpName, (String)null);
// 最后,设置请求信息的 JMX 对象名称,以便后续可以根据 JMX 对象名称来管理和监控请求处理器
rp.setRpName(rpName);
} catch (Exception var8) {
this.getLog().warn(AbstractProtocol.sm.getString("abstractProtocol.processorRegisterError"), var8);
}
}
}

}

Thanks