*無論@RequestBody還是@RequestParam注解一樣,都會使用全局的Encoding進行解碼,會導致特殊編碼的參數值丟失。
只要拋棄掉注解,就完全可以在Controller層得到請求的Raw數據!
-----
使用框架可以節約開發時間,但有時由於隱藏了一些實現細節,導致對底層的原理知之不詳,碰到問題時不知道該從哪一個層面入手解決。因此我特意記錄了下面這個典型問題的調查和解決過程供參考。
事情是這樣的,我們原來有一個移動端調用的發表評論的API,是幾年前在NET平台上開發的,移植到JAVA后,發現安卓版APP無法正常發表漢字評論。
基於SpringMVC創建的JAVA版API接口大致如下,經調查發現,關鍵的content參數,在Controller層檢查結果為空。
@RequestMapping(value = "/Test.api")
public Object test(
HttpServletRequest request,
HttpServletResponse response,
@RequestParam(value = "content", required = false, defaultValue="") String content) {
// 在這里,content的值為空
}
用Charles抓包檢查Post的Form數據,確實有字段content,且有漢字值。但檢查其Raw數據居然為這樣的形式:content=%u611f%u53d7%u4e00%u4e0b%u8d85%u4eba%u7684%u808c%u8089%uff0c
我們知道,目前java常用的URLEncoder類,一般將漢字轉換成"%xy"的形式,xy是兩位16進制的數值,不會出現%u后面跟4個字符這種情況。
%u開頭代表這是一種Unicode編碼格式,后面的四個字符是二字節unicode的四位16進制碼。在Charles軟件上,支持這種解碼,所以可以正常看到抓包數據中的漢字。
但是我們從SpringMVC框架層面統一指定了encoding為UTF-8,根據@RequestParam注解,使用UTF-8進行content參數的解碼時,必然異常,由此導致了Post過來的content字段丟失。
和安卓團隊確認,發現過去他們確實采用了自己獨有的Encode方法對Post數據進行編碼:
public static String UrlEncodeUnicode(final String s) { if (s == null) { return null; } final int length = s.length(); final StringBuilder builder = new StringBuilder(length); // buffer for (int i = 0; i < length; i++) { final char ch = s.charAt(i); if ((ch & 0xff80) == 0) { if (Utils.IsSafe(ch)) { builder.append(ch); } else if (ch == ' ') { builder.append('+'); } else { builder.append("%"); builder.append(Utils.IntToHex((ch >> 4) & 15)); builder.append(Utils.IntToHex(ch & 15)); } } else { builder.append("%u"); builder.append(Utils.IntToHex((ch >> 12) & 15)); builder.append(Utils.IntToHex((ch >> 8) & 15)); builder.append(Utils.IntToHex((ch >> 4) & 15)); builder.append(Utils.IntToHex(ch & 15)); } } return builder.toString(); }
采用這種方式的原因已經不可考證,並且安卓團隊已經決定將在未來版本中放棄該編碼方式,采用JAVA常用的Encoder類進行UTF-8的編碼。問題定位后,決定新版API中必須要兼容新舊兩種編碼方式。
但是目前SpringMVC的@RequestParam注解負責了請求數據的解碼,我們從哪一層切入,截獲請求數據,判斷其編碼方式,並動態選用不同的解碼方式來處理呢?
經過DEBUG,覺得下面的方式是可行的。
解決問題的步驟:
修改API的接口形式,放棄@RequestParam注解,放棄@RequestBody注解,在Controller層直接從request的stream中得到參數的raw數據並自己解析
@RequestMapping(value = "/Test.api") public Object test( HttpServletRequest request, HttpServletResponse response) { String line = ""; StringBuilder body = new StringBuilder(); int counter = 0; InputStream stream; stream = request.getInputStream(); //讀取POST提交的數據內容 BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); while ((line = reader.readLine()) != null) { if(counter > 0){ body.append("\r\n"); } body.append(line); counter++; } //POST請求的raw數據可以獲得 System.out.println(body.toString()); // 使用自定義的解析工具類對body的內容進行解碼 RequestParamMap map = new RequestParamMap(body.toString()); ... }
只要在進入controller層之前,確保沒有別的Filter之類調用了request.getInputStream()就沒有問題,因為request.getInputStream()只能被碰一次,之后就再無法獲取原始的請求數據了。
然而,接着就發現,確實有一個自定義的攔截器,需要獲取POST的InputStream數據...
http://www.cnblogs.com/csliwei/p/5557353.html
-----
