昨天项目遇到了乱码问题,在自己电脑上是没问题的然而在服务器上就出现的乱码的问题,所以就以乱码为点来仔细的了解一下Java中的乱码。
先简单了解一下java的字符串的编码到存储的过程:
java在字符串中统一用Unicode表示。
对于任意一个字符串:String string = “测试字符串”;
如果源文件是GBK编码,操作系统默认环境编码也为GBK,那么编译的时候,JVM将按照GBK编码将字节数组解析为字符,然后将字符转换为Unicode格式的字节数组,作为内部存储(字节数组→字符→Unicode字节数组)。当打印这个字符串时,JVM根据操作系统本地的语言环境,将Unicode转换为GBK,然后操作系统将GBK格式的内容显示出来。
当源码文件是UTF-8, 我们需要通知编译器源码的格式,javac -encoding utf-8 … , 编译时,JVM按照utf-8 解析成字符,然后转换为unicode格式的字节数组, 那么不论源码文件是什么格式,同样的字符串,最后得到的unicode字节数组是完全一致的,显示的时候,也是转成GBK来显示(跟OS环境有关)
一、Java常见的编码方式
- ASCII码:众所周知,这是最简单的编码。它总共可以表示128个字符,0~31是控制字符如换行、回车、删除等,32~126是打印字符,可以通过键盘输入并且能够显示出来的。
- ISO-8859-1:它是基于ASCII码基础上扩展的,它总共能表示256个字符,涵盖了大多数西欧语言字符。详见 ISO-8859-1 编码 该编码不支持中文,举个中文编码栗子 :字符串“I am 君山”用 ISO-8859-1 编码,下面是编码结果
由于ISO-8859-1 是单字节编码且不支持中文,直接将中文字符转成‘3f’, 3f也就是常见的"?"字符
- GB2312: 它是双字节编码,共包含6763个汉字。
- GBK:汉字内码扩展规范,是基于GB2312上拓展的,加入了更多的汉字,能表示21003个汉字。它的编码是和GB2312兼容的。也就是说用GB2312编码的汉字可以用GBK来解码,并且不会乱码。倒过来就不完全可以了,因为GB2312描述的汉字比GBK少。
- UTF-16:UTF-16是基于Unicode上定义的, 用两个字节来表示Unicode的转换格式,它采用定长的表示方法,即不能什么字符都可以用两个字节表示。两个字节是16个bit,所以就做UTF-16。(Unicode 囊括了世界上所有语言,所有语言均可通过Unicode来相互翻译,详解 Unicode 编码)
- UTF-8:由于UTF-16统一采用两个字节来表示一个字符, 有很多字符用一个字节表示即可。所以存储空间放大了一倍,还会增加网络传输的流量,所以推出了UTF-8。 UTF-8采用了一种变长技术,每个编码区域有不同的字码长度。
二、常见乱码问题分析
乱码指的是程序显示出来的字符文本无法用任何语言去解读。一般情况下会包含大量的?。本质上都是由于字符串原本的编码格式与读取时解析用的编码格式不一致导致的。
- 中文变成看不懂的字符:如果一串中文字符变成了一串看不懂的字符如:"Ì Ô £ ¡Î Ò Ï²»¶ £ ¡",这种情况通常是编码字符集与解码时所用的字符集不一致所造成的。比如使用GBK编码,如果使用ISO-8859-1解码的话结果就是这样。
- 一个汉字变成了一个问号:如果编码和解码的字符集都是一致的,那么可以确定该字符编码不支持中文,例如:ISO-8859-1
- 一个汉字变成了两个问号:中文经过多次编码且其中有一次编码或者解码使用了不支持中文的字符集
三、常见案例分析
参数传输乱码
背景:从jsp中传参数(包括中文)请求后台数据,在后台获取到的请求参数乱码。
1、前端编码设置,先讲解下jsp中编码的配置:
a、其中contentType中charset用来设置服务器发送给客户端时的内容编码;pageEncoding 用来设置JSP源文件本身和响应正文中的字符编码。通俗的说pageEncoding是jsp文件本身的编码,如果pageEncoding设置为ISO-8859-1,则jsp页面中不能保存中文字符,会自动提示你是否要设置为UTF-8.
b、jsp文件编码字符集默认为ISO-8859-1, JSP源文件字符集时,优先级为pageEncoding>contentType。如果都没有设置,默认ISO-8859-1。
c、设置响应输出的字符集时,优先级为contentType>pageEncoding。如果都没有设置,默认ISO-8859-1。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
综上所述,解决该问题乱码的第一步要设置jsp中的编码,最好统一为UTF-8。
exmaple(乱码示例):
页面效果如下:
2、后端编码设置
首先要设置tomcat编码,其中要了解两个参数(conf/server.xml):URIEncoding和useBodyEncodingForURI,可以查看官方文档说明http://tomcat.apache.org/tomcat-7.0-doc/config/http.html, 以下是我理解:
- URIEncoding是对所有GET方式的请求的数据进行统一的重新编码,默认编码 ISO-8859-1(针对URI上的请求参数)
- useBodyEncodingForURI:此设置仅适用于请求的查询字符串(针对请求体中内容)。 与URIEncoding不同,它不影响请求URI的路径部分。如果不知道请求字符编码(浏览器不提供,并且SetCharacterEncodingFilter不设置或使用Request.setCharacterEncoding方法的类似过滤器),默认编码始终为“ISO-8859-1”。URIEncoding设置对此默认值没有影响。该参数为false。通俗的说:true表示get和post的编码保持一致,post方式的编码是什么,get方式的编码就是什么。false表示get和post的字符编码各自设置,互相没有关系。
- example1(只设置URIEncoding):
server.xml:
<Connector connectionTimeout="20000" port="9080" protocol="HTTP/1.1"
redirectPort="443" URIEncoding="UTF-8" />
controller:
@RequestMapping(value = "/testURI", method=RequestMethod.POST)
@ResponseBody
public String testURI(HttpServletRequest request){
String username = request.getParameter("username");
String nickname = request.getParameter("nickname");
System.out.println("姓名:" + username + ", 昵称:" + nickname);
return "姓名:" + username + ", 性别:" + nickname;
}
jsp:
<form action="${pageContext.request.contextPath }/testURI.html?username=张三" method="post">
<input type="text" name="nickname" value="老张三"/>
<input type="submit" value="提交"/>
</form>
输出结果: 姓名:张三, 昵称:èå¼ ä¸
从结果中可以看出, URIEncoding只对URI中的参数进行编码。
- example2:只修改controller中代码,就都会显示正常
@RequestMapping(value = "/testURI", method=RequestMethod.POST)
@ResponseBody
public String testURI(HttpServletRequest request) throws UnsupportedEncodingException{
request.setCharacterEncoding("UTF-8");
String username = request.getParameter("username");
String nickname = request.getParameter("nickname");
System.out.println("姓名:" + username + ", 昵称:" + nickname);
return "姓名:" + username + ", 性别:" + nickname;
}
其实第二种做法并不是很方便,一般通过设置URIEncoding+encodingFilter即可解决。
- example3(通常做法):
web.xml代码如下,其余跟example1一样即可。
<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>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- example4:
@RequestMapping(value = "/testURI", method=RequestMethod.POST)
@ResponseBody
public String testURI(HttpServletRequest request) throws UnsupportedEncodingException{
request.setCharacterEncoding("UTF-8");
String username = request.getParameter("username");
String nickname = request.getParameter("nickname");
System.out.println("姓名:" + username + ", 昵称:" + nickname);
return "姓名:" + username + ", 性别:" + nickname;
}
如果只设置URIEncoding=ISO-8859-1,request.setCharacterEncoding("UTF-8");只会对请求体中的参数进行编码,所以username是乱码的。
- example5: 在example4的基础上设置useBodyEncodingForURI="true",设置useBodyEncodingForURI=true时,就会将请求参数和请求体中的参数根据request.setCharacterEncoding或者contentType中的字符集编码。
- request乱码:
- POST请求:request.setCharacterEncoding();--->只对post请求有效。
- GET请求:
问题本质是get方式传递的参数内容默认编码方式问ISO8859-1,而且使用request.setCharacterEncoding("utf-8")也无法解决问题
法一:要解决这个问题,修改tomcat服务器的配置文件。修改tomcat目录下的conf/server.xml文件也就是上面example1所说的设置URIEncoding
法二:下文的 new String(s.getBytes(“iso-8859-1”) ,”UTF-8”);
- response乱码:
法一:response.setCharacterEncoding("UTF-8"); 目的是用于response.getWriter()输出的字符流的乱码问题。如果是response.getOutputStream()是不需要此种解决方案的,因为这句话的意思是为了将response对象中的数据以UTF-8解码后的字节流发向浏览器;
法二:response.setContentType("text/html;charset=utf-8"); 目的是为了控制浏览器的行为,即控制浏览器用UTF-8进行解码;
response.setHeader("content-type","text/html;charset=UTF-8");告诉浏览器用utf-8解析(setHeader是HttpServletResponse的方法。如果想在拦截器Filter中设置字符编码,则无此方法,因为Filter的doFilter方法的参数类型是ServletResponse)
四、终极武器即通过平台默认字符集进行编码getBytes()
new String(s.getBytes(“iso-8859-1”) ,”UTF-8”);
new String(s.getBytes(“iso-8859-1”) ,”GBK”)
- GBK<----->UTF-8
先来看几个例子:
例如:String str = “java乱码问题”;
//错误,因为getBytes()默认使用GBK编码, 而解析时使用UTF-8编码,肯定出错。
System.out.println( new String(str.getBytes(),"UTF-8"));
//错误,因为getBytes 的编码与 UTF-8 不一致,肯定是乱码
System.out.println( new String( s.getBytes("GBK") , "UTF-8));
//结果都是正确的,因为它们的源内容编码和解析用的编码是一致
System.out.println( new String(s.getBytes(),"GBK"));
System.out.println( new String(s.getBytes("UTF-8"),"UTF-8"));
其中getBytes()是将Unicode转换为操作系统默认格式的字节数组,即“java乱码问题”的GBK格式,new String (bytes, Charset) 中的charset 是指定读取byte的方式,这里指定为UTF-8,即把bytes的内容当做UTF-8来读取。
原理:利用getBytes将unicode字符串转成UTF-8格式的字节数组,然后用utf-8 对这个字节数组解码成新的字符串,转GBK也是 同理。
//GBK转UTF-8
new String( s.getBytes("utf-8") , "utf-8");
UTF-8 转GBK
new String( s.getBytes("GBK") , "GBK");
其实核心工作都由getBytes(charset)做了。getBytes的JDK描述:Encoding this String into a sequence of bytes using the named charset,storing the result into a new byte array.
OutputStreamWriter w1 = new OutputStreamWriter(new FileOutputStream("D:\\file1.txt"),"UTF-8");
InputStreamReader( stream, charset)
可以帮助我们轻松的按照指定编码读写文件。
- 为什么在tomcat 下使用new String(s.getBytes(“iso-8859-1”) ,”GBK”)可以呢?
tomcat 默认使用iso-8859-1编码, 也就是说,如果原本字符串是GBK的,tomcat传输过程中,将GBK转成iso-8859-1了,默认情况下,使用iso-8859-1读取中文肯定是有问题的,那么我们需要将iso-8859-1 再转成GBK, 而iso-8859-1 是单字节编码的,即他认为一个字节是一个字符, 那么这种转换不会对原来的字节数组做任何改变,因为字节数组本来就是由单个字节组成的,如果之前用GBK编码,那么转成iso-8859-1后编码内容完全没变, 则 s.getBytes(“iso-8859-1”) 实际上还是原来GBK的编码内容则 new String(s.getBytes(“iso-8859-1”) ,”GBK”) 就可以正确解码了。 所以说这是一种巧合。