1. 程式人生 > >SpringMVC之StringHttpMessageConverter引出的客戶端伺服器端之間的亂碼過程分析

SpringMVC之StringHttpMessageConverter引出的客戶端伺服器端之間的亂碼過程分析

繼續上一篇文章遺留的亂碼問題,引出從客戶端資料到伺服器端的亂碼和伺服器端資料到客戶端的亂碼。 

先說明下配置: 
web.xml,還是最簡單的配置
 
Java程式碼  收藏程式碼
  1. <!DOCTYPE web-app PUBLIC  
  2.  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"  
  3.  "http://java.sun.com/dtd/web-app_2_3.dtd" >  
  4. <web-app>  
  5.   <display-name>Archetype Created Web Application</display-name>  
  6.   <servlet>  
  7.         <servlet-name>mvc</servlet-name>  
  8.         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
  9.         <load-on-startup>1</load-on-startup>  
  10.     </servlet>  
  11.     <servlet-mapping>  
  12.         <servlet-name>mvc</servlet-name>  
  13.         <url-pattern>/*</url-pattern>  
  14.     </servlet-mapping>  
  15. </web-app>  

mvc-servlet.xml配置: 
Java程式碼  收藏程式碼
  1. <?xml version="1.0" encoding="UTF-8" ?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
     xmlns:util="http://www.springframework.org/schema/util" xmlns:context="http://www.springframework.org/schema/context"  
  3.     xsi:schemaLocation="http://www.springframework.org/schema/beans  
  4.     http://www.springframework.org/schema/beans/spring-beans-3.1.xsd  
  5.     http://www.springframework.org/schema/mvc  
  6.     http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd  
  7.     http://www.springframework.org/schema/util  
  8.     http://www.springframework.org/schema/util/spring-util-2.0.xsd  
  9.     http://www.springframework.org/schema/context   
  10.     http://www.springframework.org/schema/context/spring-context-3.2.xsd">  
  11.     <mvc:annotation-driven/>  
  12.     <bean class="com.lg.mvc.StringAction"/>  
  13.     <bean name="/index" class="com.lg.mvc.HomeAction"></bean>  
  14.     <bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">  
  15.         <property name="templateLoaderPath" value="/WEB-INF/views" />  
  16.         <property name="defaultEncoding" value="utf-8" />  
  17.         <property name="freemarkerSettings">  
  18.             <props>  
  19.                 <prop key="locale">zh_CN</prop>  
  20.             </props>  
  21.         </property>  
  22.     </bean>  
  23.     <bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver">  
  24.         <property name="suffix" value=".html" />  
  25.         <property name="contentType" value="text/html;charset=utf-8" />  
  26.         <property name="requestContextAttribute" value="request" />  
  27.         <property name="exposeRequestAttributes" value="true" />  
  28.         <property name="exposeSessionAttributes" value="true" />  
  29.     </bean>  
  30. </beans>  

先說說伺服器端資料到客戶端的亂碼: 
第一種情況:
 
Java程式碼  收藏程式碼
  1. @Controller  
  2. public class StringAction {  
  3.     @ResponseBody  
  4.     @RequestMapping(value="/string",method=RequestMethod.GET)  
  5.     public String testMessageConverter(String name){  
  6.         return "中國";  
  7.     }  
  8. }  

當訪問 http://localhost:8080/string?name=aaa時,瀏覽器看到的是亂碼: 

 

分析過程: 
有了上一篇文章的知識,便可以知道原因。首先由RequestMappingHandlerAdapter來排程執行,由於是@ResponseBody,所以從所有的已註冊的HandlerMethodReturnValueHandler中找到了@ResponseBody的支持者RequestResponseBodyMethodProcess。然後就是根據客戶端Accept欄位指定的多個content-type和伺服器端指定的content-type進行比較配對,選出最合適的一個content-type。此時@RequestMapping中並沒有為produces指定相應的content-type,所以會獲取所有的已註冊的HttpMessageConverter所支援的content-type作為伺服器端指定的content-type。在本工程中最終會選出text/html作為最終的content-type,伺服器端資料要以text/html形式寫入response的body中。有了返回值的型別為String和content-type為text/html,然後就是從已註冊的HttpMessageConverter中找到一個支援這兩者的HttpMessageConverter,然後就找到了StringHttpMessageConverter,它有兩個建構函式,一個可以指定字符集,當你什麼都沒有指定時,預設使用ISO-8859-1。在將返回值"中國"以text/html形式寫入response的body中時,StringHttpMessageConverter先從上述所選出的content-type(即text/html)中嘗試獲取字符集,若獲取不到,則使用自己預設的ISO-8859-1,最終的寫入程式碼為:StreamUtils.copy(s, charset, outputMessage.getBody());  
s就是返回值"中國",charset就為StringHttpMessageConverter預設的ISO-8859-1,造成了編碼方式不對,同時ISO-8859-1是不支援中文的,所以就出現了亂碼。對以上過程還不清楚的,可以看上一篇文章的介紹。 

在整個伺服器端資料返回到瀏覽器的過程中,涉及到三次編碼。 

第一次:java檔案以什麼編碼存放在硬碟中,目前我的工程全部使用UTF-8編碼方式,所以程式中的中國是以UTF-8形式編碼的 

第二次:中國這個字串是以什麼編碼方式轉換成位元組陣列的,由於未指定@RequestMapping的produces屬性,同時也未給StringHttpMessageConverter指定編碼方式,最終‘中國’這個 
字串是以ISO-8859-1形式轉換成位元組陣列的 

第三次:資料傳送給瀏覽器後,瀏覽器接收到一堆位元組陣列,瀏覽器又是以什麼編碼方式來解碼的。 

這樣才能保證不會亂碼,首先java檔案是以UTF-8形式儲存的,然後指定StringHttpMessageConverter或者@RequestMapping的produces的編碼方式為UTF-8,最後發給瀏覽器的header中的content-type也為UTF-8,這樣才不會亂碼。 

針對本工程: 
解決方案一: 
指定@RequestMapping的produces為"text/html;charset=UTF-8"即可解決亂碼。 
首先"中國"是以UTF-8編碼的方式存在硬碟中,即硬碟中儲存的是'-28 -72 -83;-27 -101 -67',然後又指定了response的content-type為"text/html;charset=UTF-8",此時StringHttpMessageConverter可以從這個content-type讀取到編碼方式,便不再採用預設的編碼方式ISO-8859-1。執行"中國".getBytes("UTF-8")(即為上述所寫的位元組陣列)將這些位元組陣列寫人response的body中,同時設定response的content-type為produces的值即text/html;charset=UTF-8,瀏覽器拿到這個content-type便知道以UTF-8形式來解碼這些位元組陣列,便又得到的'中國'。你也可以設定瀏覽器以GBK編碼方式來解碼這些位元組陣列,必然又會出現亂碼。所以上述三個過程的編碼都統一才會保證不會亂碼。也就是你可以全部指定上述三個過程的編碼全是GBK,仍然不會亂碼。出現亂碼必然是上述三個過程的編碼不一致造成的。 

解決方案二: 
指定StringHttpMessageConverter的編碼方式為UTF-8,如下:
 
Java程式碼  收藏程式碼
  1. <mvc:annotation-driven>  
  2.         <mvc:message-converters>  
  3.             <bean class="org.springframework.http.converter.StringHttpMessageConverter">  
  4.                 <constructor-arg value="UTF-8"/>  
  5.             </bean>  
  6.         </mvc:message-converters>  
  7.     </mvc:annotation-driven>  

它背後的內容先暫不解釋,下一篇文章再介紹。這裡只是在StringHttpMessageConverter構造時,傳入一個UTF-8的字符集進去,會呼叫如下建構函式: 
Java程式碼  收藏程式碼
  1. /** 
  2.      * A constructor accepting a default charset to use if the requested content 
  3.      * type does not specify one. 
  4.      */  
  5.     public StringHttpMessageConverter(Charset defaultCharset) {  
  6.         super(new MediaType("text""plain", defaultCharset), MediaType.ALL);  
  7.         this.defaultCharset = defaultCharset;  
  8.         this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values());  
  9.     }  

這樣就更該了StringHttpMessageConverter的預設字符集編碼為UTF-8。但是這樣做有一個問題就是並沒有為content-type的字符集設定為UTF-8。看如下程式碼: 
Java程式碼  收藏程式碼
  1. /** 
  2.      * This implementation delegates to {@link #getDefaultContentType(Object)} if a content 
  3.      * type was not provided, calls {@link #getContentLength}, and sets the corresponding headers 
  4.      * on the output message. It then calls {@link #writeInternal}. 
  5.      */  
  6.     @Override  
  7.     public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)  
  8.             throws IOException, HttpMessageNotWritableException {  
  9.         final HttpHeaders headers = outputMessage.getHeaders();  
  10.         if (headers.getContentType() == null) {  
  11.             MediaType contentTypeToUse = contentType;  
  12.             if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) {  
  13.                 contentTypeToUse = getDefaultContentType(t);  
  14.             }  
  15.             if (contentTypeToUse != null) {  
  16.                 headers.setContentType(contentTypeToUse);  
  17.             }  
  18.         }  
  19.         if (headers.getContentLength() == -1) {  
  20.             Long contentLength = getContentLength(t, headers.getContentType());  
  21.             if (contentLength != null) {  
  22.                 headers.setContentLength(contentLength);  
  23.             }  
  24.         }  
  25.         if (outputMessage instanceof StreamingHttpOutputMessage) {  
  26.             StreamingHttpOutputMessage streamingOutputMessage =  
  27.                     (StreamingHttpOutputMessage) outputMessage;  
  28.             streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {  
  29.                 @Override  
  30.                 public void writeTo(final OutputStream outputStream) throws IOException {  
  31.                     writeInternal(t, new HttpOutputMessage() {  
  32.                         @Override  
  33.                         public OutputStream getBody() throws IOException {  
  34.                             return outputStream;  
  35.                         }  
  36.                         @Override  
  37.                         public HttpHeaders getHeaders() {  
  38.                             return headers;  
  39.                         }  
  40.                     });  
  41.                 }  
  42.             });  
  43.         }  
  44.         else {  
  45.             writeInternal(t, outputMessage);  
  46.             outputMessage.getBody().flush();  
  47.         }  
  48.     }  

關鍵是執行順序,先是根據request的Accept指定的content-type和@RequestMapping的produces指定的content-type,或者是所有的HttpMessageConverter所支援的content-type選出一個最合適的content-type,最終選出為text/html,然後將它作為contentType引數傳入上面的方法中,接下來就在設定header的content-type,根據程式碼最終會設定content-type為text/html但是不含字符集編碼,然後才是呼叫StringHttpMessageConverter的寫入方法,將中國以StringHttpMessageConverter的編碼集UTF-8轉換成位元組陣列寫入resposne的body中。 
此時,返回給瀏覽器的content-type欄位並沒有指定編碼集,它將以它預設的方式來解碼。 
如下content-type並沒有編碼方式,而方案一的content-type是有編碼方式的
 




如果瀏覽器的預設編碼為UTF-8則不會顯示亂碼,如果為GBK則會顯示亂碼。可以用chrome瀏覽器進行測試: 
設定chrome瀏覽器的預設編碼方式如下: 
工具-》設定-》高階設定-》自定義字型
 

 

 
至此就說完了伺服器端傳送資料到瀏覽器這一過程中的亂碼問題。然後接下來就要說瀏覽器客戶端傳資料到伺服器端顯示過程中的亂碼問題。 

StringAction新加一個方法如下:
 
Java程式碼  收藏程式碼
  1.        @ResponseBody  
  2. @RequestMapping(value="/test",method=RequestMethod.GET)  
  3. public String testClient(String name){  
  4.     System.out.println(name);  
  5.     return "abc";  
  6. }  

此時先不用管伺服器端返回給瀏覽器的亂碼問題,只關注瀏覽器端傳送給伺服器端的資料,在伺服器端是否能打印出正常資料。 
訪問http://localhost:8080/test?name=中國,伺服器端的列印情況為:
 


出現了亂碼。 
首先分析下整個過程涉及到幾次編碼: 

第一次:當你輸入http://localhost:8080/test?name=中國的時候,瀏覽器將以什麼樣的編碼方式將中國轉化成位元組陣列,這稱為URL編碼 

第二次:當瀏覽器傳送請求時,伺服器是以請求的content-type來解析請求資料的,當瀏覽器請求沒有指定content-type時,伺服器又是採用什麼樣的編碼來解析的 

亂碼的本質:這兩次編碼方式不一致 

針對第一個過程,當你僅僅在瀏覽器上輸入http://localhost:8080/test?name=中國來訪問時,不同的瀏覽器會採用不同編碼方式來將中國轉換成位元組陣列。比如說chrome瀏覽器始終以UTF-8的編碼形式將中國轉換成位元組陣列。而目前我的IE瀏覽器則是以GBK的編碼方式來轉換的,你可以找一找如何設定瀏覽器的這些行為,本文不再說明。正是由於上述不同瀏覽器的不同處理情況,導致了可能用chrome傳送伺服器端正常,IE傳送則亂碼的現象。 

針對第二個過程:由於我們未指定request的content-type,伺服器來解析這些位元組陣列,它到底採用什麼樣的方式來解析呢,不同的伺服器應該有不同的策略,並且可以進行設定。如Tomcat伺服器,預設採用的是ISO-8859-1,你可以修改Tomcat的conf/server.xml檔案來修改Tomcat的預設編碼解析方式。 

這裡的tomcat版本是7,在tomcat8中已修訂,不存在這個亂碼問題 

情況分析完了,針對我的工程就要解決這一亂碼問題。 
首先我使用chrmoe瀏覽器傳送http://localhost:8080/test?name=中國,它預設以UTF-8形式傳送給伺服器,我的tomcat伺服器沒有更改預設的編碼,即仍是採用ISO-8859-1來解析那些沒有指定content-type的請求。 
中國經過chrome瀏覽器的以UTF-8形式的編碼變為-》%E4%B8%AD%E5%9B%BD,然後此請求沒有指定content-type,所以tomcat將採用ISO-8859-1來解碼,然後肯定就出現了亂碼。 

解決方式一:方法引數name是tomcat用ISO-8859-1解碼出來的,我們需要再把它仍按照ISO-8859-1編碼回去得到瀏覽器傳過來的原始位元組陣列,這些位元組陣列就是chrome以UTF-8形式將中國編碼的,所以我們只需要將這些位元組陣列以UTF-8方式再解碼一次,就可以得到正常的資料了。其實就是撤銷掉tomcat的解碼操作,還原瀏覽器傳過來的原始位元組陣列,然後再按照瀏覽器的編碼方式來解碼這些位元組陣列,程式碼如下:
 


然而這種方式,只能針以UTF-8形式編碼資料的瀏覽器,對於IE仍是亂碼,若將程式碼改為以GBK來編碼原始資料則IE是正常的,chrome則出問題: 
Java程式碼  收藏程式碼
  1. @ResponseBody  
  2.     @RequestMapping(value="/test",method=RequestMethod.GET)  
  3.     public String testClient(String name) throws UnsupportedEncodingException{  
  4.         System.out.println(new String(name.getBytes("ISO-8859-1"),"GBK"));  
  5.         return "abc";  
  6.     }  


解決方式二:就是更改伺服器的預設編碼配置,如tomcat,在conf/server.xml檔案中 
使用URIEncoding='UTF-8'。這個設定是針對url中的請求引數的編碼的就是針對?name='中國'這種引數的編碼
 
Java程式碼  收藏程式碼
  1. <Connector port="8080" protocol="HTTP/1.1"  
  2.               connectionTimeout="20000"  
  3.               redirectPort="8443" URIEncoding='UTF-8'/>  
  4.    <!-- A "Connector" using the shared thread pool-->  

仍是上述問題,對於chrome是正常的,但對IE就亂碼。由於chrome是以UTF-8編碼的,伺服器又是以UTF-8解碼的,所以正常。對於IE,IE是以GBK編碼的,伺服器仍採用UTF-8來解碼肯定出現亂碼。對於chrome如下: 





至此瀏覽器傳送資料到伺服器亂碼,伺服器傳送資料到瀏覽器亂碼的兩個過程的原理都說完了。不知道你是否完全理解了,有沒有信心去幫助別人解決亂碼問題。 

相關推薦

SpringMVCStringHttpMessageConverter引出客戶伺服器之間亂碼過程分析

繼續上一篇文章遺留的亂碼問題,引出從客戶端資料到伺服器端的亂碼和伺服器端資料到客戶端的亂碼。  先說明下配置:  web.xml,還是最簡單的配置  Java程式碼   <!DOCTYPE web-app PUBLIC    "-//Sun Microsyste

python網路程式設計(TCP客戶/伺服器實現)

下面的程式實現的功能:客戶端發來訊息,伺服器端加上時間戳返回給使用者 伺服器端: from socket import * from time import ctime import os p

GCM(谷歌雲推送)客戶伺服器開發全指南(伺服器篇)

由於谷歌雲推送GCM升級成為FCM,所以此部落格能容僅供參考 ————2016.12.2更新 今天我們按照之前所說的步驟介紹GCM雲推送服務端的開發,因為服務端的開發比客戶端的開發較簡單,遵從由易到難,一步一步攻破的原則,所以我先於客戶端講服務端的開發,話不多

GCM(谷歌雲推送)客戶伺服器開發全指南(客戶

由於谷歌雲推送GCM升級成為FCM,所以此部落格能容僅供參考 ————2016.12.2更新 最近因為手頭上的工作所以耽誤了一下指南客戶端篇的編寫,而且客戶端的功能實現是比較複雜的,處理的邏輯也是比較多的,所以也花了點時間去研究了一下。 沒有看我之前兩篇部落

Java6學習筆記64——UDP客戶/伺服器

客戶端: import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.SocketException; import

客戶伺服器資料同步策略一

1、場景:客戶端需要向伺服器端非同步獲取資料(不是實時呼叫介面獲取資料),來更新客戶端本地資料庫資料。 2、策略:客戶端介面引數新增:lastModifiedTime,初始化介面表中lastModifiedTime值可為:19700101000000(yyyyMMddHHm

從0開始編寫一個應用(android+小程式+伺服器)第二步 專案經理完成邏輯圖。(上:產品經理的思考)

專案經理跟客戶收到基本需求後,要完善客戶的需求。因為絕大數客戶對於網際網路或者軟體不是很懂,只能提出他們想要的東西,即專案必須要實現的功能。但是沒有一個完整的專案流程,這需要專案經理去完善,細化,改善功能。 下面開始記錄專案經理工作。 上一篇客戶說到他的專案構思

從0開始編寫一個應用(android+小程式+伺服器)第二步 專案經理完成邏輯圖。(下:產品細節思考後的實現邏輯圖)

上一篇說到產品經理思考完成大概邏輯圖: 使用者釋出拼團資訊可以選擇金額釋出,非金錢釋出。並且可以分享。然後,其他使用者點選獲得獎勵,或者分享獲得獎勵。 獎勵的有錢,有釋出機會。這個就是專案的主要流程,但是專案只有這個太少了,拼團還有一個玩法就是 拼團抽獎。 所以要加上這

android上傳檔案至伺服器(android+伺服器

引言:本來android檔案上傳的部落格在網上挺多的,不過好些都只是有前臺android端的上傳,並沒有後臺伺服器端的接收。而且自己寫的時候也確實遇見了一些之前沒注意到的地方,寫出來也算是給自己提個醒。 我這裡就不把全部的程式碼都貼出來了,就只貼一下核心程式碼

(九)springmvcjson的數據請求(客戶發送json數據到服務

index.jsp null 字符串 n-2 func mda 客戶 請求 spring index.jsp <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncodi

C#程式設計 socket程式設計tcp伺服器客戶

基於Tcp協議的Socket通訊類似於B/S架構,面向連線,但不同的是伺服器端可以向客戶端主動推送訊息。 使用Tcp協議通訊需要具備以下幾個條件: (1).建立一個套接字(Socket) (2).繫結伺服器端IP地址及埠號--伺服器端 (3).利用Listen()方法開啟監聽--伺服

C#程式設計 socket程式設計udp伺服器客戶

基於Udp協議是無連線模式通訊,佔用資源少,響應速度快,延時低。至於可靠性,可通過應用層的控制來滿足。(不可靠連線) 使用Udp協議通訊需要具備以下幾個條件: (1).建立一個套接字(Socket) (2).繫結伺服器端IP地址及埠號--伺服器端 (3).通過SendTo()方法向指

libevent學習三:簡單的伺服器客戶

1.伺服器#include <stdio.h> #include <time.h> #include <event2/bufferevent.h> #include <event2/buffer.h> #include <

Android客戶+mysql+springmvc伺服器實現登陸的小案例

首先是客戶端 通過輸入使用者名稱+密碼實現登入 點選登入後向伺服器傳送http請求 伺服器收到請求後驗證使用者名稱密碼是否與mysql資料庫上的相應欄位是否一致 然後返回json資料 客戶端獲取響應的結果 然後提醒是否登入成功 MainActivity程式碼: public

極光推送伺服器向android等客戶推送例項

原文:http://blog.csdn.net/u014733374/article/details/43560983 找了兩天,總算找到一個靠譜的了。開始測試的時候總是報各種錯誤,尤其是不能找到audience的錯誤,偶然想到,用極光開放平臺整合的客戶

ZookeeperZookeeper底層客戶架構實現原理(轉載)

一次 描述 綁定 機制 一個 ini fin 源碼 receive Zookeeper的Client直接與用戶打交道,是我們使用Zookeeper的interface。了解ZK Client的結構和工作原理有利於我們合理的使用ZK,並能在使用中更早的發現問題。本文將在研究源

搭建backup服務器rsyncdaemon服務模式二rsync客戶配置

rsync1.檢查客戶端是否有rsync服務:[[email protected]/* */ ~]# rsync --versionrsync version 3.0.6 protocol version 30Copyright (C) 1996-2009 by Andrew Tridgell

python模仿ssh客戶

utf local 循環 imp true print 數據 code enc import socketclient = socket.socket()        #客戶端實例化client.connect((‘localhost‘,9999))    #與服務器建立

基於windows的簡單伺服器客戶

伺服器端套接字建立步驟:1.呼叫socket函式建立套接字。2.呼叫bind函式分配IP地址和埠號。3.呼叫listen函式轉為可接收請求狀態。4.呼叫該accept函式受理連線請求。 客戶端套接字建立步驟:1.呼叫socket函式建立套接字。2.呼叫connect函式向伺服器端傳送連線請求。  

DataTables的伺服器SpringMVC)分頁模式

Datatables是一款jquery表格外掛。它是一個高度靈活的工具,可以將任何HTML表格新增高階的互動功能。 分頁,即時搜尋和排序 幾乎支援任何資料來源:DOM, javascript, Ajax 和 伺服器處理 支援不同主題 DataTables, jQuery UI, Bo