设为首页 收藏本站
查看: 1098|回复: 0

[经验分享] tomcat源码分析之getParameter(String)与getQueryString()

[复制链接]

尚未签到

发表于 2015-11-14 11:32:07 | 显示全部楼层 |阅读模式

本文有些地方的描述对某些人来说可能比较罗嗦,但确实是自己一步步分析解决问题的思路,虽然有些地方的思考还不是很深入,主要是由于时间不是很充裕(虽然花了三天时间,但感觉还是不够),但我会在后续的博文中,结合自己遇到的实际问题或在论坛中看到的别人提出的问题,一步步的带着问题深入分析tomcat源码,这种带着问题进行源码分析的方式,比较有针对性,不至于让自己迷失在源码的汪洋之中。如果大家对博客格式或其他方面有比较好的建议,欢迎指出,非常感谢。


本次源码分析的目标是:


弄清楚org.apache.catalina.conector.RequestFacade::getQueryString()以及getParameter(String)不同及其具体实现,达到此目标即可。


引言


问题的引出是由于前些天在oschina上看到的一篇帖子http://www.oschina.net/question/820641_104356,


DSC0000.png


问题分析


起初的分析思路也是受帖子作者的影响,心想出现这种情况是否是因为hashmap destroy encoding导致的,所以就google了一下hashmap
encoding,得到一个比较相关的答案


http://stackoverflow.com/questions/8427488/hashmap-destroys-encoding,这篇帖子中出现的情况也比较奇怪。


程序功能描述如下:
从文件A中读取一组以空格为分隔符的的字符串,然后将这些字符串一行一行的写入到另外一个文件B中。
如文件A的格式为:
Aaa  bbbbb cdefggg …..
文件B的格式为:
Aaa
Bbbbb
Cdefgggg
….


程序代码:



02
 
04
 
06
 
08
 
10
 
12
 
14
 
16
 
18
 
20
 
22
 
24
 
26
 
28
 
30
 
32
 
34
 
36
 
38
 
40
 
42
 
44
        wordsMap.put(string,
string);
46
    }
48
 
50
        out.write(string
+ "\n");
52
    }
01
...
03
        for (final String
string : test)
05
                        out.write(string
+ "\n");
07
 
09
 
11
 
13
 
15
 
17
        out.close();

出现这种情况的原因,我也不是很理解,原文中关于该贴的回答,我觉得和问题没有任何关系,大多数人都在讲如何解决这个问题,而没有提到出现上述情况的原因。


经过该贴和其他一些相关帖的了解,我发现引言中提出的问题貌似和hashmap的encoding没有任何关系,可能存在别的原因,于是自己写了一个简单的servlet来实践一下。


实践


首先是问题重现,我写了一个简单的servlet如下所示:


//请求的url为:http://localhost:8080/demo/1.do?addr=上海



02
    protected void doGet(HttpServletRequest
req, HttpServletResponse resp)
04
        //System.out.println(req);
06
        String
queryString = req.getQueryString();
08
        String[] 
params = queryString.split("[=]");
10
        Map map = new HashMap();
12
        map.put(params[0],
params[1]);
14
        return;
1
3
               redirectPort="8443"
5
/>

通过上述配置文件的修改,我们得到的测试结果如下:


DSC0001.jpg


经上述分析,我们可以得出,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);


DSC0002.jpg


通过上述打印结果,我们可以看到其具体实现类为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()分析


如何进行源码的阅读和分析?我一般的思路是,先分析正常的处理逻辑,对于那些日志,错误处理,变量定义等等可以先不用关注,从而达到快速了解整体架构或关键流程。


基于上述思路,我们得到其处理流程如下:



2
 
4
 
1
@Override
3
        if (request
== null)
{
5
                            sm.getString("requestFacade.nullRequest"));
7
        return request.getQueryString();
01
/**
03
     *
Return the query string associated with this request.
05
     */
07
    @Override
09
        return coyoteRequest.queryString().toString();
1
public MessageBytes
queryString() {
3
    }

coyote.Request::queryString()做的工作非常简单,仅是返回类型为MessageBytes的queryMB字段,但这个字段是何时被赋值的呢?这是一个非常有必要弄清的问题,因为极有可能会在赋值之前进行decode操作。


queryMB赋值分析


接下来探讨下queryMB是在何时被赋值的?


queryMB是org.apache.coyote.Request的一个私有成员变量,其数据类型为MessageBytes,定义如下:



1
public MessageBytes
queryString() {
3
}

该函数是获得一个queryMB对象,既然获得了该对象,那么很有可能在获得对象后对其进行某些操作如赋值操作。



2
….
4
….
1
public Request()
{
3
        parameters.setURLDecoder(urlDecoder);
01
    /**
03
   
 *
HTTP request header parsing. Do NOT attempt to read the request body 
05
   
 *
07
   
 *
read operations, or if the given buffer is not big enough to accommodate
09
   
 *
@return true if data is properly fed; false if no data is available 
11
   
 */
13
    public boolean parseRequestLine(boolean useAvailableData)
15
….
17
            request.queryString().setBytes(buf,
questionPos + 1,
19
            request.requestURI().setBytes(buf,
start, questionPos - start);
21
            request.requestURI().setBytes(buf,
start, end - start);
23
….
1
request.queryString().setBytes(buf,
questionPos + 1,
01
-RequestFacade::getParameter(String)
03
         -Request::getParameter(String)
05
                   -Request::parseParameters()
07
                            -coyote.Request::getParameters()
09
                            -Parameters::setLimit(int)
11
                            -Parameters::setEncoding(String)
13
                            -Parameters::handleQueryParameters()
15
                                     -decodedQuery.duplicate(MessageBytes)
17
                                     -Parameters::processParameters(MessageBytes,
String)
19
                                               -Parameters::processParameters(byte[],int,int,String)
21
 
01
//
-------------------- Processing --------------------
03
    */
05
      ...
07
           decodedQuery.duplicate(
queryMB );
09
           //
Can't happen, as decodedQuery can't overflow
11
       }
13
   }
第二个和第三个参数类型都为int,分别是queryString的开始位置和queryString的长度如下:




2
        ...
4
        processParameters(
bc.getBytes(), bc.getOffset(),
6
    }
第四个参数为String类型,意思是利用何种方式进行解码,如果未定义,则使用默认的编码方式解码。

大致的处理流程是,一步步的解析queryMB,然后将解析到的每一个parameter添加到一个HashMap中,最后在这个hashmap中根据name find到自己需要的value。


Parameters::handleQueryParameters()函数中先是得到queryMB的一份拷贝,这样可以避免对queryMB直接操作,破坏原始的信息,接着交由Parameters::processParameters(DecodedQuery,
String)处理,最后交由Parameter::processParameters(byte,int,int)处理,该函数第一个参数是queryMB的一份拷贝,函数的基本功能是对该拷贝进行解析,得到一个个的解码后的parameter,再add到paramHashValues这样的一个HashMap中去。





02
  //
we are called from a single thread - we can do it the hard way
04
  ByteChunk
tmpName=new ByteChunk();
06
  private final ByteChunk
origName=new ByteChunk();
08
  CharChunk
tmpNameC=new CharChunk(1024);
10
  private static final Charset
DEFAULT_CHARSET =
1
public static final String
DEFAULT_ENCODING = "ISO-8859-1";

基本思想是:遍历字节数组,依次得到name和value值,然后调用urlDecoder对name和value进行解码,最后调用addParameter(name,value)方法添加到Parameter::HashMap中去。


queryString参数解析算法描述



02
end:
结束位置
04
    nameStart:
初始化为pos,参数名称开始位置
06
    valueStart:
初始化为-1,参数值开始位置
08
    parsingName:布尔类型,初始化为true,用来标识是否正在解析名称
10
     
12
        swtich(当前位置pos对应的字节)
14
                是否正在解析参数名称,是则nameEnd
= pos, parsingName = false,
pos++, valueStart = pos;
16
            '&':
18
                parameterComplete=ture参数整体解析完成
20
            default:
22
    while(parameter未解析完成
且 pos < end)
24
    if(pos
== end)
26
            nameEnd
= pos;
28
            valueEnd
= pos;
01
        int pos
= start;
03
 
05
            int nameStart
= pos;
07
            int valueStart
= -1;
09
 
11
            boolean decodeName
= false;
13
            boolean parameterComplete
= false;
15
            do {
17
                    case '=':
19
                            //
Name finished. Value starts from next character
21
                            parsingName
= false;
23
                        } else {
25
                            pos++;
27
                        break;
29
                        if (parsingName)
{
31
                            nameEnd
= pos;
33
                            //
Value finished
35
                        }
37
                        pos++;
39
                    case '%':
41
                        //
Decoding required
43
                            decodeName
= true;
45
                            decodeValue
= true;
47
                        pos
++;
49
                    default:
51
                        break;
53
            } while (!parameterComplete
&& pos < end);
55
            if (pos
== end) {
57
                    nameEnd
= pos;
59
                    valueEnd
= pos;
61
            }
63
        }

上述代码通过一次遍历处理,得到nameStart, nameEnd, valueStart, valueEnd四个indicator,这样便可得到name, value值。在得到parameter.name和parameter.value后,接着就需要对其进行urldecode操作,decode完成之后,调用addParameter(name, value)方法将其添加到hashmap中。



02
if (valueStart
>= 0)
{
04
} else {
06
}
08
try {
10
    String
value;
12
    if (decodeName)
{
14
    }
16
    name
= tmpName.toString();
18
    if (valueStart
>= 0)
{
20
            urlDecode(tmpValue);
22
        tmpValue.setCharset(charset);
24
    } else {
26
    }
28
    try {
30
    } catch (IllegalStateException
ise) { //
Hitting limit stops processing further params but does
32
    }
34
   ...
36
 
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


版权声明:本文为博主原创文章,未经博主允许不得转载。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-139097-1-1.html 上篇帖子: 让tomcat服务器使用url rewrite 下篇帖子: tomcat7在UBUNTU上自动启动
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表