一俊犯、項目配置:
- Spring 4.4.1-RELEASE
- Jetty 9.3.5
- JDK 1.8
- Servlet 3.1.0
- web.xml文件中沒有配置編解碼Filter
二、實際遇到的問題:
客戶端(比如java)發(fā)送post請求訪問接口伤哺,數(shù)據(jù)放在body里面燕侠,每個參數(shù)utf-8編碼。
從body里面取出的中文參數(shù)是亂碼立莉。
下面是發(fā)送請求的代碼和服務端接收請求的代碼绢彤。
客戶端代碼。
這是一個真實的第三方訪問API的案例蜓耻,這段代碼請求到PHP系統(tǒng)正常茫舶,請求到java系統(tǒng)就會出現(xiàn)亂碼。
但是中文參數(shù)放到URL中解碼正常刹淌,放到請求體中就是亂碼饶氏。
通過httpclient4.1發(fā)送Post請求如下:
public static void postData(String sign, String timestamp) { // 創(chuàng)建默認的httpClient實例.
CloseableHttpClient httpclient=null;
String result="";
try {
httpclient = HttpClients.createDefault();
String url = "http://example/api/entry";
HttpPost httpPost = new HttpPost(url); //設置請求和傳輸超時時間
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(6000).setConnectTimeout(6000).build();
httpPost.setConfig(requestConfig);
MultipartEntity entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);
entity.addPart("app_id", new StringBody("c5eb3ba8c0e7326559", Charset.forName("utf-8")));
entity.addPart("method", new StringBody("kdt.item.add", Charset.forName("utf-8")));
entity.addPart("timestamp", new StringBody(timestamp));
entity.addPart("format", new StringBody("json", Charset.forName("utf-8")));
entity.addPart("v", new StringBody("1.0", Charset.forName("utf-8")));
entity.addPart("sign", new StringBody(sign, Charset.forName("utf-8")));
entity.addPart("sign_method", new StringBody("md5", Charset.forName("utf-8")));
entity.addPart("cid", new StringBody("5000000", Charset.forName("utf-8")));
entity.addPart("tag_ids", new StringBody("0", Charset.forName("utf-8")));
entity.addPart("price", new StringBody("0.01", Charset.forName("utf-8")));
entity.addPart("title", new StringBody("測試", Charset.forName("utf-8")));
entity.addPart("desc", new StringBody("test1", Charset.forName("utf-8"))); //是否是虛擬商品。0為否芦鳍,1為是嚷往。目前不支持虛擬商品
entity.addPart("is_virtual", new StringBody("0", Charset.forName("utf-8")));
entity.addPart("post_fee", new StringBody("0.0", Charset.forName("utf-8"))); //Sku的屬性串。格式:pText:vText;pText:vText柠衅,多個sku之間用逗號分隔皮仁,如:顏色:黃色;尺寸:M,顏色:黃色;尺寸:S。pText和vText文本中不可以存在冒號和分號以及逗號
entity.addPart("sku_properties", new StringBody("color:white", Charset.forName("utf-8")));
entity.addPart("sku_quantities", new StringBody("998,999", Charset.forName("utf-8")));
entity.addPart("sku_prices", new StringBody("0.01,0.02", Charset.forName("utf-8")));
entity.addPart("sku_outer_ids", new StringBody("null,null", Charset.forName("utf-8"))); //該商品的外部購買地址。當用戶購買環(huán)境不支持微信或微博支付時會跳轉(zhuǎn)到此地址
entity.addPart("buy_url", new StringBody("http://img.cdn.sb.hongware.com/1461836641703511.gif", Charset.forName("utf-8")));
entity.addPart("quantity", new StringBody("1998", Charset.forName("utf-8"))); //寶貝修改的時候需要這個參數(shù)
httpPost.setEntity(entity);
CloseableHttpResponse response = httpclient.execute(httpPost);
try {
HttpEntity httpEntity = response.getEntity();
System.out.println(httpEntity.getContent());
InputStream content = httpEntity.getContent();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(content));
String line;
while ( (line=bufferedReader.readLine()) != null) {
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
response.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
httpclient.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}服務端代碼如下
為了簡化演示贷祈,我把參數(shù)提取代碼Map<String, String[]> parameterMap = request.getParameterMap()冗余在這個入口函數(shù)中趋急,以便說明問題:
@RequestMapping(value = "/api/entry", produces = "application/json;charset=utf-8")
public DeferredResult<Object> sign(HttpServletRequest request,HttpServletResponse response) {
DeferredResult<Object> deferredResult = new DeferredResult<>();
Map<String, String[]> parameterMap = request.getParameterMap();
String method = request.getParameter("method");
if (StringUtils.isEmpty(method)) {
ResponseEntity<String> responseEntity = new ResponseEntity<String>( String.format(Constants.ERROR_RESPONSE, 50000, "service or method is null"), HttpStatus.valueOf(200));
deferredResult.setResult(responseEntity);
return deferredResult;
}
int lastIndex = method.lastIndexOf(".");
String service = method.substring(0, lastIndex);
method = method.substring(lastIndex + 1);
event.setService(service);
event.setMethod(method);
event.setResult(deferredResult);
proxy.doAction(request,response,event);
return deferredResult;
}
服務端通過Map<String, String[]> parameterMap = request.getParameterMap()取出所有參數(shù),傳進來title參數(shù)是亂碼J铺堋呜达!
三、根本原因
Servlet 3.0規(guī)范中有關(guān)請求數(shù)據(jù)編碼的解釋如下:
當前很多瀏覽器并不發(fā)送帶Content-Type頭部的字符編碼標識符粟耻,它會把字符編碼的決定留在讀取HTTP請求的時候查近。如果客戶端沒有指明編碼,容器用來創(chuàng)建請求讀和解析POST數(shù)據(jù)的默認編碼必須是"ISO-8859-1"挤忙。然而霜威,為了提示開發(fā)者客戶端沒有成功發(fā)送一個字符編碼,容器中g(shù)etCharacterEncoding方法會返回null册烈。
如果客戶端沒有設置字符編碼戈泼,并且請求數(shù)據(jù)使用了不同編碼而不是上述的默認編碼,程序?qū)霈F(xiàn)中斷赏僧。為了糾正這種狀態(tài)大猛,一個新的方法setCharacterEncoding(String enc) 被添加到ServletRequest接口。開發(fā)者調(diào)用這個方法能重寫容器提供的字符編碼淀零。這個方法必須在解析request中任何post數(shù)據(jù)或者讀任何輸入之前調(diào)用挽绩。一旦數(shù)據(jù)已經(jīng)被讀取,調(diào)用這個方法不會影響它的編碼驾中。
另外一種相同的解釋:
四琼牧、3種解決方法
- 在web.xml中配置編解碼Filter
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
<async-supported>true</async-supported>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
關(guān)于這段配置需要強調(diào)兩點:
- web.xml中,這段配置要放在所有filter的最前面哀卫,否則會不生效巨坊,根本原因請見上述第三點的解釋。
- 兩個初始化參數(shù)的作用此改,其實看這個Filter的源碼就一目了然趾撵,這兩個參數(shù)是用來決定是否要設置request和response中的編碼。源碼很簡潔:
public class CharacterEncodingFilter extends OncePerRequestFilter {
private String encoding;
private boolean forceEncoding = false;
public CharacterEncodingFilter() { }
public void setEncoding(String encoding) {
this.encoding = encoding;
}
public void setForceEncoding(boolean forceEncoding) {
this.forceEncoding = forceEncoding;
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if(this.encoding != null && (this.forceEncoding || request.getCharacterEncoding() == null)) {
request.setCharacterEncoding(this.encoding);
if(this.forceEncoding) {
response.setCharacterEncoding(this.encoding);
}
}
filterChain.doFilter(request, response);
}
}
- 設置Content-Type
如果post請求方式是x-www-form-urlencoded共啃,那么設置如下:
Content-Type=application/x-www-form-urlencoded;charset=utf-8
這樣通過request對象取body體里面的中文是正常的占调。
這種方式有一點需要注意: 如果請求方式是multipart/form-data,如上設置會導致request取不到參數(shù)移剪。Content-Type要與傳遞數(shù)據(jù)匹配(本文data) - 手動編解碼
比如參數(shù)title="測試"究珊,這樣取出來就是"測試"。
String str = new String(request.getParameter("title").getBytes("iso-8859-1"), "utf-8");
綜上所有纵苛,最優(yōu)雅的方式是第一種解決方案--通過框架的Filter去處理剿涮。
你僅專注于業(yè)務代碼就好言津。
參考資料
- ajax post data獲取不到數(shù)據(jù)
- Servlet 3.0規(guī)范
- HTTP Content-Type常用對照表
- Spring官網(wǎng)--Consumable Media Types章節(jié)
- ISO-8859-1
- ISO-8859-1為何能顯示中文
- 字符編碼
- Media Type