was中奇怪的生僻字乱码案例

问题描述

这个今天早上提供的一个生产问题。大体是说,改资料的时候,有个客户的名字有生僻字,叫”刘”,保存之后就乱码了,变成”刘?”

分析过程

乱码需要确认数据传输过程中编码方式。

  1. 数据是通过jQuery的ajax过来的,并且没有提前处理数据(只有组装了一个js对象),所以是采用encodeURIComponent进行处理的,对于中文可以很粗糙的理解成UTF-8编码过。这一点通过抓包工具是可以确认的。
  2. 到了服务端之后会通过getParameter获取参数,由于带charsetEncoding的过滤器,并且是采用UTF-8的,那么这里拿到的字符串应该也是不会乱码的。

到了这里,代码并没有特别之处。按我的理解,只要字符集能够支持这个生僻字,就不会出现乱码。
难道保存到数据库的时候乱码了? 目前数据库是用GBK的,我去查了一下GBK的字符表,的确是有这么个字的。

我在本机上测了一下这个字的各种功能编码转换,都是正常的。
难道又是IBM的坑? 后来我又在服务器上测试了各种情况的输出,发现有另外一个字”䶮”,除了字体大小有点不一样之外,几乎一模一样的。

下面整理了一个简单的测试程序,来说明这个奇怪的问题。

测试结果

首先要说明的是,这里有2个字,一小一大,还有它们对应的unicode和utf-8编码。
测试结果是采用secureCRT的GB18030编码显示。

有两个字:       小        大
unicode        \uE863    \u4dae
浏览器(utf-8)   %EE%A1%A3  %E4%B6%AE

下面的测试代码,为了编译时不关心字符集,所以换成utf-8字节来生成字符串。

public class Test {
    public static void main(String[] args) throws java.io.UnsupportedEncodingException {
        new Test().test();
    }

    public void test() throws java.io.UnsupportedEncodingException {
        byte[] bbs = {-18,-95,-93,-28,-74,-82};
        String x = new String(bbs, "utf-8");
        String utf8 = new String(x.getBytes("utf-8"), "iso-8859-1");
        //byte[] bs = utf8.getBytes("iso-8859-1");  //test case 1
        //byte[] bs = x.getBytes("GBK");  //test case 2
        for(byte b : bs){
            System.out.println(b);
        }
        System.out.println(x);
    }
}

对于Test Case 1, 测试一下字符串是不是本来就乱了。测试结果显示,2个字都正常,要输出成GB18030才是可以的(secureCRT设置GB18030编码)。

>/tools/jdk1.6.0_20/bin/java -Dfile.encoding=GBK Test
-18
-95
-93
-28
-74
-82
䶮?
>/opt/IBM/WebSphere/AppServer/java/bin/java -Dfile.encoding=GBK Test
-18
-95
-93
-28
-74
-82
?䶮
>/opt/IBM/WebSphere/AppServer/java/bin/java -Dfile.encoding=GB18030 Test
-18
-95
-93
-28
-74
-82
䶮
>/tools/jdk1.6.0_20/bin/java -Dfile.encoding=GB18030 Test
-18
-95
-93
-28
-74
-82
䶮

对于Test Case 2,主要测试一下转换成GBK字节的情况,因为这是保存到数据库的必要转换。
测试结果显示,ibm的jdk下,第一个字会编程乱码(对应的是63)。

>/tools/jdk1.6.0_20/bin/java -Ddefault.client.encoding=GBK -Dfile.encoding=GBK Test
-2
-97
63
?
>/opt/IBM/WebSphere/AppServer/java/bin/java -Ddefault.client.encoding=GBK -Dfile.encoding=GBK Test
63
-2
-97
?

现象总结

  1. 在GBK字符表中,第一个字是存在的,第二个字不存在。在GB18030中两个都存在。从显示上,也证明了GBK和GB18030并不完全兼容。
  2. IBM的jdk为找不到第一个字,但能找到第二个字。oracle的jdk刚好相反。
  3. 尝试使用百度拼音输入的时候,是可以找到2个字的。如下图的第2和第6个字。
  4. 客户需要的是小的字(第一个),但使用IBM的jdk转换GBK是找不到这个字的,一定会乱码。
  5. 假设从前台输入的是第二个字,IBM的jdk应该是可以正常转换并得到的”正确”的字(正确的小字),从而保证数据库不乱码。

yan

规避方法,选择输入第二个字(大字,截图中的第二个字,应该看不出有什么区别)。话说回来,感觉这是ibm的jdk的bug,字符对应错了。

相关资料

初始化httpClient失败原因分析

问题描述

最近有个程序上线,启动失败,堆栈提示使用httpClient进行网络请求,初始化失败。具体如下:

使用httpClient进行网络请求,当使用IBM J9(JDK6实现)进行运行的时候,会有以下情况:

  • 使用32位版本,在初始化DefaultHttpClient的时候出错,详细情况如下。
  • 使用64位版本,可以正常启动。

测试代码如下:

public class TestSSL
{
  public static void main(String[] args)
  {
    HttpClient httpClient = new DefaultHttpClient();
    HttpPost httpPost = new HttpPost("http://10.132.10.88:81/xxx/Receiver4XXX");
    httpPost.setHeader("contentType", "multipart/form-data");
    MultipartEntity reqEntity = new MultipartEntity();
    httpPost.setEntity(reqEntity);
    try
    {
      httpClient.execute(httpPost);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

启动命令如下:

java -Djava.ext.dirs=./lib/  TestSSL

出错堆栈如下:

java.lang.IllegalStateException: Failure initializing default SSL context
        at org.apache.http.conn.ssl.SSLSocketFactory.createDefaultSSLContext(SSLSocketFactory.java:211)
        at org.apache.http.conn.ssl.SSLSocketFactory.<init>(SSLSocketFactory.java:333)
        at org.apache.http.conn.ssl.SSLSocketFactory.getSocketFactory(SSLSocketFactory.java:165)
        at org.apache.http.impl.conn.SchemeRegistryFactory.createDefault(SchemeRegistryFactory.java:45)
        at org.apache.http.impl.client.AbstractHttpClient.createClientConnectionManager(AbstractHttpClient.java:294)
        at org.apache.http.impl.client.AbstractHttpClient.getConnectionManager(AbstractHttpClient.java:445)
        at org.apache.http.impl.client.AbstractHttpClient.createHttpContext(AbstractHttpClient.java:274)
        at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:797)
        at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:754)
        at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:732)
        at TestSSL.main(TestSSL.java:21)
Caused by: java.lang.NullPointerException
        at org.apache.harmony.security.fortress.Services$NormalServices.createDefaultProviderInstance(Services.java:286)
        at org.apache.harmony.security.fortress.Services$NormalServices.loadAllProviders(Services.java:218)
        at org.apache.harmony.security.fortress.Services$NormalServices.access$400(Services.java:141)
        at org.apache.harmony.security.fortress.Services$NormalServices$2.run(Services.java:207)
        at org.apache.harmony.security.fortress.Services$NormalServices$2.run(Services.java:205)
        at java.security.AccessController.doPrivileged(AccessController.java:202)
        at org.apache.harmony.security.fortress.Services$NormalServices.getProviderList(Services.java:205)
        at org.apache.harmony.security.fortress.Services$NormalServices.access$1300(Services.java:141)
        at org.apache.harmony.security.fortress.Services.getProvidersList(Services.java:645)
        at sun.security.jca.GetInstance.getProvidersList(GetInstance.java:79)
        at sun.security.jca.GetInstance.getInstance(GetInstance.java:232)
        at javax.net.ssl.KeyManagerFactory.getInstance(KeyManagerFactory.java:16)
        at org.apache.http.conn.ssl.SSLSocketFactory.createSSLContext(SSLSocketFactory.java:184)
        at org.apache.http.conn.ssl.SSLSocketFactory.createDefaultSSLContext(SSLSocketFactory.java:209)
        ... 10 more

分析有点冗长,所以分了几个阶段来说明。

问题分析(阶段1)

整理一下堆栈中各部分的功能:

        at org.apache.harmony.security.fortress.Services$NormalServices.createDefaultProviderInstance(Services.java:286)
        at org.apache.harmony.security.fortress.Services$NormalServices.loadAllProviders(Services.java:218)
        at org.apache.harmony.security.fortress.Services$NormalServices.access$400(Services.java:141)
        at org.apache.harmony.security.fortress.Services$NormalServices$2.run(Services.java:207)
        at org.apache.harmony.security.fortress.Services$NormalServices$2.run(Services.java:205)
        at java.security.AccessController.doPrivileged(AccessController.java:202)
        at org.apache.harmony.security.fortress.Services$NormalServices.getProviderList(Services.java:205)
        at org.apache.harmony.security.fortress.Services$NormalServices.access$1300(Services.java:141)
        at org.apache.harmony.security.fortress.Services.getProvidersList(Services.java:645)   -- ServicesJAVA_HOME/security.jar,尝试加载所有的密码算法提供类
        at sun.security.jca.GetInstance.getProvidersList(GetInstance.java:79)    
        at sun.security.jca.GetInstance.getInstance(GetInstance.java:232)          -- GetInstanceJAVA_HOME/rt.jar
        at javax.net.ssl.KeyManagerFactory.getInstance(KeyManagerFactory.java:16)   -- KeyManagerFactoryJAVA_HOME/ibmjssefw.jar.这里需要获取一个算法实现,具体算法是通过java.securityssl.KeyManagerFactory.algorithm指定
        at org.apache.http.conn.ssl.SSLSocketFactory.createSSLContext(SSLSocketFactory.java:184)     -- 基于TLS/SSL协议,需要一个KeyManagerFactory来管理密钥
        at org.apache.http.conn.ssl.SSLSocketFactory.createDefaultSSLContext(SSLSocketFactory.java:209)   -- 默认会先注册httphttps的处理类

问题就出现在加载密码算法提供类的过程,在java的安全体系中,这些提供类是通过JAVA_HOME/security/java.security这个配置文件指定的。

# 格式如下: security.provider.<n>=<className>, 序号代表优先级
security.provider.1=sun.security.provider.Sun
security.provider.2=sun.security.rsa.SunRsaSign
security.provider.3=com.sun.net.ssl.internal.ssl.Provider
security.provider.4=com.sun.crypto.provider.SunJCE
security.provider.5=sun.security.jgss.SunProvider
security.provider.6=com.sun.security.sasl.Provider
security.provider.7=org.jcp.xml.dsig.internal.dom.XMLDSigRI
security.provider.8=sun.security.smartcardio.SunPCSC
security.provider.9=sun.security.mscapi.SunMSCAPI

在IBM的实现中,会配合另外两个配置文件(在security.jar中的org.apache.harmony.security.fortress这个包里边): services.properties和providerClassName.properties。
其中providerClassName.properties指定(提供者的标识, 实现类名)的对应关系。services.properties指定(算法,提供者的标识)的对应管理。

这样就可以实现”寻找DES算法”,找到“提供者的标识”, 最后找到”具体的实现类”,然后就可以调用了,整个过程对开发来说是透明的。太具体的匹配逻辑就不说了,知道这点就可以了。

明显,加载这些类肯定用的是反射技术。不过从反编译的源码上看,IBM在具体的实现细节上有差异。

32位的NormalServices#createDefaultProviderInstance实现

    private static Provider createDefaultProviderInstance(Services.ProviderInfo paramProviderInfo) {
      paramProviderInfo.setLoading();
      String str = paramProviderInfo.getProviderClassName();
      Provider localProvider = createProviderInstance(str, defaultNameProviderMap);

      for (Services.ProviderInfo localProviderInfo : defaultOrderedProviderInfoList) {
        if (localProviderInfo.getProviderClassName().equals(str)) {
          localProviderInfo.setProviderName(localProvider.getName());
          localProviderInfo.setLoaded();
        }
      }
      return localProvider;
    }

64位的NormalServices#createDefaultProviderInstance实现

    private static Provider createDefaultProviderInstance(Services.ProviderInfo paramProviderInfo) {
      synchronized (Services.loadingAndRefreshLock) {
        if (paramProviderInfo.isLoaded()) {
          return (Provider)defaultNameProviderMap.get(paramProviderInfo.getProviderName());
        }
        paramProviderInfo.setLoading();
        String str = paramProviderInfo.getProviderClassName();
        Provider localProvider = createProviderInstance(str, defaultNameProviderMap);

        if (localProvider != null) {
          for (Services.ProviderInfo localProviderInfo : defaultOrderedProviderInfoList) {
            if (localProviderInfo.getProviderClassName().equals(str)) {
              localProviderInfo.setProviderName(localProvider.getName());
              localProviderInfo.setLoaded();
            }
          }
        }
        return localProvider;
      }
    }

其中createProviderInstance方法是通过classpath去加载类。对于当前这个问题来说,最重要的区别在于localProvider是否有没有判空。因为一旦找不到提供类,localProvider将会为null。 而对于目前32位的security.provider配置来说,下面两个类是放在JAVA_HOME/ext下面的:

  • com.sun.security.sasl.Provider 在ibmsaslprovider.jar
  • com.ibm.xml.enc.IBMXMLEncProvider 在ibmxmlencprovider.jar

所以加载到这2个类的时候,localProvider会变成null,导致后面出现空指针。而64位只是忽略不加载而已。

尝试注释这两个security.provider,可以发现启动正常。

问题分析(阶段2)

高大上的IBM JDK怎么会有这种问题呢? 再继续研究研究。

大家有没有注意到启动参数中有个 -Djava.ext.dirs=./lib/, 这个变量以前解释过.

大概就是说,设置classpath要一个个jar包都设置,实在麻烦,于是乎出现这个变量,大多数情况下的确很好很强大。
在少数情况下,这个变量是可能有副作用的,上面提到的问题刚好就是一个例子,导致找不到提供类。

默认情况下,这个变量是指向JAVA_HOME/ext目录的,对应java中的扩展类加载器,使用这个变量就相当于覆盖了扩展类加载的路径。

所以,有另外一种解决办法,就是把原来的扩展类路径添加上去,也是可以正常启动的。

java -Djava.ext.dirs=/usr/java6/jre/lib/ext:./lib/  TestSSL

其实这个变量还是比较少用的,大家可以看看其他比较有名的程序,他们的启动脚本都是读取某个目录的jar包,然后拼接成classpath再启动。
直接指定目录,还有一个不好的地方就是,只要是jar格式的都会被加载(跟后缀名无关,很多人喜欢改名字进行备份的要注意了)

问题分析(阶段3,可略过)

更多探讨,仅供有兴趣的童鞋参考

  • 提供类com.ibm.crypto.provider.IBMJCE也在ext目录的ibmjceprovider.jar中,为什么没有报错

这个类很幸(悲)运(剧)的,因为有人在lib目录里边添加ibmjceprovider.jar这个包,所以它是能被加载到的。

具体可以添加-verbose:class参数,就可以看到的确是在ibmjceprovider.jar中加载到了。

class load: java/util/jar/JarVerifier$VerifierStream
class load: com.ibm.crypto.provider.IBMJCE from: file:/home/hwcrm/caiqs/NGSENDWF/lib/ibmjceprovider.jar
class load: com.ibm.crypto.provider.f from: file:/home/hwcrm/caiqs/NGSENDWF/lib/ibmjceprovider.jar
class load: com/ibm/jsse2/IBMJSSEProvider2
  • 在ext目录被加载很好理解,为什么其他ibm打头的类没在ext中也能被找到

在jre/lib/目录下面有个jars.cfg的配置,个人认为是IBM自己的特殊处理逻辑来的(未经证实),我稍微加了点中文注释,大家可以看看。

# j2se的api被拆开很多个包进行开发,默认的rt.jar是会加载的(可以发现没有常见的集合类,sql类等),欠缺的部分使用Harmony引入(不知大家有没有对印象,曾经的jdk开源实现)
# add Harmony jars 
annotation.jar
beans.jar
java.util.jar
jndi.jar
logging.jar
security.jar
sql.jar

# ORB,就是用于COBRA的开发api
# jars for the IBM ORB
# these must precede rt.jar unless we know that 
# the Sun ORB API has been removed from rt.jar 
ibmorb.jar
ibmorbapi.jar
ibmcfw.jar

# 大家可以发现很多ibm打头的包,这些就是提供类了,看下面的注释,可以发现这些类都是在启动的时候加载的,这样就可以被加载到了。
# List of bootclasspath jars, ordered, relative to jre/lib/
rt.jar
charsets.jar
resources.jar
ibmpkcs.jar
ibmcertpathfw.jar
ibmjgssfw.jar
ibmjssefw.jar
ibmsaslfw.jar
ibmjcefw.jar
ibmjgssprovider.jar
ibmjsseprovider2.jar
ibmcertpathprovider.jar
ibmxmlcrypto.jar
management-agent.jar
xml.jar
jlm.jar
javascript.jar
  • 一定是httpclient引起的么? 会影响其他库或代码么?

从IBM的实现上看,只要使用到security.provider,都会出错。 例如只是简单的使用内置的DES算法实现,就像下面那样,同样也是会出错的。有兴趣可以试试。

Cipher.getInstance("DES");

但是,对于其他JDK实现,例如oracle JDK,不一定有问题(测试一下,发现的确也是没问题的)。因为jdk api是标准规范,当具体的实现并没有做要求。

问题总结

  • IBM JDK对security.provider的处理有所不同,对于找不到的提供类,可能报错也可能不报错。目前只是一个特例,不代表在其他版本或其他JDK中存在。
  • 正常情况下(除非添加自己的实现或修改配置),security.provider都是能够被加载到的。
  • 使用-Djava.ext.dirs会修改扩展类加载路径,可能导致某些提供类找不到。
  • 有以下方式可以修复,仅供参考:
  1. 对httpclient进行定制,跳过https注册或自定义实现,如目前的规避代码。缺点在于仅仅是规避,对其他库可能不适用。
  2. 通过classpath代替java.ext.dirs变量,这是标准的启动方式。缺点在于脚本要重写。
  3. 在java.ext.dirs添加原来的ext目录,是一个方便又能解决问题的手段。缺点在于需要修改脚本, 并可能由于备份文件被加载而造成混乱。
  4. 通过修改java.security配置文件,屏蔽没法使用的提供类。不推荐,影响全局。
  5. 把ext中相关的jar包拷贝到lib目录中。目前有个jar包是这样的,不过不推荐,在不理解系统加载机制的情况下,很容易造成混乱。
  6. 给IBM提意见,修改一下实现方式。说说而已,别想了。

Android-Universal-Image-Loader源码快扫

ImageLoaderConfiguration/ImageLoaderConfiguration.Builder学习

构造复杂对象的方式: Builder模式,用来创建ImageLoaderConfiguration对象,适用于链式写法。

常见结构如下:关键点: 私有构造(拷贝而非应用,避免build复写)、内部类(影响局部化,内聚好)、返回this(支持链式)

class A {
  private A(Builder builder) {
    // create A with builder's copy
  }

  public static class Builder{
     Builder buildStep1(){
        //...
        return this;
     }
     
     Builder buildStep2(){
        //...
        return this;
     }   
     
     A build() {
        return new A(this);
     }  
  }
}

ImageLoaderConfiguration支持的特性

-- 源码没什么养分,关注特性是如何表现在内部结构上。
-- 类层次的结构,能体会就体会,不体会就拉到。层次是渐进实现的,不能体会也没什么。
-- 如果有机会的话,可以在实践项目中调试进去学学。

如果想学习相关特性是怎么实现的,可以根据配置的去反推实现代码(搜索或调用关系):

基本特性(可配置):
  基本控制:
    线程池大小 -- 不能太多
    线程优先级 -- 略低
    请求任务排队  -- 默认FIFO
    是否开启调试日志 -- DEBUG log
  多级缓存特性:
    内存缓存大小
    硬盘缓存大小,文件数量限制,文件名生成规则
    -- 关联硬盘图片处理器BitmapProcessor
  图片专用:
    缓存图最大宽高 --- 用于约束图片大小
    显示图的约束
    -- 关联下载器ImageDownloader(还通过networkDeniedDownloader/slowNetworkDownloader区分不同情况,这不就是Null Object模式么?)
    -- 关联解码器ImageDecoder

关于ImageLoader如何解决错乱问题

ImageLoader就是一个singleton实现,配合init+ImageLoaderConfiguration进行初始化,没什么说的。很常见的设计实现。

大多是helper method 关注displayImage/loadImage最终实现即可。这也是常见做法,便于使用。

问题: url – 关联的view,问题在于url请求是异步的,而view可能被重复利用。
这里用的ImageAware,看他们的实现类,有个ViewAware,就是一个view的包装,我想说的是WeakReference在android中很常用呀。 – 这句是废话

使用ImageAware占位
  图片宽高
  实际的View --可重用
  id -- 尼玛,这是解决问题的关键

见DisplayBitmapTask有个isViewWasReused倒出重点,从它的实现就可以判断完整的算法拉。

内部结构 cacheKeysForImageAwares map<ImageAware.id, memorycachekey(由URI + 宽高生成)>

不过ViewAware的id是用view的hashcode来指定的, NonViewAware的id是用url来指定的。
so, listview的时候,使用loadImage是可以避免错乱的,而用displayImage就呵呵拉。
判断方式如下:根据id可以找到目前的url和当前的url比较即可。知道是不是被复用了。

相对于原理上的image#setTag(url)然后比较的方式,更加透明,侵入性少。

再看看ImageLoaderEngine在任务管理、线程方面的处理

原以为这货应该比较好处理,图样图森破呀,毕竟它支持多种状态(暂停,恢复,关闭等)

先说一下几个AtomicBoolean的变量,主要就是判断状态的boolean拉,当然这货是线程安全的,其实用boolean也行,毕竟也是原子的(注意可见性问题即可)

	private final AtomicBoolean paused = new AtomicBoolean(false);
	private final AtomicBoolean networkDenied = new AtomicBoolean(false);
	private final AtomicBoolean slowNetwork = new AtomicBoolean(false);`

另外,还有3个线程执行器,简单看看用途。至于为什么要这么多个,哥认为是这样的:

  • 在loader中走进displayImage之后,它重要检查一下内存中有木有(耗时小,无需线程)。
  • 如果没有才会扔给engine,engine首先检查一下硬盘上有木有(有io,所以走taskDistributor)。
  • 如果硬盘上有,走taskExecutorForCachedImages(同理有io)
  • 还是没有,走其他(maybe网络io)
  • 在请求较多,兼顾命中、不命中的情况,它选择了采用多级的Executor
	private Executor taskExecutor; -- 木有在disk的情况,一般走网络
	private Executor taskExecutorForCachedImages; -- 在disk的情况
	private Executor taskDistributor;  -- 根据情况分发给上面2个

还有2个map,第一个就是用来保存id和图片url的对应关系(简单是这么理解)
另外是用来控制不同view但有相同uri的并发请求。

	private final Map<Integer, String> cacheKeysForImageAwares = Collections
			.synchronizedMap(new HashMap<Integer, String>());
	private final Map<String, ReentrantLock> uriLocks = new WeakHashMap<String, ReentrantLock>();

不熟悉ReentrantLock的童鞋可参考 https://www.ibm.com/developerworks/cn/java/j-jtp10264/

LoadAndDisplayImageTask通过控制相同的uri持有同一个锁,这样执行的时候,后面的就会等待。
具体可以参考LoadAndDisplayImageTask的实现,不过用ReentrantLock需要特别注意写法,避免死锁。
不过我认为getLockForUri并没有同步,还是有存在相同url获取到不同lock的可能,不过这不影响功能,而且受限于并发大小也很难出现。

关于url的中文

网上有看到一些人说中文图片名会出错,可能是很久之前的版本吧。我这里要说的是,我认为这是个简单的问题,即使改源码也很容易处理。
不过即使源码有相关处理,通常也不会关注的,不过今天刚好有人问我一个中文路径的问题,所以我就关注了一下它怎么实现的。

首先看BaseImageDownloader是如何请求中文图片的。由于http只是支持ascii的url编码,所以必须要编码的,通常用utf-8,虽然这个没规定。貌似我们的基线没有考虑这个问题,应该是没有掉过坑。

	protected HttpURLConnection createConnection(String url, Object extra) throws IOException {
		String encodedUrl = Uri.encode(url, ALLOWED_URI_CHARS);
		HttpURLConnection conn = (HttpURLConnection) new URL(encodedUrl).openConnection();
		conn.setConnectTimeout(connectTimeout);
		conn.setReadTimeout(readTimeout);
		return conn;
	}

另外,还有一个容易出现中文问题的就是保存到硬盘的情况(android貌似不是什么大问题,如果是做server开发的话,就得特别注意)
带的实现有HashCodeFileNameGenerator(默认)和Md5FileNameGenerator,这2种处理都不会产生中文问题。
不过我建议还是用Md5FileNameGenerator,hashcode做唯一性并不是很靠谱,如果是大量的固定文件名长度的图片,还是很容易冲突的。

小结

源码何其多,带着问题学习效果更好。挑几个疑惑看看别人怎么处理就是收获。
从类的层次着手是很困难的,特别是大型源码。了解上层架构,学示例,然后调调源码或许更好。 大多数情况,相对于细节,应该更关注关键数据的结构、如何组织数据的结构来解决问题。
如果自己设计,应该考虑的重要问题有: 如何使用? 用怎样的结构表示数据和状态?
XX设计模式不要硬套,从过程式演变出来更加自然(经验性的除外)。推荐重构与模式。
大而全的源码解读没有什么用,带问题分析的更有价值。
学好基础,模仿起来也不容易掉坑。

– 以上纯属肉眼分辨,并无调试过,不做正确性验证,仅供参考。

java字符编码问题

1.假设文件用UTF-8保存了中文”操作计算机”,然后使用GBK编码进行读取?

String str = FileUtils.readFileToString(new File("/myfile"), "GBK");
System.out.println(str);
str = new String(str.getBytes("GBK"), "UTF-8");
System.out.println(str);

可以发现,后续转成UTF-8仍然有部分乱码,如果保存的内容是”操作计算”就不会乱码。为什么?

2.继续上述问题,如果使用ISO-8859-1进行读取?

String str = FileUtils.readFileToString(new File("/myfile"), "ISO-8859-1");
System.out.println(str);
str = new String(str.getBytes("ISO-8859-1"), "UTF-8");
System.out.println(str);

可以发现,可以发现无论是”操作计算机”还是”操作计算”、”操 作计算”,都不会乱码。为什么?

3.如果文件采用GBK编码保存中文,但是使用UTF-8读取,就会发现怎么转都是乱码? 为什么?

4.假设代码如下,为什么前面3行都是输出乱码?

System.out.println(new String("123你".getBytes("ISO-8859-1"), "ISO-8859-1"));
System.out.println(new String("123你".getBytes("ISO-8859-1"), "GBK"));
System.out.println(new String("123你".getBytes("ISO-8859-1"), "UTF-8"));
System.out.println(new String("123你".getBytes("GBK"), "GBK"));
System.out.println(new String("123你".getBytes("UTF-8"), "UTF-8"));

5.请思考,下面的同样掺和了ISO-8859-1,为什么却能正常?

System.out.println(new String(new String("123你".getBytes("GBK"), "ISO-8859-1")
        .getBytes("ISO-8859-1"), "GBK"));

6.假设使用http发送xml,那么xml报文采用何种编码发送和xml的编码头部指定的编码有什么关系?

<?xml version="1.0" encoding="GBK" ?>

使用net.sf.json库进行json反序列化时存在的问题

问题描述

String content = "{\"response_head\":{\"menuid\":\"xxx\",\"process_code\":\"xxx\",\"verify_code\":\"\",\"resp_time\":\"20150107103234\",\"sequence\":{\"resp_seq\":\"20150107103301\",\"operation_seq\":\"\"},\"retinfo\":{\"retcode\":\"120\",\"rettype\":\"0\",\"retmsg\":\"[182096|]处理失败,原因:[屏蔽具体的失败原因!]\"}},\"response_body\":{} }";
JSONObject object = JSONObject.fromObject(content);
System.out.println(object.toString());

/*
{"response_head":{"menuid":"xxx","process_code":"xxx","verify_code":"","resp_time":"20150107103234","sequence":{"resp_seq":"20150107103301","operation_seq":""},"retinfo":{"retcode":"120","rettype":"0","retmsg":["182096|"]}},"response_body":{}}
*/

问题分析

采用json-lib-2.4-jdk15.jar,测试代码如上,会发现retmsg的值变成”[182096 ”.

测试简化json字符串,最终效果如下:

解析失败的例子:

"{\"response_head\":{\"retmsg\":\"[182096|]处理失败,原因:[屏蔽具体的失败原因!]\"}}"

继续简化的话,就会解析成功

"{\"response_head\":\"[182096|]处理失败,原因:[屏蔽具体的失败原因!]\"}"

找了一下源码,发现json-lib在某些情况下(绕来绕去,断点发现的)会尝试解析字符串,看看是不是json对象。(尼玛,太智能了)

AbstractJSON.java中的260行,这个时候str是后面的内容。

         } else if( JSONUtils.mayBeJSON( str ) ) {
            try {
               return JSONSerializer.toJSON( str, jsonConfig );
            } catch( JSONException jsone ) {
               return str;
            }
         }

JsonArray.java中的1130行,这个时候v已经是”182096|”。这个时候会判断v是不是一个json对象,如果搞一个数组回去,否则就是搞一个字符串(上述现象)。

               tokener.back();
               Object v = tokener.nextValue( jsonConfig );
               if( !JSONUtils.isFunctionHeader( v ) ){
                  if( v instanceof String && JSONUtils.mayBeJSON( (String) v ) ){
                     jsonArray.addValue( JSONUtils.DOUBLE_QUOTE + v + JSONUtils.DOUBLE_QUOTE,
                           jsonConfig );
                  }else{
                     jsonArray.addValue( v, jsonConfig );
                  }
                  fireElementAddedEvent( index, jsonArray.get( index++ ), jsonConfig );
               }

例如,下面的情况会产生一个数组:

"{\"response_head\":{\"retmsg\":\"[{1820: 96|}]处理失败,原因:[屏蔽具体的失败原因!]\"}}"

{"response_head":{"retmsg":[{"1820":"96|"}]}}

关于如何判断是否是json,是会判断以[开头,以]结束的,刚好中枪。而尝试去截取中间内容的时候,又碰巧遇到中间的]字符,所以生成的字符串就是被截断了一部分的。

   /**
    * Tests if the String possibly represents a valid JSON String.<br>
    * Valid JSON strings are:
    * <ul>
    * <li>"null"</li>
    * <li>starts with "[" and ends with "]"</li>
    * <li>starts with "{" and ends with "}"</li>
    * </ul>
    */
   public static boolean mayBeJSON( String string ) {
      return string != null
            && ("null".equals( string )
                  || (string.startsWith( "[" ) && string.endsWith( "]" )) || (string.startsWith( "{" ) && string.endsWith( "}" )));
   }

问题结论

  • 当json对象中某个值是以”{“开头,”}”结束,或者”[“开头,”]”结束的时候,解析结果可能不是期望的。
  • 不幸的是,目前来看,这个问题是无解的,考虑使用其他json库吧。