Tomcat源碼分析之getParameter(String)與getQueryString()

歡迎點擊「算法與編程之美」↑關注咱們!git

本文首發於微信公衆號:"算法與編程之美",歡迎關注,及時瞭解更多此係列博客。web

本文有些地方的描述對某些人來講可能比較羅嗦,若是想直接進入正題,可閱讀「源碼分析」節。但本文是本身一步步分析解決問題思路的記錄,雖然有些地方的思考還不是很深刻,主要是因爲時間不是很充裕(雖然花了三天時間,但感受仍是不夠),我會在後續的博文中,結合本身遇到的實際問題或在論壇中看到的別人提出的問題,一步步的帶着問題深刻分析tomcat源碼,這種帶着問題進行源碼分析的方式,比較有針對性,不至於讓本身迷失在源碼的汪洋之中。若是你們對博客格式或其餘方面有比較好的建議,歡迎指出,很是感謝。算法

本次源碼分析的目標是:apache

弄清楚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
….tomcat

程序代碼:服務器

final StringBuffer fileData = new StringBuffer(1000);

    final BufferedReader reader = new BufferedReader(

            new FileReader("fileIn.txt"));

    char[] buf = new char[1024];

    int numRead = 0;

    while ((numRead = reader.read(buf)) != -1)

    {

        final String readData = String.valueOf(buf, 0, numRead);

        fileData.append(readData);

        buf = new char[1024];

    }

    reader.close();

    String mergedContent = fileData.toString();

    mergedContent = mergedContent.replaceAll("\\<.*?>", " ");

    mergedContent = mergedContent.replaceAll("\\r\\n|\\r|\\n", " ");

    final BufferedWriter out = new BufferedWriter(

            new OutputStreamWriter(

                    new FileOutputStream("fileOut.txt")));

    final HashMap<String, String> wordsMap = new HashMap<String, String>();

    final String test[] = mergedContent.split(" ");

    for (final String string : test)

    {
        wordsMap.put(string, string);

    }
    for (final String string : wordsMap.values())

    {
        out.write(string + "\n");

    }
    out.close();

這種狀況下,發現文件B中的內容爲亂碼,而若是將上述程序中的部分代碼改成下面這樣,則會獲得指望的結果。

...

        for (final String string : test)
        {
                        out.write(string + "\n");
            //wordsMap.put(string, string);

        }

        //for (final String string : wordsMap.values())

        //{

        //  out.write(string + "\n");

        //}
        out.close();

出現這種狀況的緣由,我也不是很理解,原文中關於該貼的回答,我以爲和問題沒有任何關係,大多數人都在講如何解決這個問題,而沒有提到出現上述狀況的緣由。

通過該貼和其餘一些相關帖的瞭解,我發現引言中提出的問題貌似和hashmap的encoding沒有任何關係,可能存在別的緣由,因而本身寫了一個簡單的servlet來實踐一下。

實踐

首先是問題重現,我寫了一個簡單的servlet以下所示:

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

@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		//System.out.println(req);
		System.out.println("Request::getParameter(addr) is: "+ req.getParameter("addr"));
		String queryString = req.getQueryString();
		System.out.println("queryString is: "+queryString);
		String[]  params = queryString.split("[=]");
		
		Map<String, String> map = new HashMap<String, String>();
		
		map.put(params[0], params[1]);
		System.out.println("Map::get(addr) is: "+map.get(params[0]));
		return;
	}

在運行的時候,獲得的結果是:

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"):

<Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443"
               URIEncoding="utf-8"
/>

 

經過上述配置文件的修改,咱們獲得的測試結果以下:

經上述分析,咱們能夠得出,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&ccedil;ade模式的包裝類,因此咱們須要先了解一下fa&ccedil;ade模式的相關知識。

Facade模式介紹

facade模式的核心是爲子系統的一組接口提供一個統一的界面,方便客戶端使用子系統,客戶端也沒必要關心子系統的具體實現。

facade設計模式的適用狀況:

1. 原來的類提供的接口比較多,也比較複雜,而咱們只須要使用其部分接口;

2. 原類提供的接口只能部分的知足咱們的須要,且不但願重寫一個新類來代替原類;

...

在本文中,RequestFacade是對Request的一個封裝,因爲Request自己提供的接口很是之多,而本系統中只須要使用其部分功能,在實際分析過程當中,咱們發現Request的具體工做最後delegate到底層的coyote.Request去作。

RequestFacade::getQueryString()分析

如何進行源碼的閱讀和分析?我通常的思路是,先分析正常的處理邏輯,對於那些日誌,錯誤處理,變量定義等等能夠先不用關注,從而達到快速瞭解總體架構或關鍵流程。

基於上述思路,咱們獲得其處理流程以下:

-RequestFacade::getQueryString()

         -Request::getQueryString()

                   -org.apache.coyote.Request::queryString()::toString()

 

經過以上分析能夠看出,其處理流程比較簡單,經過一步步的delegate,最後真正作工做的是coyote.Request,因此咱們接下來只須要分析該類是如何處理。

相關函數源碼以下:

org.apache.catalina.connector.RequestFacade::getQueryString()

@Override
    public String getQueryString() {
        if (request == null) {
            throw new IllegalStateException(
                            sm.getString("requestFacade.nullRequest"));
        }
        return request.getQueryString();
    }

 

org.apache.catalina.connector.Request::getQueryString()

/**

     * Return the query string associated with this request.

     */

    @Override
    public String getQueryString() {
        return coyoteRequest.queryString().toString();
    }

 

org.apache.coyote.Request::queryString()

public MessageBytes queryString() {
        return queryMB;
    }

 

coyote.Request::queryString()作的工做很是簡單,僅是返回類型爲MessageBytes的queryMB字段,但這個字段是什麼時候被賦值的呢?這是一個很是有必要弄清的問題,由於極有可能會在賦值以前進行decode操做。

queryMB賦值分析

接下來探討下queryMB是在什麼時候被賦值的?

queryMB是org.apache.coyote.Request的一個私有成員變量,其數據類型爲MessageBytes,定義以下:

private MessageBytes queryMB = MessageBytes.newInstance();

 

咱們如何定位queryMB這個變量是在何時賦值的呢?在eclipse中,選中queryMB,點擊鼠標右鍵,選擇open call hierarchy,能夠看到queryMB在哪些地方被調用,截圖以下所示:

從上圖能夠看出,有三個地方調用了queryMB,分別是:    

public MessageBytes queryString() {
        return queryMB;
}

 

該函數是得到一個queryMB對象,既然得到了該對象,那麼頗有可能在得到對象後對其進行某些操做如賦值操做。

public void recycle() {
….
    queryMB.recycle();
….
}

 

顧名思義,queryMB.recycle()是對queryMB的從新回收利用,對該對象進行reset操做,和賦值沒有任何聯繫。

public Request() {
        parameters.setQuery(queryMB);
        parameters.setURLDecoder(urlDecoder);
}

 

Request()構造函數中,對其成員變量parameters進行了賦值,和queryMB的賦值沒有關係。

根據上述三種狀況的分析,咱們得出只有在第一種狀況最有可能出現賦值操做,因此接下來將繼續分析queryString()被哪些函數所調用,以下圖所示:

從截圖看出共有七個函數調用了queryString(),從函數名,咱們能夠簡單的判斷出,只有parseRequestLine(boolean)這個函數最有可能對其進行賦值,這個函數是解析http請求request line信息。

    /**
     * Read the request line. This function is meant to be used during the 
     * HTTP request header parsing. Do NOT attempt to read the request body 
     * using it.
     *
     * @throws IOException If an exception occurs during the underlying socket
     * read operations, or if the given buffer is not big enough to accommodate
     * the whole line.
     * @return true if data is properly fed; false if no data is available 
     * immediately and thread should be freed
     */
@Override
    public boolean parseRequestLine(boolean useAvailableData)
        throws IOException {
….
        if (questionPos >= 0) {
            request.queryString().setBytes(buf, questionPos + 1,
                                           end - questionPos - 1);
            request.requestURI().setBytes(buf, start, questionPos - start);
        } else {
            request.requestURI().setBytes(buf, start, end - start);
        }
….
}

 

從上述代碼,咱們能夠看到,在解析http request line的時候,的確對queryMB進行了操做,直接從inputbuffer中得到字節信息,並對queryMB進行賦值。

request.queryString().setBytes(buf, questionPos + 1,
                                           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()的正常處理流程,以下:

-RequestFacade::getParameter(String)

         -Request::getParameter(String)

                   -Request::parseParameters()

                            -coyote.Request::getParameters()

                            -Parameters::setLimit(int)

                            -Parameters::setEncoding(String)

                            -Parameters::handleQueryParameters()

                                     -decodedQuery.duplicate(MessageBytes)

                                     -Parameters::processParameters(MessageBytes, String)

                                             -Parameters::processParameters(byte[],int,int,String) 
                   -coyote.Request::getParameters()::getParameter(String)

                            -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)

 

 // -------------------- Processing --------------------
    /** Process the query string into parameters
     */
    public void handleQueryParameters() {
       ...
        try {
            decodedQuery.duplicate( queryMB );
        } catch (IOException e) {
            // Can't happen, as decodedQuery can't overflow
            e.printStackTrace();
        }
        processParameters( decodedQuery, queryStringEncoding );
    }

第二個和第三個參數類型都爲int,分別是queryString的開始位置和queryString的長度以下:

public void processParameters( MessageBytes data, String encoding ) {
        ...
        ByteChunk bc=data.getByteChunk();
        processParameters( bc.getBytes(), bc.getOffset(),
                           bc.getLength(), getCharset(encoding));
    }

第四個參數爲String類型,意思是利用何種方式進行解碼,若是未定義,則使用默認的編碼方式解碼( 關於tomcat何時解析配置文件,得到connector節中的URIEncoding編碼信息,並傳到本函數的encoding,將在後面的博文中一步步的詳細闡述 :tomcat源碼分析之解析server.xml )。

大體的處理流程是,一步步的解析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>>中去。

  // -------------------- Parameter parsing --------------------
    // we are called from a single thread - we can do it the hard way
    // if needed
    ByteChunk tmpName=new ByteChunk(); 
    ByteChunk tmpValue=new ByteChunk();
    private final ByteChunk origName=new ByteChunk();
    private final ByteChunk origValue=new ByteChunk();
    CharChunk tmpNameC=new CharChunk(1024);
    public static final String DEFAULT_ENCODING = "ISO-8859-1";
    private static final Charset DEFAULT_CHARSET =
    Charset.forName(DEFAULT_ENCODING);

還記得前面提出的默認編碼問題嗎?您猜對了,就是在這兒定義了默認的default encoding

public static final String DEFAULT_ENCODING = "ISO-8859-1";

基本思想是:遍歷字節數組,依次獲得name和value值,而後調用urlDecoder對name和value進行解碼,最後調用addParameter(name,value)方法添加到Parameter::HashMap<String, ArrayList<string>>中去。

queryString參數解析算法描述

pos: 開始位置
end: 結束位置
while(pos < end)
	nameStart: 初始化爲pos,參數名稱開始位置
	nameEnd: 初始化爲-1,參數名稱結束位置,經過nameStart和nameEnd可得到參數名稱
	valueStart: 初始化爲-1,參數值開始位置
	valueEnd: 初始化爲-1,參數值結束位置,經過valueStart和valueEnd可得到參數值
	parsingName:布爾類型,初始化爲true,用來標識是否正在解析名稱
	parameterComplete: 布爾類型,初始化爲false,用來標識一個parameter是否解析完成
	
	do
		swtich(當前位置pos對應的字節)
			'=':
				是否正在解析參數名稱,是則nameEnd = pos, parsingName = false, pos++, valueStart = pos;
				不然pos++;
			'&':
				是否正在解析參數名稱,若是是,則nameEnd=pos,不然valueEnd = pos, pos++;
				parameterComplete=ture參數總體解析完成
				pos++;
			default:
				pos++;
	while(parameter未解析完成 且 pos < end)
	
	if(pos == end)
		if(nameEnd == -1)
			nameEnd = pos;
		if(valueStart > -1 && valueEnd == -1)
			valueEnd = pos;
end while

算法點評

上述算法的精髓在於四個位置indicator和兩個boolean變量,在完成一次parameter解析後,經過nameStart,nameEnd得到parameter.name的值,經過valueStart, valueEnd得到parameter.value的值,而parameterComplete用來標識一次parameter解析是否完成,parsingName用來標識是否正在解析名稱(爲何須要這個標識?由於有些時候,parameter.value可能爲空如name=&passwd=123這種狀況下)。

算法源碼

        int pos = start;
        int end = start + len;

        while(pos < end) {
            int nameStart = pos;
            int nameEnd = -1;
            int valueStart = -1;
            int valueEnd = -1;

            boolean parsingName = true;
            boolean decodeName = false;
            boolean decodeValue = false;
            boolean parameterComplete = false;

            do {
                switch(bytes[pos]) {
                    case '=':
                        if (parsingName) {
                            // Name finished. Value starts from next character
                            nameEnd = pos;
                            parsingName = false;
                            valueStart = ++pos;
                        } else {
                            // Equals character in value
                            pos++;
                        }
                        break;
                    case '&':
                        if (parsingName) {
                            // Name finished. No value.
                            nameEnd = pos;
                        } else {
                            // Value finished
                            valueEnd  = pos;
                        }
                        parameterComplete = true;
                        pos++;
                        break;
                    case '%':
                    case '+':
                        // Decoding required
                        if (parsingName) {
                            decodeName = true;
                        } else {
                            decodeValue = true;
                        }
                        pos ++;
                        break;
                    default:
                        pos ++;
                        break;
                }
            } while (!parameterComplete && pos < end);

            if (pos == end) {
                if (nameEnd == -1) {
                    nameEnd = pos;
                } else if (valueStart > -1 && valueEnd == -1){
                    valueEnd = pos;
                }
            }
            ...
        }

上述代碼經過一次遍歷處理,獲得nameStart, nameEnd, valueStart, valueEnd四個indicator,這樣即可獲得name, value值。在獲得parameter.name和parameter.value後,接着就須要對其進行urldecode操做,decode完成以後,調用addParameter(name, value)方法將其添加到hashmap中。

            tmpName.setBytes(bytes, nameStart, nameEnd - nameStart);
            if (valueStart >= 0) {
                tmpValue.setBytes(bytes, valueStart, valueEnd - valueStart);
            } else {
                tmpValue.setBytes(bytes, 0, 0);
            }

            try {
                String name;
                String value;

                if (decodeName) {
                    urlDecode(tmpName);
                }
                tmpName.setCharset(charset);
                name = tmpName.toString();

                if (valueStart >= 0) {
                    if (decodeValue) {
                        urlDecode(tmpValue);
                    }
                    tmpValue.setCharset(charset);
                    value = tmpValue.toString();
                } else {
                    value = "";
                }

                try {
                    addParameter(name, value);
                } catch (IllegalStateException ise) { // Hitting limit stops processing further params but does
                    ...
                }
            } catch (IOException e) {
               ...
            }

            tmpName.recycle();
            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出現問題的緣由

若是您對算法或編程感興趣,歡迎掃描下方二維碼並關注公衆號「算法與編程之美」,和您一塊兒探索算法和編程的神祕之處,給您不同的解題分析思路。

相關文章
相關標籤/搜索