tomcat源码分析之getParameter(String)与getQueryString()
来源:互联网 发布:php视频直播技术 编辑:程序博客网 时间:2024/06/08 14:49
本文有些地方的描述对某些人来说可能比较罗嗦,如果想直接进入正题,可阅读“源码分析”节。但本文是自己一步步分析解决问题思路的记录,虽然有些地方的思考还不是很深入,主要是由于时间不是很充裕(虽然花了三天时间,但感觉还是不够),我会在后续的博文中,结合自己遇到的实际问题或在论坛中看到的别人提出的问题,一步步的带着问题深入分析tomcat源码,这种带着问题进行源码分析的方式,比较有针对性,不至于让自己迷失在源码的汪洋之中。如果大家对博客格式或其他方面有比较好的建议,欢迎指出,非常感谢。
本次源码分析的目标是:
弄清楚org.apache.catalina.conector.RequestFacade::getQueryString()以及getParameter(String)的不同之处及其各自的具体实现,达到此目标即完成任务。
引言
问题的引出是由于前些天在oschina上看到的一篇帖子http://www.oschina.net/question/820641_104356,
问题分析
起初的分析思路也是受帖子作者的影响,心想出现这种情况是否是因为hashmap destroy encoding导致的,所以就google了一下hashmap encoding,得到一个比较相关的答案
http://stackoverflow.com/questions/8427488/hashmap-destroys-encoding,这篇帖子中出现的情况也比较奇怪。
程序功能描述如下:
从文件A中读取一组以空格为分隔符的的字符串,然后将这些字符串一行一行的写入到另外一个文件B中。
如文件A的格式为:
Aaa bbbbb cdefggg …..
文件B的格式为:
Aaa
Bbbbb
Cdefgggg
….
程序代码:
01
final
StringBuffer fileData =
new
StringBuffer(
1000
);
02
03
final
BufferedReader reader =
new
BufferedReader(
04
05
new
FileReader(
"fileIn.txt"
));
06
07
char
[] buf =
new
char
[
1024
];
08
09
int
numRead =
0
;
10
11
while
((numRead = reader.read(buf)) != -
1
)
12
13
{
14
15
final
String readData = String.valueOf(buf,
0
, numRead);
16
17
fileData.append(readData);
18
19
buf =
new
char
[
1024
];
20
21
}
22
23
reader.close();
24
25
String mergedContent = fileData.toString();
26
27
mergedContent = mergedContent.replaceAll(
"\\<.*?>"
,
" "
);
28
29
mergedContent = mergedContent.replaceAll(
"\\r\\n|\\r|\\n"
,
" "
);
30
31
final
BufferedWriter out =
new
BufferedWriter(
32
33
new
OutputStreamWriter(
34
35
new
FileOutputStream(
"fileOut.txt"
)));
36
37
final
HashMap<String, String> wordsMap =
new
HashMap<String, String>();
38
39
final
String test[] = mergedContent.split(
" "
);
40
41
for
(
final
String string : test)
42
43
{
44
wordsMap.put(string, string);
45
46
}
47
for
(
final
String string : wordsMap.values())
48
49
{
50
out.write(string +
"\n"
);
51
52
}
53
out.close();
这种情况下,发现文件B中的内容为乱码,而如果将上述程序中的部分代码改为下面这样,则会得到期望的结果。
01
...
02
03
for
(
final
String string : test)
04
{
05
out.write(string +
"\n"
);
06
//wordsMap.put(string, string);
07
08
}
09
10
//for (final String string : wordsMap.values())
11
12
//{
13
14
// out.write(string + "\n");
15
16
//}
17
out.close();
出现这种情况的原因,我也不是很理解,原文中关于该贴的回答,我觉得和问题没有任何关系,大多数人都在讲如何解决这个问题,而没有提到出现上述情况的原因。
经过该贴和其他一些相关帖的了解,我发现引言中提出的问题貌似和hashmap的encoding没有任何关系,可能存在别的原因,于是自己写了一个简单的servlet来实践一下。
实践
首先是问题重现,我写了一个简单的servlet如下所示:
//请求的url为:http://localhost:8080/demo/1.do?addr=上海
01
@Override
02
protected
void
doGet(HttpServletRequest req, HttpServletResponse resp)
03
throws
ServletException, IOException {
04
//System.out.println(req);
05
System.out.println(
"Request::getParameter(addr) is: "
+ req.getParameter(
"addr"
));
06
String queryString = req.getQueryString();
07
System.out.println(
"queryString is: "
+queryString);
08
String[] params = queryString.split(
"[=]"
);
09
10
Map<String, String> map =
new
HashMap<String, String>();
11
12
map.put(params[
0
], params[
1
]);
13
System.out.println(
"Map::get(addr) is: "
+map.get(params[
0
]));
14
return
;
15
}
在运行的时候,得到的结果是:
getParameter()得到的值是乱码,而通过getQueryString()解析后存放在map中的值是经过utf-8编码的。
对于getParameter()是乱码,这个原因比较明显,由于浏览器默认的urlencoding一般是utf-8,而tomcat中默认的URIEncoding是ISO-8859-1不是utf-8(为什么默认的编码是iso-8859-1?耐心看完本文后,就会明白),当客户端的请求到达tomcat的时候,tomcat就会用其他的编码方式去decode utf-8编码,那么自然就会出现乱码(具体的tomcat是如何处理queryString的,请继续阅读后面的源码分析节),所以解决方法是在tomcat的配置文件server.xml中加入如下配置(URIEncoding="utf-8"):
1
<Connector port=
"8080"
protocol=
"HTTP/1.1"
2
connectionTimeout=
"20000"
3
redirectPort=
"8443"
4
URIEncoding=
"utf-8"
5
/>
通过上述配置文件的修改,我们得到的测试结果如下:
经上述分析,我们可以得出,getParameter()的值是根据tomcat中设置的URIEncoding编码进行decode后得到的值,而对于getQueryString() tomcat没有对其进行decode操作,保留了原有的urlencoding编码方式。
至此,我们基本可以推测,出现引言中的情况的原因是:
由于客户端对http get请求的url编码方式与tomcat中定义的URIEncoding不一致,导致tomcat服务器利用另外一种解码方式来解码客户端的url,这样必然会出现中文乱码现象。而放入Map中的字符串为什么没有出现乱码?原因就在于getQueryString()没有对客户端的url进行decode,因而保留了原有的客户端utf-8编码,所以在后面的使用过程中,如果利用utf-8对其解码,则不会出现中文乱码现象。
源码分析
经过上述实践,基本可以确定问题的原因,但为了进一步的加以验证,我试着分析了一下tomcat在处理getParameter()和getQueryString()的不同。
由于HttpServletRequest为一接口,故我们看不到其getParameter()和getQueryString()具体实现,所以我们首先需要确定request的具体实现类是什么,我们在刚才的servlet中加入如下代码:
System.out.pritnln(req);
通过上述打印结果,我们可以看到其具体实现类为org.apache.catalina.connector.RequestFacade,所以下一步我们的工作就是具体的分析这个类是如何处理的,也就是分析两个函数的处理流程,一是RequestFacade::getQueryString(),另外一个是RequestFacade::getParameter(String)。
首先要获得tomcat的源码,通常的做法是在eclipse中通过egit插件,将远程的git库clone下来,然后再导入工程。
所有的准备工作就绪后,接下来就是具体的源码分析工作了:
从org.apache.catalina.connector.RequestFacade这个类,我们可以看到,这是一个使用了façade模式的包装类,所以我们需要先了解一下façade模式的相关知识。
Façade模式介绍
facade模式的核心是为子系统的一组接口提供一个统一的界面,方便客户端使用子系统,客户端也不必关心子系统的具体实现。
facade设计模式的适用情况:
1. 原来的类提供的接口比较多,也比较复杂,而我们只需要使用其部分接口;
2. 原类提供的接口只能部分的满足我们的需要,且不希望重写一个新类来代替原类;
...
在本文中,RequestFacade是对Request的一个封装,由于Request本身提供的接口非常之多,而本系统中只需要使用其部分功能,在实际分析过程中,我们发现Request的具体工作最后delegate到底层的coyote.Request去做。
RequestFacade::getQueryString()分析
如何进行源码的阅读和分析?我一般的思路是,先分析正常的处理逻辑,对于那些日志,错误处理,变量定义等等可以先不用关注,从而达到快速了解整体架构或关键流程。
基于上述思路,我们得到其处理流程如下:
1
-RequestFacade::getQueryString()
2
3
-Request::getQueryString()
4
5
-org.apache.coyote.Request::queryString()::toString()
通过以上分析可以看出,其处理流程比较简单,通过一步步的delegate,最后真正做工作的是coyote.Request,所以我们接下来只需要分析该类是如何处理。
相关函数源码如下:
org.apache.catalina.connector.RequestFacade::getQueryString()
1
@Override
2
public
String getQueryString() {
3
if
(request ==
null
) {
4
throw
new
IllegalStateException(
5
sm.getString(
"requestFacade.nullRequest"
));
6
}
7
return
request.getQueryString();
8
}
org.apache.catalina.connector.Request::getQueryString()
01
/**
02
03
* Return the query string associated with this request.
04
05
*/
06
07
@Override
08
public
String getQueryString() {
09
return
coyoteRequest.queryString().toString();
10
}
org.apache.coyote.Request::queryString()
1
public
MessageBytes queryString() {
2
return
queryMB;
3
}
coyote.Request::queryString()做的工作非常简单,仅是返回类型为MessageBytes的queryMB字段,但这个字段是何时被赋值的呢?这是一个非常有必要弄清的问题,因为极有可能会在赋值之前进行decode操作。
queryMB赋值分析
接下来探讨下queryMB是在何时被赋值的?
queryMB是org.apache.coyote.Request的一个私有成员变量,其数据类型为MessageBytes,定义如下:
1
private
MessageBytes queryMB = MessageBytes.newInstance();
我们如何定位queryMB这个变量是在什么时候赋值的呢?在eclipse中,选中queryMB,点击鼠标右键,选择open call hierarchy,可以看到queryMB在哪些地方被调用,截图如下所示:
从上图可以看出,有三个地方调用了queryMB,分别是:
1
public
MessageBytes queryString() {
2
return
queryMB;
3
}
该函数是获得一个queryMB对象,既然获得了该对象,那么很有可能在获得对象后对其进行某些操作如赋值操作。
1
public
void
recycle() {
2
….
3
queryMB.recycle();
4
….
5
}
顾名思义,queryMB.recycle()是对queryMB的重新回收利用,对该对象进行reset操作,和赋值没有任何联系。
1
public
Request() {
2
parameters.setQuery(queryMB);
3
parameters.setURLDecoder(urlDecoder);
4
}
Request()构造函数中,对其成员变量parameters进行了赋值,和queryMB的赋值没有关系。
根据上述三种情况的分析,我们得出只有在第一种情况最有可能出现赋值操作,所以接下来将继续分析queryString()被哪些函数所调用,如下图所示:
从截图看出共有七个函数调用了queryString(),从函数名,我们可以简单的判断出,只有parseRequestLine(boolean)这个函数最有可能对其进行赋值,这个函数是解析http请求request line信息。
01
/**
02
* Read the request line. This function is meant to be used during the
03
* HTTP request header parsing. Do NOT attempt to read the request body
04
* using it.
05
*
06
* @throws IOException If an exception occurs during the underlying socket
07
* read operations, or if the given buffer is not big enough to accommodate
08
* the whole line.
09
* @return true if data is properly fed; false if no data is available
10
* immediately and thread should be freed
11
*/
12
@Override
13
public
boolean
parseRequestLine(
boolean
useAvailableData)
14
throws
IOException {
15
….
16
if
(questionPos >=
0
) {
17
request.queryString().setBytes(buf, questionPos +
1
,
18
end - questionPos -
1
);
19
request.requestURI().setBytes(buf, start, questionPos - start);
20
}
else
{
21
request.requestURI().setBytes(buf, start, end - start);
22
}
23
….
24
}
从上述代码,我们可以看到,在解析http request line的时候,的确对queryMB进行了操作,直接从inputbuffer中获得字节信息,并对queryMB进行赋值。
1
request.queryString().setBytes(buf, questionPos +
1
,
2
end - questionPos -
1
);
getQueryString()总结
由上层的Request一步步的delegate到底层,最后返回coyote.Request::queryMB()字段,而该字段是由底层直接解析http request line信息,并将得到的字节数组直接赋值给coyote.Request::queryMB。
(首先在connector.RequestFacade中调用getQueryString(),然后转交给connector.Request::getQueryString()处理,最后交由最底层的类coyote.Request直接调用getQueryString()返回该对象中保存的类型为MessageBytes的queryMB字段值,而queryMB是在解析http request line的时候,直接得到原始的bytes信息,然后保存在queryMB中,至此,上层调用的getQueryString()返回的是,未经上层任何处理,直接解析Http request line的字节信息。)
RequestFacade::getParameter()分析
(我们知道在web开发中,处理的比较多的是http get请求和http post请求,对于get请求我们可直接由url通过getParameter()方法获得,但对于post请求就会有requsetBody,那么tomcat又是如何处理的?请看后续博文分析)
继续上述getQueryString()的思路,我们先得到getParameter()的正常处理流程,如下:
01
-RequestFacade::getParameter(String)
02
03
-Request::getParameter(String)
04
05
-Request::parseParameters()
06
07
-coyote.Request::getParameters()
08
09
-Parameters::setLimit(
int
)
10
11
-Parameters::setEncoding(String)
12
13
-Parameters::handleQueryParameters()
14
15
-decodedQuery.duplicate(MessageBytes)
16
17
-Parameters::processParameters(MessageBytes, String)
18
19
-Parameters::processParameters(
byte
[],
int
,
int
,String)
20
-coyote.Request::getParameters()::getParameter(String)
21
22
-Parameters::paramHashValues.get(String)
附录中,有上述每个函数的具体实现源码,有需要的同学可在此处查看http://my.oschina.net/gschen/blog/120740。
从上述流程,我们可以看到,最终的处理函数是Parameter::processParameters(byte[],int,int),接下来将重点分析该方法。
Parameter::processParameters(byte[],int,int,String)该函数有四个参数,第一个参数类型是byte[],是handleQueryParameter()函数中,获得一份queryMB的拷贝,然后传给processParameters(MessageBytes,String),再传给processParameters(byte[],int,int,String)
01
// -------------------- Processing --------------------
02
/** Process the query string into parameters
03
*/
04
public
void
handleQueryParameters() {
05
...
06
try
{
07
decodedQuery.duplicate( queryMB );
08
}
catch
(IOException e) {
09
// Can't happen, as decodedQuery can't overflow
10
e.printStackTrace();
11
}
12
processParameters( decodedQuery, queryStringEncoding );
13
}
1
public
void
processParameters( MessageBytes data, String encoding ) {
2
...
3
ByteChunk bc=data.getByteChunk();
4
processParameters( bc.getBytes(), bc.getOffset(),
5
bc.getLength(), getCharset(encoding));
6
}
大致的处理流程是,一步步的解析queryMB,然后将解析到的每一个parameter添加到一个HashMap<String, ArrayList<String>>中,最后在这个hashmap中根据name find到自己需要的value。
Parameters::handleQueryParameters()函数中先是得到queryMB的一份拷贝,这样可以避免对queryMB直接操作,破坏原始的信息,接着交由Parameters::processParameters(DecodedQuery, String)处理,最后交由Parameter::processParameters(byte,int,int)处理,该函数第一个参数是queryMB的一份拷贝,函数的基本功能是对该拷贝进行解析,得到一个个的解码后的parameter,再add到paramHashValues这样的一个HashMap<String, ArrayList<String>>中去。
01
// -------------------- Parameter parsing --------------------
02
// we are called from a single thread - we can do it the hard way
03
// if needed
04
ByteChunk tmpName=
new
ByteChunk();
05
ByteChunk tmpValue=
new
ByteChunk();
06
private
final
ByteChunk origName=
new
ByteChunk();
07
private
final
ByteChunk origValue=
new
ByteChunk();
08
CharChunk tmpNameC=
new
CharChunk(
1024
);
09
public
static
final
String DEFAULT_ENCODING =
"ISO-8859-1"
;
10
private
static
final
Charset DEFAULT_CHARSET =
11
Charset.forName(DEFAULT_ENCODING);
还记得前面提出的默认编码问题吗?您猜对了,就是在这儿定义了默认的default encoding
1
public
static
final
String DEFAULT_ENCODING =
"ISO-8859-1"
;
基本思想是:遍历字节数组,依次得到name和value值,然后调用urlDecoder对name和value进行解码,最后调用addParameter(name,value)方法添加到Parameter::HashMap<String, ArrayList<string>>中去。
queryString参数解析算法描述
01
pos: 开始位置
02
end: 结束位置
03
while
(pos < end)
04
nameStart: 初始化为pos,参数名称开始位置
05
nameEnd: 初始化为-
1
,参数名称结束位置,通过nameStart和nameEnd可获得参数名称
06
valueStart: 初始化为-
1
,参数值开始位置
07
valueEnd: 初始化为-
1
,参数值结束位置,通过valueStart和valueEnd可获得参数值
08
parsingName:布尔类型,初始化为
true
,用来标识是否正在解析名称
09
parameterComplete: 布尔类型,初始化为
false
,用来标识一个parameter是否解析完成
10
11
do
12
swtich(当前位置pos对应的字节)
13
'='
:
14
是否正在解析参数名称,是则nameEnd = pos, parsingName =
false
, pos++, valueStart = pos;
15
否则pos++;
16
'&'
:
17
是否正在解析参数名称,如果是,则nameEnd=pos,否则valueEnd = pos, pos++;
18
parameterComplete=ture参数整体解析完成
19
pos++;
20
default
:
21
pos++;
22
while
(parameter未解析完成 且 pos < end)
23
24
if
(pos == end)
25
if
(nameEnd == -
1
)
26
nameEnd = pos;
27
if
(valueStart > -
1
&& valueEnd == -
1
)
28
valueEnd = pos;
29
end
while
算法点评
上述算法的精髓在于四个位置indicator和两个boolean变量,在完成一次parameter解析后,通过nameStart,nameEnd获得parameter.name的值,通过valueStart, valueEnd获得parameter.value的值,而parameterComplete用来标识一次parameter解析是否完成,parsingName用来标识是否正在解析名称(为什么需要这个标识?因为有些时候,parameter.value可能为空如name=&passwd=123这种情况下)。
算法源码
01
int
pos = start;
02
int
end = start + len;
03
04
while
(pos < end) {
05
int
nameStart = pos;
06
int
nameEnd = -
1
;
07
int
valueStart = -
1
;
08
int
valueEnd = -
1
;
09
10
boolean
parsingName =
true
;
11
boolean
decodeName =
false
;
12
boolean
decodeValue =
false
;
13
boolean
parameterComplete =
false
;
14
15
do
{
16
switch
(bytes[pos]) {
17
case
'='
:
18
if
(parsingName) {
19
// Name finished. Value starts from next character
20
nameEnd = pos;
21
parsingName =
false
;
22
valueStart = ++pos;
23
}
else
{
24
// Equals character in value
25
pos++;
26
}
27
break
;
28
case
'&'
:
29
if
(parsingName) {
30
// Name finished. No value.
31
nameEnd = pos;
32
}
else
{
33
// Value finished
34
valueEnd = pos;
35
}
36
parameterComplete =
true
;
37
pos++;
38
break
;
39
case
'%'
:
40
case
'+'
:
41
// Decoding required
42
if
(parsingName) {
43
decodeName =
true
;
44
}
else
{
45
decodeValue =
true
;
46
}
47
pos ++;
48
break
;
49
default
:
50
pos ++;
51
break
;
52
}
53
}
while
(!parameterComplete && pos < end);
54
55
if
(pos == end) {
56
if
(nameEnd == -
1
) {
57
nameEnd = pos;
58
}
else
if
(valueStart > -
1
&& valueEnd == -
1
){
59
valueEnd = pos;
60
}
61
}
62
...
63
}
上述代码通过一次遍历处理,得到nameStart, nameEnd, valueStart, valueEnd四个indicator,这样便可得到name, value值。在得到parameter.name和parameter.value后,接着就需要对其进行urldecode操作,decode完成之后,调用addParameter(name, value)方法将其添加到hashmap中。
01
tmpName.setBytes(bytes, nameStart, nameEnd - nameStart);
02
if
(valueStart >=
0
) {
03
tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart);
04
}
else
{
05
tmpValue.setBytes(bytes,
0
,
0
);
06
}
07
08
try
{
09
String name;
10
String value;
11
12
if
(decodeName) {
13
urlDecode(tmpName);
14
}
15
tmpName.setCharset(charset);
16
name = tmpName.toString();
17
18
if
(valueStart >=
0
) {
19
if
(decodeValue) {
20
urlDecode(tmpValue);
21
}
22
tmpValue.setCharset(charset);
23
value = tmpValue.toString();
24
}
else
{
25
value =
""
;
26
}
27
28
try
{
29
addParameter(name, value);
30
}
catch
(IllegalStateException ise) { <span></span>
// Hitting limit stops processing further params but does
31
...
32
}
33
}
catch
(IOException e) {
34
...
35
}
36
37
tmpName.recycle();
38
tmpValue.recycle();
上述代码是对tmpName和tmpValue进行urldecode操作,然后将解码后的信息addParameter。
关于decode的一些说明:
在得到name和value后,调用UDecoder对其解码,如果tomcat的server.xml中未定义URIEncoding,则使用默认的"ISO-8859-1"对其进行解码。
Futuer work
在本次源码分析过程中,尚有一些未解决的问题,将在以后分析的过程中,逐步的解决。
问题列表:
1. tomcat是在什么时候加载server.xml配置文件的,得到URIEncoding值的;
2. digest是如何解析xml文件的;
3. 底层的coyote是如何实现的;
.....
下一篇将分析该贴中http://www.oschina.net/question/853764_103942出现问题的原因
本文中提到的相关函数源码,我已经集中放在另外一篇博客方便大家集中查阅http://my.oschina.net/gschen/blog/120740。
- tomcat源码分析之getQueryString()与getParameter(String)
- tomcat源码分析之getParameter(String)与getQueryString()
- tomcat源码分析之getParameter(String)与getQueryString()
- tomcat源码分析之getParameter(String)与getQueryString()
- request.getQueryString()与request.getParameter()乱码
- request.getParameter()与getParameter(String name)对比
- Tomcat源码分析之Context的创建与启动分析
- JavaWeb笔记getParameter,getParameterValues,getParameterMap,getQueryString
- Tomcat源码阅读之StandardService与MapperListener分析
- Tomcat源码阅读之StandardEngine分析与Valve的设计
- Tomcat源码阅读之StandardHost与HostConfig的分析
- Tomcat源码分析之StringManager与单例模式
- JDK源码分析之String
- String源码分析之BeanFactory
- Java源码分析之String
- Java源码分析之String
- tomcat源码分析之connector
- Tomcat源码分析之Connector
- jQuery整理笔记七----几个经典表单应用
- 黑马程序员——关于LinkedList集合的索引
- 动态内存分配
- 湘潭大学OJ1200ProblemCRC(摸拟,坑题)
- Xen基本原理
- tomcat源码分析之getParameter(String)与getQueryString()
- 算法java实现--回溯法--n后问题
- jQuery整理笔记六----jQuery动画
- 漫话Unity3D(四)
- 关于多态基类的析构函数声明为virtual
- cocos2dx-3.0rc0安装和创建新工程
- 地址
- gcc vs g++
- 程序员的幽默