Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 30021|回复: 0

SpringMVC使用@ResponseBody输出字符串时遇到的乱码问题及解决办法

[复制链接]
  • TA的每日心情
    奋斗
    2024-4-6 11:05
  • 签到天数: 748 天

    [LV.9]以坛为家II

    2034

    主题

    2092

    帖子

    70万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    705612
    发表于 2021-4-6 11:18:05 | 显示全部楼层 |阅读模式
    今番又遇到乱码问题,有时候真觉得英语母语的那些地区确实挺省事的,至少不用为了这个经典麻烦去折腾。 

    网络上讨论乱码问题的文章很多,因为各作者使用的计算机环境的不同,往往不是很全面。 
    这里非常推荐的一篇文章: 
    http://dohkoos.name/java-garbled-analysis.html 

    简而言之,乱码的“根本原因是由于编码和解码采用的不是同一种码”。例如作者所举的例子,使用GBK编码为UTF-8,使用ISO-8859从UTF-8解码,可能会导致乱码问题。这就好比有一篇中文文章想给王五看,不过这篇文章先由张三翻译成为了英文,然后再由李四翻译成俄文(而不是翻译回中文),但是王五只看得懂中文,于是就麻瓜了。 

    我们需要保持编码或者解码两头,所使用的字符集转换方向需要正好相反:使用 GBK --> UTF-8 与 UTF-8 --> GBK。由于Java采用了UTF-8编码,所以编码解码均以UTF-8为中介。 
    对于翻译而言,就是先相当于: 先 中译英,对应的解码,反过来就是 英译中。 

    遇到乱码问题,通常的检查项包括: 
    1. 编辑器保存文件的字符集; 
    2. 数据库的字符集; 
    3. 应用服务器或者Web服务器处理字符串采用的字符集 
    4. JSP对于字符集声明 
    5. Servlet过滤器,以及MVC框架拦截器对于字符集的处理 
    6. 其它涉及字符集处理的环节 

    检查各个环节,统一按UTF-8设置。推断我这次碰到的问题属于上述第6中情况。 

    因为是通过SpringMVC提供的注解@ResponseBody来返回一个JSON字符串,然后在客户端上解析JSON(现如今以JSON作为数据交换格式貌似越来越时髦了,客户端我用的比较多的是jqGrid或者ExtJS)。 

    Controller代码如下: 
    Java代码    收藏代码
    1. @Controller  
    2. @RequestMapping("/*")  
    3. public class HelloController {  
    4.     private transient final Log log = LogFactory.getLog(HelloController.class);  
    5.       
    6.     @Autowired  
    7.     private UserManager mgr = null;  
    8.       
    9.     @RequestMapping(value="hello_list.do", method = RequestMethod.POST)  
    10.     @ResponseBody  
    11.     public String helloList() {  
    12.         StringBuilder str = new StringBuilder("{totalProperty:100,root:[");  
    13.           
    14.         List<User> users = mgr.getUsers();  
    15.         for (User user : users) {  
    16.             str.append("{id: ").append(user.getId());  
    17.             str.append(", name:'").append(user.getLastName());  
    18.             str.append("', descn:'").append(user.getFullName()).append("'},");  
    19.         }  
    20.         str.append("{id:4, name:'생활', descn:'Китай'},");  
    21.         str.append("{id:5, name:'tchen8', descn:'中文'}]}");  
    22.           
    23.         log.info(str.toString());  
    24.           
    25.         return str.toString();  
    26.     }  
    27.   
    28. }  


    在Spring配置文件里,默认如下: 
    Xml代码    收藏代码
    1. <!-- Enables the Spring MVC @Controller programming model -->  
    2. <mvc:annotation-driven />  


    调试程序,控制台输出日志看到是中文,但是在firebug中看到的服务器端送过来的字符串是???? (如果是 "口口口"这样的输出,需要先排除是否为系统的字体缺失),于是判断是服务器最后往端口写字符串流的时侯字符集不对。 

    通过调试跟踪Spring的源码,声明@ResponseBody时,Spring会通过AnnotationMethodHandlerAdapter去寻找对应的HttpMessageConverter, 我们这里声明返回的类型是String,于是对应StringHttpMessageConverter。通过实验,猜测这个StringHttpMessageConverter也就是<mvc:annotation-driven />触发的默认的字符串转换工作类。 

    比较不幸的是,StringHttpMessageConverter所使用的默认字符集是ISO-8859-1 
    Java代码    收藏代码
    1. ......  
    2. public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {  
    3.   
    4.     public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1");  
    5. ......  


    这里不得不提的是与StringHttpMessageConverter 同级的类MappingJacksonHttpMessageConverter,天知道是什么原因:同一个作者,对于这两个类,默认字符集一个是ISO-8859-1,一个是UTF-8。 

    既然事已如此,那就想办法把这个地方用到的ISO-8859-1也改成UTF-8了。有两个思路: 
    1. 替换默认字符集; 
    2. 替换StringHttpMessageConverter 

    搜索了一下,先看到这个解决办法: 
    http://forum.springsource.org/showthread.php?t=81858 
    这里提供的是使用一个所谓的ConfigurableStringHttpMessageConverter来替代StringHttpMessageConverter,基本的思路技术是:由于StringHttpMessageConverter中的默认字符集变量声明为final,无法直接通过继承去覆盖,那就把StringHttpMessageConverter照抄一遍,构造函数中新增一个代表字符集的输入参数,然后在配置文件里面通过构造方法注入UTF-8。在配置文件中,将这个Bean声明在<mvc:annotation-driven />前面,从而能够先于StringHttpMessageConverter被Spring识别和注入。 

    但是这个方法多少有些蛮干的味道,基于它简化的一个版本可以如下,即通过继承StringHttpMessageConverter,然后在子类中注入我们想要的字符集配置: 
    Java代码    收藏代码
    1. public class MyStringHttpMessageConverter extends StringHttpMessageConverter {  
    2.   
    3.     public MyStringHttpMessageConverter(Charset defaultCharset) {  
    4.         List<MediaType> mediaTypeList = new ArrayList<MediaType>();  
    5.         mediaTypeList.add(new MediaType("text", "plain", defaultCharset));  
    6.         mediaTypeList.add(MediaType.ALL);  
    7.         super.setSupportedMediaTypes(mediaTypeList);  
    8.     }  
    9.       
    10. }  


    Bean的配置依然类似: 
    Xml代码    收藏代码
    1. ...  
    2. ...  
    3.     <beans:bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">  
    4.         <beans:property name="messageConverters">  
    5.             <util:list>  
    6.                 <beans:bean id="stringHttpMessageConverter" class="org.tchen8.myapp.common.ConfigurableStringHttpMessageConverter">  
    7.                     <beans:constructor-arg value="UTF-8" />  
    8.                 </beans:bean>  
    9.             </util:list>  
    10.         </beans:property>  
    11.     </beans:bean>  
    12.   
    13.     <!-- Enables the Spring MVC @Controller programming model -->  
    14.     <mvc:annotation-driven />  
    15. ...  
    16. ...  


    上面的办法是以属性注入的方式,替换了默认的字符集,但为此也需要把converter替换。 



    另外一个比较简洁的办法,则不需要自己写converter类,而是直接通过属性注入,修改StringHttpMessageConverter的默认配置。 
    Xml代码    收藏代码
    1. ...  
    2.     <beans:bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">    
    3.         <beans:property name="messageConverters">    
    4.             <util:list>    
    5.                 <beans:bean class="org.springframework.http.converter.StringHttpMessageConverter">    
    6.                     <beans:property name="supportedMediaTypes">    
    7.                         <util:list>  
    8.                             <beans:value>text/html;charset=UTF-8</beans:value>  
    9.                         </util:list>    
    10.                     </beans:property>    
    11.                 </beans:bean>    
    12.             </util:list>    
    13.         </beans:property>    
    14.     </beans:bean>  
    15. ...  

    上面的这个办法,实际上通过setSupportedMediaTypes方法,其实也就是StringHttpMessageConverter在类注释中所提到的办法: 



    如果再多看一下StringHttpMessageConverter的源码,可以到它的父类中AbstractHttpMessageConverter有这么个方法: 
    Java代码    收藏代码
    1. ...  
    2.     /** 
    3.      * Returns the default content type for the given type. Called when {@link #write} 
    4.      * is invoked without a specified content type parameter. 
    5.      * <p>By default, this returns the first element of the 
    6.      * {@link #setSupportedMediaTypes(List) supportedMediaTypes} property, if any. 
    7.      * Can be overridden in subclasses. 
    8.      * @param t the type to return the content type for 
    9.      * @return the content type, or <code>null</code> if not known 
    10.      */  
    11.     protected MediaType getDefaultContentType(T t) {  
    12.         List<MediaType> mediaTypes = getSupportedMediaTypes();  
    13.         return (!mediaTypes.isEmpty() ? mediaTypes.get(0) : null);  
    14.     }  
    15. ...  


    注释中写的明白:"Can be overridden in subclasses." 那就不必客气了。于是我们大概也能有如下的做法: 
    Java代码    收藏代码
    1. ...  
    2. public class MyStringHttpMessageConverter2 extends StringHttpMessageConverter {  
    3.       
    4.     private static final MediaType utf8 = new MediaType("text", "plain", Charset.forName("UTF-8"));   
    5.   
    6.     @Override  
    7.     protected MediaType getDefaultContentType(String dumy) {  
    8.         return utf8;  
    9.     }  
    10.       
    11. }  
    12. ...  

    对应的配置: 
    Xml代码    收藏代码
    1. <beans:bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">  
    2.     <beans:property name="messageConverters">  
    3.         <util:list>  
    4.             <beans:bean id="myStringHttpMessageConverter2" class="org.tchen8.myapp.common.MyStringHttpMessageConverter2" />  
    5.         </util:list>  
    6.     </beans:property>  
    7. </beans:bean>   
    8.   
    9. <!-- Enables the Spring MVC @Controller programming model -->  
    10. <mvc:annotation-driven />  


    以上的几个方法,都能解决@ResponseBody导致的乱码问题,虽然StringHttpMessageConverter将来确实有可能把默认字符集修改成UTF-8,从而导致上述功夫最后变成白忙活。但也确实感谢有这么个小阻碍,迫使自己去分析问题寻找答案。收获不在于结果,而在过程吧  

    最后show一把我的页面: 

     
    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-5-17 17:07 , Processed in 0.079759 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表