一个“”字引发的痛苦经历
(一篇老文章,还有点价值,特意整理了一下。由于涉及客户项目,已经进行了脱敏处理)
1 写在前面的话
虽然这个问题是有解决方案的,但我不建议大家提供给客户,理由见此。
2 问题描述
2010.10.12,业务部门提交问题单(2010.09.14):在对xx铁路局进行“缴费通知单明细信息导入”时,因遇到生僻字“贾”(上龙下天),系统无法识别并报错,使得业务不得不暂停,望给予解决。
3 问题分析
3.1 初步分析
生僻字的乱码问题, 我们认为在生产系统中是不存在的 (除非超出 GBK 字符集的范围),因为系统运行 2 年多来,已经录入了一些不在 GB2312 中的生僻字。
在开发环境中验证,导入存在生僻字的个人基本信息,没有问题;
在 21 测试环境中(AIX+WebLogic+Oracle) ,导入存在生僻字的个人基本信息,有问题,并且在界面上也无法直接录入。
开始在 21 测试环境中分析问题:
1、检查 AIX、ORACLE 等相关字符集设臵,未发现问题;
2、上传文件后,BLOB 字段中的信息正确;
3、修 改 startWeblogic.sh 文 件 , 在 启 动 java 时 , 增 加:
-Dfile.encoding=GBK -Ddefault.client.encoding=GBK,没有效果;
4、修改前台 BAT,增加上述参数,没有效果;
基于第 2 点,证实环境没有问题,怀疑问题出在导入的代码中,可能是读取、转换时造成的乱码。
检查代码。 在 xxx.xx.xxxx.PersonInfoInputBO 类的 batchProcess 方法中, 增加调试输出信息,发现在实际保存数据前,都没有乱码,只是在以下代码执行后,数据库中保存的数据为乱码:
if (!bHasErr ) {
getHibernateTemplate().saveOrUpdateAll(arr);
}
至此,确认代码没有问题,只能怀疑是数据库驱动的问题。使用以下方式验证:
1、删除 WEB-INF/lib/classes12.zip 文件。
2、把 17 上 eprk_a 环境中的数据库连接池驱动类型从 Oracle 改为 Weblogic(数据库:dtp/dtp1521@21_DB)。
重新导入数据,结果正确,未发生乱码现象。
另,在界面上仍无法录入“”字,怀疑与客户端程序(JRE)中的字符集环境设臵有关。
可以确定是 Oracle JDBC 驱动的问题。 Oracle Thin(OJDBC14.jar/Classes12.jar)的 JDBC 驱动与其它数据库的 JDBC 驱动在字符集设臵方面有区别。其它数据库(如DB2/SQLServer 等)的 JDBC 驱动是可以在 URL 中指定字符集的,但 Oracle 的 JDBC 不可以,将自动使用系统、用户的当前字符集。
使用 Weblogic 的数据库驱动,可解决上述问题,并且性能会有所提升,但第三方驱动的安全有效性值得怀疑,且需要大规模测试。
该问题尚未解决。
以上结论有问题。
3.2 进一步分析
部门其他项目组同事打电话说明也存在相同的问题,本人认为这个问题有一定的挑战性,同时需要一个明确的答案,因为以后一定还会碰到,并且这是对本人相对空档时期的严重藐视,故下决心解决此问题。历时一周,在无数次检查、发版、调试、Google、挑灯夜战、希望、失望、绝望。 。 。后,终于有了结论,抱墙痛哭。
之所以形成这个详细的文档,除了记录并缅怀我那近 50个工时外,还希望对我及对本文档感兴趣的同事警示以下基本要点:
一、不要放过任何你认为“应该不会吧”的假设,一定要验证之;
二、必须要有全局视角,对问题的前后左右,每一步进行细致的验证;
三、Microsoft、IBM、ORACLE 都 TM 操蛋!GB 码更操蛋!!
四、不是兄弟我无能,实在是共军太狡猾了。
五、很多事情,你跟他讲理吧,他跟你耍流氓;你跟他耍流氓吧,他跟你讲理。还是耍流氓比较好。唉。
在初步分析中, 结论是 Oracle 数据库驱动的问题, 但在 WINDOWS 中, 使用Oracle JDBC驱动连接相同的数据库是没有问题的,所以这个结论是不正确的,但 Oracle 的驱动的确存在不能设臵字符集的问题。
还是怀疑是环境的问题,在 GBK 和 GB18030 之间反反复复尝试,由于需要在 AIX 上发版,这个过程几乎占用了80%的时间,尝试了几乎所有的可能性和排列组合,问题没有得到解决。
3.2.1 环境检查的要点
1、检查 Oracle 数据库字符集:
select * from nls_database_parameters;
注意红框处。另外,还有以下相关的 SQL;
select * from nls_instance_parameters;
select * from nls_session_parameters;
select userenv('language') from dual;
这里有个问题,ZHS16GBK 我理解对应 GBK 编码,检查数据库,GB18030 在数据库中应对应 ZHS32GB18030 编码,但由于 Oracle 数据库安装好后再改字符集存在较大风险,没有进行尝试,以下 SQL 可以查看 Oracle 支持的所有字符集:
select * from V$NLS_VALID_VALUES v where v.PARAMETER = 'CHARACTERSET';
2、检查 AIX 系统字符集(WINDOWS 系统不用检查)
3、检查 AIX 用户环境设置
env
LANG,应为 GB18030 或 Zh_CN.GB18030(注意大小写)
NLS_LANGUAGE,应为 AMERICAN_AMERICA.ZHS16GBK,经测试,这个参数不设也没有关系,但相关文档中都是这么要求的,所以还是设臵上为好。
4、JAVA
在服务器(TOMCAT、Weblogic) 、客户端相应的启动 JAVA 的角本中,增加以下参数:
SUN JDK:-Dfile.encoding=GB18030
IBM JDK:-Dibm.stream.nio=true –Dfile.encoding=GB18030
5、在运行时检查环境
参考以下代码
(略)
可以在后台业务处理类或其它地方,嵌入以上代码,并把输出定向到 logger 中,注意输出信息中与encoding 相关的输出值。
一个很没意思的细节:Windows中,sun.unicode 值为 UncodeLittle,AIX 中,其值为UnicodeBig。 太 TM 乱了。 为什么在 Windows中就是Little?在 AIX 中就是 Big?就因为 “微软”?太 TM 有才了。哦,I'm a Big Man!
BTW:金正日是金日成的儿子。据说翻成 E 文是这样的:The King who is fxxking is the son of the King who has fxxked.
3.2.2 下载 并安装 IBM JDK
暂时排除 Oracle JDBC 驱动的嫌疑,在跟踪代码时,发现在调用 JdbcTemplate 保存 VO 前,都能查看到正确的汉字(能够在 logger 中输出。这是最大的、错误的疏漏,其实这时已经乱码了),怀疑是 Hibernate 编码转换的问题,疯狂跟踪、分析 Hibernate 的相关代码,发现虽然看得到汉字,但其value 值为空。往前怀疑,IBM JDK的问题。
SUN从来没有为IBM POWER系列CPU提供过JDK。JDK1.4前,IBM在SUN JDK的基础上修改形成 IBM JDK,JDK 1.4 及以后,IBM 自主开发 JDK。由于只在测试环境中存在问题,每次都要发版,太不方便了,决定下载 IBM JDK,在本机上,看能不能复现问题。
IBM JDK 太难下载了。其官网上只提供 JRE 下载,并且必须安装在 IBM 的机器上,会检查BIOS!Fxxk!
NND,死马当作活马医,我不过了,我决定下载 IBM 官网提供的、所有与 JAVA 相关的东西。还好,美帝总是欺软怕硬的,在 IBM 提供的,基于 Eclipse 的开发环境里就有 IBM JDK 1.6,是完整的 JDK,并且没有 BIOS 限制。下载地址我忘了,下载后的文件名为:
IBM_DevelopmentPackage_for_Eclipse_Win32_3.0.0.zip
好了,米有了,下锅吧。
3.2.3 适配 IBM JDK 和嵌入式 Tomcat
把开发环境中项目的 JRE 改为 IBM JDK 中提供的 JRE,启动 Tomcat,报错:
查了一下,Tomcat 的 SLL 设臵默认使用 SUN 的 X509。但 IBM JDK 中,改成了 IbmX509。
怎么设置?真是闹心。一阵狂试,修改方式如下:
修改 StartServer:
1、在合适的位臵增加 c.setAttribute("algorithm", "IbmX509");
2、注释 main 函数中与 XML 处理相关的代码,如下图:
不知道这样是不是就够了。 我估计不够, 还需要修改 IBM JDK 相关的文件, 具体改了哪里,我不记得了,也不想记得,最终文件如下:
1、把xerces.jar文件放到 ibm_sdk60\jre\lib 目录中;
2、把xerces.properties文件也放到上述目录中。
进行以上修正后,StartServer 能够在 IBM JDK 中正常使用。
调试环境就绪,可以稍微舒心一点干活了。
3.3 问题解决
3.3.1 查看数据库 BLOB 字段中的内容
前文描述过,上传文件后,数据库中 BLOB 字段中的数据看起来没有问题。
数据库 BLOB 中的信息:
以十六进制查看:
所以从来没有怀疑在导入操作前就存在问题,到这个地步,只能再往前走,根据目前掌握的信息,怀疑在上传时,就存在问题了,理由如下:
1、在跟踪“导入”代码时,发现虽然这个字能够输出到日志,但其 value 值为空;
2、GB18030 针对 GBK 增补的汉字(特别是 FE 区),在 Unicode 中有两个编码;
3、JDK 默认编码使用 Unicode(UTF8);
4、WINDOWS 默认编码使用 GBK。
3.3.2 在ORACLE 中进行编码转换试验
select
convert('屁, 'ZHS16GBK', 'ZHS16GBK') gbk,
convert('屁, 'ZHS32GB18030', 'ZHS32GB18030') gb18030,
convert('屁, 'UTF8', 'UTF8') utf8,
convert('屁, 'ZHS16GBK', 'UTF8') utf2gbk,
convert('屁, 'ZHS32GB18030', 'ZHS16GBK') gbk2gb18030,
convert('屁, 'ZHS32GB18030', 'UTF8') utf2gb18030,
convert(convert('屁, 'UTF8', 'UTF8'), 'ZHS32GB18030', 'UTF8') utf2gb18030
from dual;
说明:Convert(内容,目标字符集,来源字符集)
特别注意 GBK2GB18030,乱码了。GBK 是 GB18030 的子集,它们之间的转码根据官方描述应不存在问题,但实际转码时“屁”没有问题, “”有问题。个人推断:Oracle 内部默认字符为Unicode(UTF8), 在进行转码时, 使用Unicode 编码方式进行中间过渡, 导致在Unicode中有两个编码、GB18030 中只有一个编码的汉字乱码。
查看源文件中的编码:
也没有问题啊!唉,这里看起来是没有问题,而且 16 进制编码与数据库 BLOB 字段的也一样,但问题的关键却就在这里。我试着说一下我的理解,不一定对哈:
GBK 是没有收录“”字的,但在 WINDOWS、UltraEdit 却都能正常显示,为什么?因为我安装了 Office(或是其它)提供 GB18030 字符集支持的软件,但 WINDOWS 系统默认是GBK 编码(WIN 7也是)!能显示并不代表编码正确!并且我并没有为 Txt 文件指定编码格式,显示的时候没有问题,当 IBM JDK 以字节流方式读入文件时,这个文件的编码格式被认为是Unicode(UTF8), 即使我明确要求转码为GB18030, 也就是从Unicode(UTF8)转为GB18030,由于取的是 Unicode 中两个编码中对应不到 GB18030 中的那个,所以乱码了。
至此,怀疑 TXT 文件的编码格式有问题。嘿嘿。
3.3.3 修改源文件编码格式
使用 UltraEdit 转码,不支持 GB18030……下载 EditPlus,支持。转码后,相同内容、不同编码格式的文件大小有区别(GB18030 编码的文件大几个字节),并且 GB18030 编码格式文件中的“ ”在记事本中显示的是乱码。乱码就有门啦,不是 GBK,不是 Unicode,是 GB18030啦。
使用 EditPlus 打开 txt 文件, “另存为” :
注意:默认情况下, “编码”框中没有 GB18030,需要先使用“More…”引入,方式为在弹出框中把需要的编码放到左边就行:
GB18030 文件内容,在记事本中无法正确显示:
相应修改源代码
xxx.xx.xxxx.mapping.DataMapping
public Document xsltTransform(Document sourceDoc, MappingContext context) {
Document targetdoc = XMLUtils.getDocumentBuilder().newDocument();
System.out.println("begin xslt Transform " + new Date() + ":" + new Date().getTime());
try {
mappingContextLocal.set(context);
Transformer transformer = template.newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "GB18030");
transformer.transform(new DOMSource(sourceDoc), new DOMResult(targetdoc));
} catch (TransformerException ex) {
throw new EaiException(ex);
}
return targetdoc;
}
增加红色部分的代码。
private Document getSourceDocFromExcel(Object src, Worksheet page) throws BiffException, IOException, EaiException { 。。。 } else if (src instanceof byte[]) {
byte[] bytedata = (byte[]) src;
if (bytedata.length > 2 && bytedata[0] == -48 && bytedata[1] == -49) {
workbook = Workbook.getWorkbook(new ByteArrayInputStream((byte[]) src));
} else {
iscsv = true;
csvdata = new String((byte[]) src,"GB18030");
}
} else {
iscsv = true;
csvdata = (String) src;
}
。。。
}
修改红色部分代码。
xxx.xx.xxxx.File_upBO
private void afterInputSave(File_upVO vo, String path) { if (DataMapping.getMappingContext() != null && DataMapping.getMappingContext().getErrMsg() != null && !"".equals(DataMapping.getMappingContext().getErrMsg())) {
try {
vo.setFiles(DataMapping.getMappingContext().getErrMsg().getBytes("GB18030"));
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e.getMessage());
}
vo.setMemo("导入数据出现失败");
} else {
vo.setMemo("导入数据全部成功");
}
}
修改红色部分代码。
3.3.4 修改 JRE 参数
为 StartServer、LoginUI 指定正确的 JRE 参数:
主要是-Dfile.encoding=GB18030,在 IBM JDK 上,最好再加上-Dibm.stream.nio=true。因为IBM JDK 在 IO 异常处理方面,比 SUN JDK 要严格,别那么麻烦算了。
注意:如果使用 Client,必须在 login.bat 中进行相应的设置。
3.3.5 运行并测试
没有问题了,上传、导入、显示均正常。
4 要点总结
1、 来源文件的编码格式必须正确;
2、 JRE 参数的编码格式必须正确;
3、 JAVA 代码中转码设臵必须正确;
4、 操作系统编码必须支持 GB18030;
5、 操作系统用户的语言环境必须是 GB18030,包括数据库 NLS 设臵;
6、 数据库编码必须为 ZHS16GBK。(如果是 ZHS32GB18030 呢?没有环境测试,想来也是正确的)
7、 以本例来看,由于来源数据是 txt 类型的,所以对应到 BLOB 字段是不合适的,如果使用 CLOB,情况要简单得多,代码也要简单得多。
5 体会
1、 汉字的标准化、国际化,还有很长的路要走啊,并且很可能现在就走乱啦,不知道几只脚在走路啦。
2、 Oracle,你丫真能显示甲骨文么?
6 最终结论及建议
先发点牢骚,你说 GB2312 中 6 千多个汉字不够起名用的,那么兄台,GBK 中 2.1 万个汉字可够?你一定要 GB18030 吗?为什么?WINDOWS都不支持呀,很多输入法都不支持呀,身份证、银行卡等一系列需要把信息保存到计算机的事你很可能都没法办了呀。一定要?俺给你起个名,SB!
按照我前一个行业的经验来看,名字实在不宜起得太复杂。最好就用 GB2312 里的字,太复杂了的话,要么连老师都不认识,要么写起来麻烦。你想想,人家孩子的名字 10 来个笔划,你家孩子的名字 100 来个笔划,新学期发下 10 来本练习本,要求立刻把姓名都写上,你信不信你家孩子写哭了也写不好?造孽呀。
言归正传,虽然这个问题是有解决方案的,但我不建议大家提供给客户,理由如下:
1、 Windows默认编码为 GBK,很多输入法也是;
2、 就算解决了这个生僻字,不知道还没有其它生僻字有问题?康熙字典中有 4.7 万个汉字,能全部支持吗?不能!
3、 你架不住有强人如武则天,在仓颉、许慎等人基础上,自己造字吧?
4、 在计算机里,存在太多的方言,也有太多的水翻译,并且有些方言里的字,在其它方言里根本不存在——讲理的话,应该、必须存在的。
5、 要大范围检查并修改代码。
6、 测试覆盖率怎么定?生僻字怎么定义?我们知道有多少生僻字么?
结论:对于 GBK 没有收录的汉字,系统也不提供支持。要么拆开输入,要么用拼音代替,要么客户自己想替代方案。
对于怎么判断汉字在不在 GBK 里,目前我能想到的比较省事但客户操作起来比较麻烦的解决方案是这样的: 我们向客户提供附件中的 GBK 汉字编码表, 由客户在需要时去一一对比。
不知道客户会不会接收?或自动放弃?或自动想其它办法?
我的方法或描述可能存在问题,但我的结论就这样了。爱咋地咋地。
7 附
7.1 汉字编码表
GBK汉字编码对照表.xls
gb18030的汉字编码.xls
7.2 GB18030 针对 GBK 增补的部分汉字(FE 区 )
增补汉字和部首 80 个, 包括 28 个部首和 52 个汉字。 GBK 编码是从 FE50-FE7E, FE80-FEA0。
在制定 GBK 时,Unicode 中还没有这些字符,所以使用了专用区的码位,这 80 个字符的码位是 0xE815-0xE864。后来,Unicode 将 52 个汉字收录到“CJK 统一汉字扩充 A” 。28 个部首中有 14 个部首被收录到“CJK 部首补充区” 。所以在上图中,这些字符都有两个 Unicode 编码。
一个“”字引发的痛苦经历的更多相关文章
- 由一个emoji引发的思考
由一个emoji引发的思考 从毕业以来,基本就一直在做移动端,但是一直就关于移动端的开发,各种适配问题的解决,在日常搬砖中处理了就过了,也没有把东西都沉淀下来,觉得甚是寒颜.现就一个小bug,让我们来 ...
- QT中QProcess调用命令行的痛苦经历(调用Winrar,设置工作目录,获得输出,注意引号与括号,等等)
QT中QProcess调用命令行的痛苦经历 阅读目录 创建压缩包的方法 在QT中调用命令行 在QT中调用C++创建的dll 在QT程序中需要将某些目录和文件压缩为一个rar的压缩包,于是想到了在Q ...
- bitnami openedx安装的各种坑及痛苦经历
那天在华为网站上学习,看见他们的培训系统挺不错的,看到下边写着openedx,百度了一下是个开源软件,挺流行的,在这之前对于openedx没有任何了解,然后开始了一周多的痛苦经历. 首先在opened ...
- Replication的犄角旮旯(六)-- 一个DDL引发的血案(上)(如何近似估算DDL操作进度)
<Replication的犄角旮旯>系列导读 Replication的犄角旮旯(一)--变更订阅端表名的应用场景 Replication的犄角旮旯(二)--寻找订阅端丢失的记录 Repli ...
- Replication的犄角旮旯(七)-- 一个DDL引发的血案(下)(聊聊logreader的延迟)
<Replication的犄角旮旯>系列导读 Replication的犄角旮旯(一)--变更订阅端表名的应用场景 Replication的犄角旮旯(二)--寻找订阅端丢失的记录 Repli ...
- Spring之LoadTimeWeaver——一个需求引发的思考---转
原文地址:http://www.myexception.cn/software-architecture-design/602651.html Spring之LoadTimeWeaver——一个需求引 ...
- 一个字母引发的血案 java.io.File中mkdir()和mkdirs()
一个字母引发的血案 明天开始放年假了,临放假前有个爬虫的任务,其中需要把网络图片保存到本地,很简单,马上写完了代码: //省略部分代码... Long fileId= (Long) data.get( ...
- Vue一个案例引发「内容分发slot」的最全总结
今天我们继续来说说 Vue,目前一直在自学 Vue 然后也开始做一个项目实战,我一直认为在实战中去发现问题然后解决问题的学习方式是最好的,所以我在学习一些 Vue 的理论之后,就开始自己利用业余时间做 ...
- Vue一个案例引发的递归组件的使用
今天我们继续使用 Vue 的撸我们的实战项目,只有在实战中我们才会领悟更多,光纸上谈兵然并卵,继上篇我们的<Vue一个案例引发的动态组件与全局事件绑定总结> 之后,今天来聊一聊我们如何在项 ...
随机推荐
- spring jar包
org.springframework.aop- 3.0.0.RELEASE--------------------Spring的面向切面编程,提供AOP(面向切面编程)实现 org.springfr ...
- Divide Sum 比赛时竟然想不出。。。。。。。
Divide Sum Time Limit: 2000/1000MS (Java/Others) Memory Limit: 128000/64000KB (Java/Others) SubmitSt ...
- 手把手教你用npm发布一个包,详细教程
我们已经实现了路由的自动化构建,但是我们可以看到,一大串代码怼在里面.当然你也可以说,把它封装在一个JS文件里面,然后使用require('./autoRoute.js')给引入进来,那也行.但是,为 ...
- jvm系列(十):如何优化Java GC「译」
本文由CrowHawk翻译,是Java GC调优的经典佳作. 本文翻译自Sangmin Lee发表在Cubrid上的"Become a Java GC Expert"系列文章的第三 ...
- ch3-模板语法({{}} v-html v-bind:id 表达式 指令 修饰符 过滤器)
1 模板语法 Vue.js 使用了基于 HTML 的模版语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据. 所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器 ...
- uvalive 2965 Jurassic Remains
https://vjudge.net/problem/UVALive-2965 题意: 给出若干个由大写字母组成的字符串,要求选出尽量多的字符串,使得每个大写字母出现的次数是偶数. 思路: 如果说我们 ...
- php 连接mysql数据库以及增删改查
php 连接数据库 一般是用面向对象的方法,需要先创建一个对象,即造一个连接对象,然后再写sql语句,(增改查删),最后执行sql语句 其中在创建连接对象时 我们用到的是MySQLI 是不区分大小写 ...
- 面试题:使用LinkedList来模拟一个堆栈或者队列数据结构
请使用LinkedList来模拟一个堆栈或者队列数据结构. 堆栈:先进后出 First In Last Out (FILO) 队列:先进先出 First In First Out (FIFO) 我 ...
- 即时通信系统Openfire分析之八:集群管理
前言 在第六章<路由表>中,客户端进行会话时,首先要获取对方的Session实例.获取Session实例的方法,是先查找本地路由表,若找不到,则通过路由表中的缓存数据,由定位器获取. 路由 ...
- Java 7 JVM和垃圾收集
---恢复内容开始--- 写JAVA程序,一定要了解JVM(JAVA Virtual machine)一些基础知识和垃圾收集.如果对JVM已经很了解了,可以不用继续往下阅读了.本文只针对Java 7, ...