記錄一下通過分析Tomcat內部jar包找出request.getReader()所用的字元編碼在哪裡設置和起效的完整分析流程

来源:http://www.cnblogs.com/silentdoer/archive/2017/11/19/7862426.html
-Advertisement-
Play Games

前言: 之前寫Java服務端處理POST請求時遇到了請求體轉換成字元流所用編碼來源的疑惑,在doPost方法里通過request.getReader()獲取的BufferedReader對象內部的 Reader用的是什麼編碼將位元組流轉換成字元流的呢?又是在哪裡設置呢和什麼時候生效的呢?通過查找資料, ...


前言:

  之前寫Java服務端處理POST請求時遇到了請求體轉換成字元流所用編碼來源的疑惑,在doPost方法里通過request.getReader()獲取的BufferedReader對象內部的

Reader用的是什麼編碼將位元組流轉換成字元流的呢?又是在哪裡設置呢和什麼時候生效的呢?通過查找資料,我瞭解到通過HttpServletRequest對象獲得請求體數據

有三種方法,其中兩種是不管HTTP請求頭設置Content-Type為何值都能夠在不重覆獲取輸入流的前提下獲取到數據的,一個是request.getInputStream(),一個是request.getReader();

對於前者我們可以在其上面套一個InputStreamReader並設置編碼便能正確讀取出字元數據,但是對於後者猜測是通過request.setCharacterEncoding(charsetName);來設置;但是當時

挺想知道這兩句代碼是怎麼關聯起來的,於是就開始了讀源碼的過程。

步驟:

  最開始的時候我是想通過request.getReader()來找出答案,於是通過列印request.getClass().toString(),知道了request對象真正的類是org.apache.catalina.connector.RequestFacade,

通過名字最終找出這個類是Tomcat安裝目錄中lib目錄下的catalina.jar,導入到項目找出RequestFacade.getReader()的源碼為:

public BufferedReader getReader() throws IOException {
    if (this.request == null) {
        throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
    } else {
        return this.request.getReader();
    }
}

然後找出this.request的類是org.apache.catalina.connector.Request,通過RequestFacade構造方法初始化,接著找到org.apache.catalina.connector.Request.getReader()的代碼為:

public BufferedReader getReader() throws IOException {
    if (this.usingInputStream) {
        throw new IllegalStateException(sm.getString("coyoteRequest.getReader.ise"));
    } else {
        this.usingReader = true;
        this.inputBuffer.checkConverter();
        if (this.reader == null) {
            this.reader = new CoyoteReader(this.inputBuffer);
        }
        return this.reader;
    }
}

這裡註意this.inputBuffer.checkConverter();這裡將會把request.setCharacter(charsetName)設置的編碼應用在位元組流轉換為字元串的過程上,這個過程後面再講。

我們先看new CoyoteReader(this.inputBuffer);由於CoyoteBuffer是繼承自BufferedReader,故真正將位元組流轉換為字元流的應該是this.inputBuffer,

查看代碼得知它的類型為:org.apache.catalina.connector.InputBuffer,類定義為:

public class InputBuffer extends Reader implements ByteInputChannel, CharInputChannel, CharOutputChannel {
。。。。。
}

由於它和InputStreamReader有共同的父類Reader,故我猜測將位元組流轉換成字元流的應該就是InputBuffer類了,但是線索到了就斷了我不知道接下來該看哪裡了(後來理清思路後發現其實應該往上找看InputBuffer是在哪創建及賦值的),

於是我回到最初的猜測,request.getReader()是通過request.setCharacterEncoding(charsetName)來實現的;通過查看request.setCharacterEncoding(charsetName)源碼

得知RequestFacade設置字元編碼是通過內部的org.apache.catalina.connector.Request,而這個Request又是通過內部的org.apache.coyote.Request來實現的,導入所需jar包:tomcat-coyote.jar

其中coyoteRequest.setCharacterEncoding(charsetName)的代碼為:

public void setCharacterEncoding(String enc) {
    this.charEncoding = enc;
}

到了這裡後線索又斷了,我只知道最初RequestFacade設置的編碼最終是保存在org.apache.catalina.connector.Request里,但是這個編碼是什麼時候用到了InputBuffer上就不知道了。

趁著這階段還弄清楚了RequestFacade無論是設置編碼、獲得編碼、getContentLength()等方法本質上都是通過org.apache.coyote.Request來最終實現的。

回到正題,線索斷了以後我後來通過找到是哪裡new了InputBuffer及是哪裡給InputBuffer設置編碼和位元組流等思考繼續回到了org.apache.catalina.connector.Request類的定義里,

通過搜索發現org.apache.catalina.connector.Request內部的this.inputBuffer是在構造方法里創建的,但是只有一個空殼,而RequestFacade.getInputStream()最終也是以this.inputBuffer作為了

位元組流的參數new CoyoteInputStream(this.inputBuffer);故它可能本身既能讀取字元流又能讀取位元組流,即它是存儲著第一手的數據。

接著找到了org.apache.catalina.connector.Request中的一個方法:

public void setCoyoteRequest(org.apache.coyote.Request coyoteRequest) {
    this.coyoteRequest = coyoteRequest;
    this.inputBuffer.setRequest(coyoteRequest);
}

我之前一直鑽進找InputBuffer編碼的巷道里,忘了找coyoteRequest這麼重要的屬性是從哪賦值的了,經過搜查org.apache.catalina.connector.Request里只有這個set方法可以給this.coyoteRequest賦值,故這個set方法

一定會執行,也就是說this.inputBuffer.setRequest(coyoteRequest);會執行,而coyoteRequest里保存著RequestFacade設置的編碼,故而InputBuffer里需要的編碼來源有了。

接著看InputBuffer里哪裡會用到這個coyoteRequest,找了一下InputBuffer里一大堆方法都用到了coyoteRequest,經過一番思考想到外部程式是通過BufferedReader來讀取字元流的,而BufferedReader讀取字元流又是

通過構造方法初載入的的Reader來讀取的,即是通過InputBuffer的Read(char[]....)方法讀取數據的,故找到InputBuffer中的這個方法,定義如下:

public int read(char[] cbuf, int off, int len) throws IOException {
    if (this.closed) {
        throw new IOException(sm.getString("inputBuffer.streamClosed"));
    } else {
        return this.cb.substract(cbuf, off, len);
    }
}

可見InputBuffer讀取字元流又是通過this.cb的substract方法讀取的,查找代碼得知cb是CharChunk類,導入jar包:tomcat-util.jar,CharChunk.substract的源碼為:

public int substract(char[] src, int off, int len) throws IOException {
        int n;
        if (this.end - this.start == 0) {
            if (this.in == null) {
                return -1;
            }

            n = this.in.realReadChars(this.buff, this.end, this.buff.length - this.end);
            if (n < 0) {
                return -1;
            }
        }

        n = len;
        if (len > this.getLength()) {
            n = this.getLength();
        }

        System.arraycopy(this.buff, this.start, src, off, n);
        this.start += n;
        return n;
    }

這裡面的this.in.realReadChars(...)很關鍵,從名字可以猜測這個是真正讀取字元數組的方法,然後通過查找,this.in就是之前的InputBuffer對象。

然後我通過看CharChunk的代碼,發現this.start和this.end最初值為0,故第一次調用此方法時會執行this.in.realReadChars(...),我們來看這個方法定義:

public int realReadChars(char[] cbuf, int off, int len) throws IOException {
        if (!this.gotEnc) {
            this.setConverter();
        }

        boolean eof = false;
        if (this.bb.getLength() <= 0) {
            int nRead = this.realReadBytes(this.bb.getBytes(), 0, this.bb.getBytes().length);
            if (nRead < 0) {
                eof = true;
            }
        }

        if (this.markPos == -1) {
            this.cb.setOffset(0);
            this.cb.setEnd(0);
        } else {
            this.cb.makeSpace(this.bb.getLength());
            if (this.cb.getBuffer().length - this.cb.getEnd() == 0 && this.bb.getLength() != 0) {
                this.cb.setOffset(0);
                this.cb.setEnd(0);
                this.markPos = -1;
            }
        }

        this.state = 1;
        this.conv.convert(this.bb, this.cb, eof);
        return this.cb.getLength() == 0 && eof ? -1 : this.cb.getLength();
    }

通過查看代碼發現this.goEnc初始為false,只有this.setConverter()後才變為true,故第一次會執行setConverter(),再來看setConverter()的源碼:

protected void setConverter() throws IOException {
        if (this.coyoteRequest != null) {
            this.enc = this.coyoteRequest.getCharacterEncoding();
        }

        this.gotEnc = true;
        if (this.enc == null) {
            this.enc = "ISO-8859-1";
        }

        this.conv = (B2CConverter)this.encoders.get(this.enc);
        if (this.conv == null) {
            if (SecurityUtil.isPackageProtectionEnabled()) {
                try {
                    this.conv = (B2CConverter)AccessController.doPrivileged(new PrivilegedExceptionAction<B2CConverter>() {
                        public B2CConverter run() throws IOException {
                            return new B2CConverter(InputBuffer.this.enc);
                        }
                    });
                } catch (PrivilegedActionException var3) {
                    Exception e = var3.getException();
                    if (e instanceof IOException) {
                        throw (IOException)e;
                    }
                }
            } else {
                this.conv = new B2CConverter(this.enc);
            }

            this.encoders.put(this.enc, this.conv);
        }

    }

有代碼:this.enc = this.coyoteRequest.getCharacterEncoding();

並且通過this.enc初始化了一個B2CConverter對象,從名字可猜測這個類是將位元組流轉換成字元流的轉換器;

我們回到realReadChars(...)的源碼里有必執行的代碼:this.conv.convert(this.bb, this.cb, eof);

這個代碼是將this.bb轉換生成字元流數據到this.cb里(bb是ByteChunk對象),至此可知將位元組流轉換成字元流是通過InputBuffer的this.conv.convert(...)轉換,而字元編碼則是通過setConverter()來獲取coyoteRequest的編碼進行設置在this.conv里,且

setConverter()只執行一次,因為setConverter()內部會將this.gotEnc = true;,故我們需要找出最早執行setConverter()地方,發現除了realReadChars()還有checkConverter()方法也會執行setConverter()方法,而

checkConverter()方法在org.apache.catalina.connector.Request.getReader()方法里就會執行,故可以得知必需先調用RequestFacade.setCharacterEncoding(charsetName)方法再執行getReader()方法,順序錯了設置的編碼將不會起效於Reader中,

對於ResponseFacade.getWriter()也是一樣。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 使用靜態方法實現類的多態 類的封裝--升級版 繼承升級版 ...
  • 註:本文為mysql基礎知識的總結,基礎點很多若是有些不足夠,還請自行搜索。後續增加 一、mysql簡介 資料庫簡介 資料庫是電腦應用系統中的一種專門管理數據資源的系統 資料庫是一組經過電腦處理後的數據,存儲在多個文件中,而管理資料庫軟體被稱為資料庫管理系統 DBMS 而MYSQL ORACLE ...
  • [TOC] PS: 本地預覽目錄OK,但是博客園貌似不支持,那就只能這樣了。 前言(可以不看) 最開始只是想寫一篇博文,準備使用markdown,感覺很流行(github、簡書……很多都支持),而且渲染出來很好看,一直很想學,沒有合適的機會,結果拖到了現在。比起什麼python、C之類的編程語言,m ...
  • 發佈-訂閱消息模式 一、訂閱雜誌 我們很多人都訂過雜誌,其過程很簡單。只要告訴郵局我們所要訂的雜誌名、投遞的地址,付了錢就OK。出版社定期會將出版的雜誌交給郵局,郵局會根據訂閱的列表,將雜誌送達消費者手中。這樣我們就可以看到每一期精彩的雜誌了。 發佈-訂閱消息模式 一、訂閱雜誌 我們很多人都訂過雜誌 ...
  • 1. 學習了一下 AI 五子棋,順手改作 19 路的棋盤,便於圍棋通用。render.py 主要修改如下: 2. 發現 pygame 還不錯,便從網上搜索到《Beginning Game Development With Python And Pygame》,其中螞蟻游戲的 AI 表現甚好,主要代碼 ...
  • 前言 本篇將結合JDK1.6的TreeMap源碼,來一起探索紅-黑樹的奧秘。紅黑樹是解決二叉搜索樹的非平衡問題。 當插入(或者刪除)一個新節點時,為了使樹保持平衡,必須遵循一定的規則,這個規則就是紅-黑規則: 1) 每個節點不是紅色的就是黑色的 2) 根總是黑色的 3) 如果節點是紅色的,則它的子節 ...
  • 本文主要給大家分享使用matlab編寫代碼,完成課程設計、畢業設計或者研究項目時,matlab調試程式的技巧和方法。 快速完成一個項目,最簡單的方法就是利用前人的開源代碼,然後根據自己項目的具體需求和參數,對已有代碼進行調試,並增加或刪減部分功能,最終實現自己項目的全部功能。所謂“站在前人的肩膀上... ...
  • Joinpoint 連接點 Pointcut 切入點 Advice 通知/增強 舉例: Aspact 切麵 比如給add()增加日誌功能的過程即稱為切麵 還有幾個術語知道就可以,不常用 Introduction (引入)使用動態的方法在原有屬性基礎上 添加新屬性的操作 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...