使用框架可以節約開發時間,但有時由於隱藏了一些實現細節,導致對底層的原理知之不詳,碰到問題時不知道該從哪一個層面入手解決。因此我特意記錄了下面這個典型問題的調查和解決過程供參考。
事情是這樣的,我們原來有一個移動端調用的發表評論的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,覺得下面兩種方式是可行的。
解決問題的方法1:
修改API的接口形式,放棄@RequestParam注解,使用@RequestBody注解,直接獲取POST請求的Raw數據,在Controller層獨立解碼
@RequestMapping(value = "/Test.api")
public Object test(
HttpServletRequest request,
HttpServletResponse response,
@RequestBody String body) {
// 在這裡,可以獲得如下的raw數據: Id=185904&content=%u611f%u53d7%u4e00%u4e0b
// 可以自己對raw數據進行解析和解碼 (具體的解碼方式暫不考慮)
}
解決問題的方法2:
通過自定義Filter的形式,在doFilter()方法中獲取request的getInputStream(),也可以得到raw數據,解析後通過setAttribute()方法可以保存request中。
但這個操作過程和@RequestBody注解相比,要改web.xml加Filter配置等,更麻煩一些,不一定要用。
但有一個需要特別注意的事項,就是getInputStream()是一個一次性的動作,一旦被執行了,如果其他地方用到,就無效了。參考下面代碼的注釋:
package org.jiagoushi.api.aop;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Map;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyFilter implements Filter {
protected FilterConfig filterConfig;
String encoding = null;
public void destroy() {
this.filterConfig = null;
}
/**
* 初始化
*/
public void init(FilterConfig filterConfig) {
this.filterConfig = filterConfig;
}
/**
* 過濾處理
*/
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
// String s = servletRequest.getInputStream()
String line = "";
StringBuilder body = new StringBuilder();
int counter = 0;
InputStream stream;
stream = servletRequest.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);
//注意事項:因為servletRequest.getInputStream()被調用過1次,以後再調用也沒有了。
//下面的getParameterMap()和getParameter()方法,本質上也是去getInputStream(),
//所以都無法再獲取到任何參數了
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
Map map = request.getParameterMap();
String v = request.getParameter("content");
// 繼續執行下一個 filter, 無一下個 filter 則執行請求
chain.doFilter(request, response);
}
}
要讓上面的Filter生效,web.xml中需要增加如下配置,注意Filter調用順序和web.xml中寫的先後順序移植
<filter>
<filter-name>myFilter</filter-name>
<filter-class>org.jiagoushi.api.aop.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/Test.api</url-pattern>
</filter-mapping>
此問題的調查告一段落,歡迎討論。