1. 问题
今天为storm程序添加了一个计算bolt,上线后正常,结果发现之前的另一个bolt在将中文插入到hbase中后查询出来乱码。其中字符串是以UTF-8编码的url加密串,然后我使用的URLDecoder.decode(str, "UTF-8")解码,最后插入到hbase中。
2. 排查
(1)hbase中的数据传输都是使用的UTF-8,因此肯定不会出问题,故排除hbase端的问题;
(2)既然在测试的时候没乱码,线上却乱码,想到肯定是线上机子jvm环境的问题;
(3)确定了是jvm环境的问题,再一想URLDecoder.decode(str, "UTF-8")这句解码肯定用的是UTF-8,如果str编码是UTF-8的解出来当然就不会乱了,于是明确str在jvm中被用非UTF-8编码了;
(4)排查线上那台机子的jvm默认编码
首先,打印了 echo $LANG、echo $LC_ALL 等linux系统变量,发现都是一致的UTF-8,排除了 os 环境的问题。
然后,重点放在了 java 环境上,使用System.getProperty(
"file.encoding"
);打印jvm的默认编码,结果出来的是:ISO-8859-1。
到这里我们可以知道原因了:由于线上那台机子的 jvm 参数(file.encoding)不一致导致了中文的乱码。
3. 解决方案
知道原因了,解决起来就简单了,目标就是改变JVM file.encoding参数的值。
由于这个参数是jvm的启动参数,运行时不可更改(你可以理解为这个参数是个全局参数,而且被缓存了,如果一旦运行时更改了, 可能会造成整个 jvm 里面的程序奔溃)。
(1)临时方案
jvm的启动参数里加上-Dfile.encoding="UTF-8"来指定。
(2)一劳永逸方案
修改系统的charset,linux的字符集在/etc/sysconfig/i18n文件中设置,下面是我的机子默认设置:
LANG="en_US" SYSFONT="latarcyrheb-sun16"
有两种修改方式:1.将/etc/sysconfig/i18n中的LANG修改为LANG="en_US.UTF-8",修改后需要重启机子才能生效;2.在/etc/profile中添加export LANG=en_US.UTF-8,然后source /etc/profile即可生效。
4. 疑问
为何之前这个bolt一直正常?因为storm对bolt的分配是自己控制的(对用户而言相当于随机分配到不同的节点),之前这个bolt分配到的那个机子的jvm编码设置的为en_US.UTF-8,故不会出现问题。
5. 深入理解 jvm 的 -Dfile.encoding 参数
上面说了这么多,可能有同学还是不大明白:jvm 的这参数有啥用啊?为啥之前都没听过这玩意呢?恩,没听过正常,之前我也没听过哈~
(1) JVM编码原理
jvm内部的(字节码)编码方式为unicode,编码和解码过程为:(1)编码:首先将字符串使用jvm默认的编码方式(也可以手动指定)转换为unicode存储到内存中;(2)解码:然后就unicode编码的字符串解码为用户指定的编码字符串。因此,只要保证编码和解码两端的字符集编码方式一致就不会出现乱码。
(2)查询源码
在JDK 1.6.0_20的src.zip文件中,查找包含file.encoding字眼的文件,共找到4个:
(a)先上重头戏 java.nio.Charset类:
public static Charset defaultCharset() { if (defaultCharset == null) { synchronized (Charset.class) { java.security.PrivilegedAction pa = new GetPropertyAction("file.encoding"); String csn = (String) AccessController.doPrivileged(pa); Charset cs = lookup(csn); if (cs != null) defaultCharset = cs; else defaultCharset = forName("UTF-8"); } } return defaultCharset; }
在java中,如果没有指定charset的时候,比如new String(byte[] bytes), 都会调用Charset.defaultCharset()的方法,我们可以清楚的看到defaultCharset是只能被初始化一次,这里还是有点小问题的,在多线程并发调用的时候还是会初始话多次,当然后面都是从cache(lookup的函数)里读出来的,问题也不大。
当我们在改变System.getProperties里的file.encoding 的时候,defaultCharset已经被初始化过了,所以不会在调用初始化的代码。
当jvm 启动的时候,load class, 最后调用main函数之前,defaultCharset已经初始化好,而很多函数里都掉用了这个方法象String.getBytes, 还有 InputStreamReader, InputStreamWriter 都是调用了 Charset.defaultCharset()的方法。
(b)java.net.URLEncoder的静态方法, 影响到的方法 java.net.URLEncoder.encode(String)
恩,这里也需要注意,之前已经有同学掉坑里去了,请使用:encode(String s, String enc) 方法
(c)com.sun.org.apache.xml.internal.serializer.Encoding的getMimeEncoding方法(209行起)
(d)最后一个javax.print.DocFlavor类的静态构造方法
可以看到,系统变量file.encoding影响到
1. Charset.defaultCharset() Java环境中最关键的编码设置
2. URLEncoder.encode(String) Web环境中最常遇到的编码使用
3. com.sun.org.apache.xml.internal.serializer.Encoding 影响对无编码设置的xml文件的读取
4. javax.print.DocFlavor 影响打印的编码
(3)Java's file.encoding property on Windows platform
This property is used for the default encoding in Java, all readers and writers would default to use this property. “file.encoding” is set to the default locale of Windows operationg system since Java 1.4.2. System.getProperty(“file.encoding”) can be used to access this property. Code such as System.setProperty(“file.encoding”, “UTF-8”) can be used to change this property. However, the default encoding can not be changed dynamically even this property can be changed. So the conclusion is that the default encoding can’t be changed after JVM starts. “java -Dfile.encoding=UTF-8” can be used to set the default encoding when starting a JVM. I have searched for this option Java official documentation. But I can’t find it.
5. 参考文章
(1)系统变量file.encoding对Java的运行影响有多大
http://www.blogjava.net/ivanwan/archive/2011/01/31/343810.html
(2)linux查看系统和修改系统编码
http://www.poluoluo.com/server/201401/258604.html
(3)en_US.UTF-8和zh_CN.UTF-8的区别
http://www.iteye.com/problems/90396