1. 程式人生 > >spring-cloud-zuul檔案上傳中文名亂碼解決過程

spring-cloud-zuul檔案上傳中文名亂碼解決過程

       由於專案中用到了zuul作為閘道器,所有的請求都要經過zuul轉發,因此上傳請求也被代理了。經過辛辛苦苦的敲程式碼,終於完成了功能開發,上傳非中文名的檔案一切都很完美,可是到了中文檔名時,檔案伺服器收到的請求裡面中文名全部變成了 ‘?’ ,我也是有很多疑問了,同樣的功能,咋就中文名稱不行呢?難道這也有字元編碼的問題?於是乎,開始網上找了,竟然發現都有同樣的問題,不過zuul官方給出瞭解決方案,使用/zuul開頭的請求可以避免中文名亂碼以及支援大檔案上傳,於是乎,按照文件改了一波,沒想到又有了新的問題,上傳檔案不能成功,伺服器接收到的檔案流總是不完整,經過斷點除錯,最終發現是某位同學挖的坑(自定義的可重複讀過濾器中處理了所有的請求,應該放過上傳檔案的請求),填完後發現可以上傳了,而且中文名也解決了,算是完成了整個功能。

      鑑於強迫症,為啥使用不帶/zuul的請求上傳就會有中文亂碼呢,於是開始了跟蹤定位。FormBodyWrapperFilter過濾器是zuul處理請求的第二個過濾器,主要功能就是將請求的流進行重新包裝,主要邏輯是首先取contentData(從reqeust.getInputStream()來)資料,如果為空,則取request.getParts()資料,由於請求首先經過了dispatchServlet,因此只能從request.getParts()取資料,然後進行處理,具體處理的呼叫棧如圖:

FormBodyWrapperFilter.FormBodyRequestWrapper類的getContentType()方法中呼叫了buildContentData(),也就是去獲取contentData,上圖的程式呼叫棧很清晰的反映了呼叫過程。FormBodyWrapperFilter中有個AllEncompassingFormHttpMessageConverter的物件作為convert的成員變數,而AllEncompassingFormHttpMessageConverter是FormHttpMessageConverter的子類,這個類的功能就是對請求form-data進行處理,而對於multipart/form-data請求處理時會用到FormHttpMessageConverter.MultipartHttpOutputMessage,是一個私有的靜態類,也就是說無法對該類進行重寫,該類中最終處理檔名的方法也就是呼叫棧圖中的最後一個方法writeHeaders(),看下該方法的原始碼:

private void writeHeaders() throws IOException {
       if (!this.headersWritten) {
          for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
               byte[] headerName = getAsciiBytes(entry.getKey());
               for (String headerValueString : entry.getValue()) {
                    byte[] headerValue = getAsciiBytes(headerValueString);
                    this.outputStream.write(headerName);
                    this.outputStream.write(':');
                    this.outputStream.write(' ');
                    this.outputStream.write(headerValue);
                    writeNewLine(this.outputStream);
               }
           }      
         writeNewLine(this.outputStream);
         this.headersWritten = true;
      }
}

private byte[] getAsciiBytes(String name) {
     try {
           return name.getBytes("US-ASCII");
         }catch (UnsupportedEncodingException ex) {
         // Should not happen - US-ASCII is always supported.
            throw new IllegalStateException(ex);
         }
}

可以看到在遍歷headers時,對於head中值的處理使用了getAsciiBytes(String name)方法,該方法的實現就是取字串的“US-ASCII”編碼的位元組陣列,那麼對於中文字串的處理就會有亂碼的問題了。這裡就是使用zuul代理上傳時為什麼中文名會亂碼的根本原因了,那麼問題來了,這個問題要怎麼解決呢?可以看到該類是私有的內部類,是無法重寫的,按理說這個問題應該早都有人發現了吧,於是乎,將spring-web版本升級到5.0.6.RELEASE版本後,檢視該類的原始碼:

private void writeHeaders() throws IOException {
   if (!this.headersWritten) {
      for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
           byte[] headerName = getBytes(entry.getKey());
           for (String headerValueString : entry.getValue()) {
               byte[] headerValue = getBytes(headerValueString);
               this.outputStream.write(headerName);
               this.outputStream.write(':');
               this.outputStream.write(' ');
               this.outputStream.write(headerValue);
               writeNewLine(this.outputStream);
            }
         }
       writeNewLine(this.outputStream);
       this.headersWritten = true;
   }
}

private byte[] getBytes(String name) {
    return name.getBytes(this.charset);
}

果然,原始碼已經變了,從原來的getAsciiBytes(String name)變為了getBytes(String name),而getBytes(String  name)的實現會根據設定的字元編碼來獲取對應的位元組陣列,而這個字元編碼是可以通過FormHttpMessageConverter.setCharset()方法來修改的,這樣就不會有亂碼的問題了。因此如果用zuul代理上傳請求,且不能忽略中文名亂碼時,要將spring-web版本升級到5.0.x

      至此,zuul代理上傳請求,中文名亂碼的問題算是解決了,雖然最終的解決方式很簡單,只需要升級一下sping-web的版本,但是定位問題的過程還是比較有重要的,起碼也熟悉了zuul處理請求的過程。