小毛的胡思乱想

凡走过,必留痕迹.

关于android Ndk的jni总结

| Comments

开发工具支持

主要要点如下,更详细的应该参考官方文档:

  1. 需要下载android ndk,并设置ANDROID_NDK_HOME并设置PATH, 在eclipse中顺便也设置一下。
  2. eclipse支持android ndk开发,只需要在项目中右键添加Android Tools > Add Native Support即可。

配置文件

主要配置文件有2个: Android.mk,Application.mk,详细配置还是应该阅读官方文档。下面说一下常用配置。

Application.mk

详细配置参考https://developer.android.com/intl/zh-cn/ndk/guides/application_mk.html

  • APP_STL := stlport_static 设置是否依赖的C++标准库特性,非常重要,详细参数参考https://developer.android.com/intl/zh-cn/ndk/guides/cpp-support.html#runtimes
  • APP_ABI := armeabi armeabi-v7a 设置需要生成so的平台,可以指定或者用all
  • APP_OPTIM := release 生成debug还是relase版本,默认就是release

Android.mk

详细配置参考https://developer.android.com/intl/zh-cn/ndk/guides/android_mk.html
这个配置是可以一次性生成多个so文档的,只需要区分不同的LOCAL_MODULE、LOCAL_SRC_FILES即可。

发现ndk好像默认不支持c/c++混编,所以最好统一成cpp后缀。又或者是我不清楚实际是可以的

  • LOCAL_LDLIBS += -L$(SYSROOT)/usr/lib -lz -llog -landroid 依赖的库,这个例子表示依赖zlib、android log模块及android运行库
  • LOCAL_MODULE := protect 生成的模块名
  • LOCAL_SRC_FILES := NativeApplication.cpp NativeHelper.cpp arcfour.cpp MultiDex.cpp 就是把so需要的相关源文件列出来

jni编程

剩下的内容和Android都没特别关系了,都是java jni的知识。 对于android中jni的各种限制,可以参考官方文档: http://developer.android.com/intl/zh-cn/training/articles/perf-jni.html

生成native方法的头文件

和普通java的没区别,用javah就可以了,就是需要在classpath中添加android的jar即可。举例:

1
2
cd native
javah -cp ./bin/classes;D:\05programs\Android\android-windows\platforms\android-19\android.jar -d ./jni com.huawei.g3.proxy.NativeApplication

默认生成的方法名是有特殊命名规则的(具体规则请自行查阅资料),如果需要不同名字,可以在JNI_OnLoad中进行动态注册,参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void load(JNIEnv * env, jclass clz, jobject obj);
void run(JNIEnv * env, jclass clz, jobject obj);
static JNINativeMethod methods[] = { { "load", "(Landroid/app/Application;)V", (void*) load }, { "run", "(Landroid/app/Application;)V",
        (void*) run } };

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    // Register methods with env->RegisterNatives.
    int len = sizeof(methods) / sizeof(methods[0]);
    jclass native = env->FindClass("com/huawei/g3/proxy/NativeApplication");
    if (env->RegisterNatives(native, methods, len) < 0) {
        return JNI_ERR;
    }
    return JNI_VERSION_1_6;
}

调用java对象、方法、属性

当需要和java进行交互的时候,需要通过特定的api进行调用(类似java的反射,比较麻烦),这种方式是绕过java安全检查机制的。

首先,需要了解jni中的类型表示法(基本就是java字节码那套表示法),特别注意的是内部类的写法

Type Signature Java Type 备注
Z boolean |
B byte |
C char |
S short |
I int |
J long |
F float |
D double |
L fully-qualified-class ; fully-qualified-class Ljava/lang/String; 或内部类Lcom/test/A$B; |
[ type type[] [I 或 [Ljava/lang/String; |
( arg-types ) ret-type method type ()V 或 (Ljava/lang/String;I)Z |
V void |

看懂上面一套表示法,下面的代码也比较容易理解了:

1
2
3
4
5
6
7
8
9
jclass contextClass = env->FindClass("android/content/Context");
jfieldID fieldID = env->GetStaticFieldID(contextClass, "MODE_PRIVATE", "I");
jint mpFv = (jint) env->GetStaticIntField(contextClass, fieldID);

jstring _payload_dex = env->NewStringUTF("payload_dex");

jclass appClass = env->FindClass("android/app/Application");
jmethodID methodID = env->GetMethodID(appClass, "getDir", "(Ljava/lang/String;I)Ljava/io/File;");
jobject dex = env->CallObjectMethod(obj, dirMd, _payload_dex, mpFv); //obj是Application对象,传进来的

需要注意的时候,FindClass参数中类名是不以L开头,不以;结束的

上面的代码,其实就完成了下面一句java代码:

1
File dex = obj.getDir("payload_dex", Context.MODE_PRIVATE);

基本套路都是一样的: 找到类、找到方法或属性、调用方法或调用属性,对应的是jclass、jmethodID、jfieldID几种类型。
详细的方法应该参考jni的官方文档,也很好理解。

1
2
3
4
5
6
* CallStatic<type>Method
* Call<type>Method
* SetStatic<type>Field
* Set<type>Field
* GetStatic<type>Field
* Get<type>Field

这里主要讲一下注意点:

  • 不带后缀、带V、带A的方法名有什么区别

以CallObjectMethod为例,会存在三个方法: CallObjectMethod, CallObjectMethodV, CallObjectMethodA 这个方法都是返回Object对象(jobject)的,效果是没什么区别的,只在于参数传递机制上存在区别。

  • 类型能不完全匹配么?

像java反射那样,获取方法是可以不指定参数类型的。但是jni的类型是必须完全匹配的, 例如找方法void get(HashMap map)的时候,需要使用”(Ljava/util/HashMap;)V”, 而不能使用”(Ljava/util/Map;)V”。

这种问题在处理api兼容性的时候就特别突出。例如下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (version < 19) {
    //using hashmap
    ft1 = "Ljava/util/HashMap;";
    ft2 = "java/util/HashMap";
} else {
    //using arraymap
    ft1 = "Landroid/util/ArrayMap;";
    ft2 = "android/util/ArrayMap";
}

jobject mPackages = _getField(env, currentActivityThread, "mPackages", ft1);
...
jmethodID methodID = _getMethod(env, ft2, "get", "(Ljava/lang/Object;)Ljava/lang/Object;");
jobject wr = env->CallObjectMethod(mPackages, methodID, (jobject) pk);

在java中就可以不用那么烦躁,获取mPackages对象,强制转换成两个类的共同接口Map即可,省事很多。

  • 关于引用、字符串、异常处理

在jni中主要有LocalRef、GlobalRef两种。正常产生的jobject对象,是属于LocalRef的,它的生命周期在当前线程的当前方法有效,类似于c/c++在栈分配的对象。
官方tips也提到了,即使这个对象本身在本地方法返回之后仍然存在,这个引用也是无效的。而实际上只预留了16个LocalRef空间

所以在使用上需要特别注意:

  1. 不要过度分配LocalRef,及时通过DeleteLocalRef方法进行删除。或者通过EnsureLocalCapacity/PushLocalFrame预留更多,不过貌似很少需要。
  2. 如果需要在多次调用中保留,应该采用GlobalRef。通过NewGlobalRef/DeleteGlobalRef手动维护引用。
  3. 和反射一样,查找类、获取方法、获取属性都是有消耗的,在频繁调用的jni方法中,应该通过GlobalRef预先保留相关对象。
  4. 对于stirng类,如果和原生c字符串进行转换操作的时候,需要注意释放内存。
  5. 虽然C++本身也有异常处理,但是切记空指针异常不同于java,需要注意可能为NULL的代码。
  6. 不像java传参是传值(对象是隐含指针传递),在c++中要注意区分传值、传指针、传引用。

通过GlobalRef优化jni的例子

注意: jint等基本类型、jmethodID、jfieldID都不是jobject,不需要管理引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static jobject decryptCipher;

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
        ...
  jobject localDecryptCipher = env->CallStaticObjectMethod(cipher, mid,
          desbuf);
  decryptCipher = (jobject) env->NewGlobalRef(localDecryptCipher);
  env->DeleteLocalRef(localDecryptCipher);
}

void JNI_OnUnload(JavaVM *vm, void *reserved) {
        ...
  if (decryptCipher != NULL) {
      env->DeleteGlobalRef(decryptCipher);
  }
  if (encryptCipher != NULL) {
      env->DeleteGlobalRef(encryptCipher);
  }
}

操作java字符串、c字符串的例子

1
2
3
4
5
6
const char *c_msg2 = env->GetStringUTFChars(dataDir, NULL);  // dateDir是jstring对象
string libPath(c_msg2);
env->ReleaseStringUTFChars(dataDir, c_msg2); // 和GetStringChars不同,GetStringUTFChars方法会分配内存并进行拷贝到c字符串,所以需要手动释放
libPath.append("/lib");
LOGI("lib path is %s", libPath.c_str());
jstring libDir = env->NewStringUTF(libPath.c_str()); // 重新转换成jstring

Weblogic 11g类加载问题总结

| Comments

本人在此之前甚少接触weblogic,家里的weblogic也是第一次安装的。如果发现错误,敬请指正。

问题描述

XX局点升级weblogic为11g,重新发包出错。现在记录一下处理的各种问题总结。

错误1: apache commons某些包的方法没有找到

这是最早出现的问题,会出现类似下面的错误信息。

1
2
<2015-10-14 下午05时57分30秒 CST> <Error> <HTTP> <BEA-101017> <[ServletContext@1385406679[app:XXService module:XXService path:/XXService spec-version:2.5]] Root cause of ServletException.
java.lang.NoSuchMethodError: org.apache.commons.io.FileUtils.copyInputStreamToFile(Ljava/io/InputStream;Ljava/io/File;)V
  • 原因分析

这是weblogic部署最常见的问题,因为weblogic会自带I一些commons-*的包,这些包的版本还比较旧。具体可以见WEBLOGIC_HOME/modules目录的jar包。

  • 此次采用的处理方式

添加weblogic.xml并设置prefer-web-inf-classes,即优先加载web应用下的类

1
2
3
4
5
6
<?xml version="1.0" encoding="UTF-8"?>
<weblogic-web-app>
  <container-descriptor>
      <prefer-web-inf-classes>true</prefer-web-inf-classes>
  </container-descriptor>
</weblogic-web-app>

错误2: jsp使用jstl时出现SAXParserFactory的ClassCastException

这是使用prefer-web-inf-classes为true之后出现的问题,会出现类似下面的错误信息。

1
The validator class: "org.apache.taglibs.standard.tlv.JstlCoreTLV" has failed with the following exception: "java.lang.ClassCastException: weblogic.xml.jaxp.RegistrySAXParserFactory cannot be cast to javax.xml.parsers.SAXParserFactory".
  • 原因分析

这是weblogic部署很常见的问题,jstl会调用sax,sax是通过spi机制加载实现,获取是weblogic的实现,但它使用的是jdk自带的javax.xml.parsers.SAXParserFactory接口。 刚好web应用下也带了jar包xml-apis-1.x.jar,它也有javax.xml.parsers.SAXParserFactory这个接口。根据prefer-web-inf-classes的设置,jstl代码中用的是这个接口。 由此可知,使用classloader并不一样,无法转换。

  • 此次采用的处理方式

删除WEB-INF/lib/xml-apis-1.x.jar后本地测试该问题恢复。

错误3: 出现QName的LinkageError

这是错误2解决后,继续解析spring时出现的问题。

  • 原因分析

这个问题和上面的差不多,太细就不深究了。

  • 此次采用的处理方式

这种情况下,如果使用prefer-web-inf-classes为true,则需要排除存在QName的jar包并删除,但最后没有采用(改动太大,得不偿失)。
所以这次重新设置了prefer-web-inf-classes为false,但仍然优先加载commons,如下:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<weblogic-web-app>
  <container-descriptor>
      <prefer-web-inf-classes>false</prefer-web-inf-classes>
    <prefer-application-packages>
        <package-name>org.apache.commons.*</package-name>
    </prefer-application-packages>
  </container-descriptor>
</weblogic-web-app>

修改后本地测试ok,但发布到生产仍然失败。

错误4: MemCachedClient获取key失败(序列化问题)

错误3处理后,发布到生产仍然出错,报错信息如下:

1
2
3
4
5
6
7
8
9
18:44:57.337 [[ACTIVE] ExecuteThread: '0' for queue: 'weblogic.kernel.Default (self-tuning)'] ERROR com.danga.MemCached.MemCachedClient - ++++ exception thrown while trying to get object from cache for key: init_error_key_0098
18:44:57.351 [[ACTIVE] ExecuteThread: '0' for queue: 'weblogic.kernel.Default (self-tuning)'] ERROR com.danga.MemCached.MemCachedClient - com.xxx.hnxx.mybatis.entity.PlaterrorCodeBean
java.io.IOException: com.xxx.hnxx.mybatis.entity.PlaterrorCodeBean
  at com.schooner.MemCached.ObjectTransCoder.decode(Unknown Source) ~[MemCached-2.6.6.jar:na]
  at com.schooner.MemCached.AscIIClient.get(Unknown Source) [MemCached-2.6.6.jar:na]
  at com.schooner.MemCached.AscIIClient.get(Unknown Source) [MemCached-2.6.6.jar:na]
  at com.schooner.MemCached.AscIIClient.get(Unknown Source) [MemCached-2.6.6.jar:na]
  at com.danga.MemCached.MemCachedClient.get(Unknown Source) [MemCached-2.6.6.jar:na]
  at com.xxx.hnxx.cache.mencached.MemcacheManagerClient.get(MemcacheManagerClient.java:162) [MemcacheManagerClient.class:na]

上面的错误信息表示获取init_error_key_0098这个可以的时候失败,实际上这个key是在应用启动的时候就塞进去的。

  • 原因分析

这里有很多意想不到的事情,所以详细解释一下。

首先,这个出现了IOException让人联想到是否memcached服务器连接的问题。
实际上是因为库在实现java对象放入memcached的时候,有一个序列化/反序列化的过程(就是java自带的那个),在反序列化的时候找不到类会出现ClassNotFoundException,然后库将错误信息(就是一个类名)取出重新包装为IOException。
所以,这实际上是一个类找不到的问题。

再者,这个问题一开始在家里的weblogic没法重现。后来我重新检查了生产上weblogic的启动日志才发现了一些差异。
关键信息如下所示,生产上的weblogic在domain的lib目录也是有jar包的,而家里的是没有的。尝试修改把jar包也拷贝一份,果然重现。

1
<2015-10-14 下午06时44分36秒 CST> <Notice> <WebLogicServer> <BEA-000395> <Following extensions directory contents added to the end of the classpath:/weblogic/bea/user_projects/domains/PLATFORM_DOM/lib/MemCached-2.6.6.jar:/weblogic/bea/user_projects/domains/PLATFORM_DOM/lib/MyXMLSerializer-1.0.0.jar...

最后,这个问题就好解释多了。

  1. 需要序列化/反序列化的类是在com.huawei下面的,这部分类指在web应用中存在。在system classloader是找不到的。
  2. 序列化/反序列化时候,都是由web应用中的类,调用memcached库去实现的(虽然web应用中也有,但是根据prefer-web-inf-classes设置,加载的是domain中lib目录的)
  3. 序列化只是没什么特别。但是反序列化需要加载类,很明显system classloader(memcached库的classloader)是加载不到web应用中的类的。

  4. 此次采用的处理方式

有好几种方式,都列举一下:

  1. 删除domain中的jar包,这样就会加载到web应用中的类,让库和需要序列化的类都有web classloader加载
  2. 让库也由web优先加载,如下所示
1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<weblogic-web-app>
  <container-descriptor>
      <prefer-web-inf-classes>false</prefer-web-inf-classes>
    <prefer-application-packages>
        <package-name>org.apache.commons.*</package-name>
        <package-name>com.danga.*</package-name>
        <package-name>com.schooner.*</package-name>
    </prefer-application-packages>
  </container-descriptor>
</weblogic-web-app>
  1. 指定memcached库进行反序列化时的classloader,如下所示:
1
2
3
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    // MemCachedClient实例化时,会持有SockIOPool.getInstance()单利的引用
    cachedClient = new MemCachedClient((String)null, true, false, classLoader, null);

个人推荐的优先级是2 - 3 - 1, 尽量做到容器无关,并少动全局的东西。由于目前生产上的weblogic版本已经回退,待后续上生产验证。

weblogic的类加载器介绍

  • 整体的类加载器层次如下(只关注war部分),并采用标准的双亲委托加载机制
1
2
3
4
WebLogic Server System classloader (classpath<domain>/lib)
Filtering classloader ()
Application classloader (EJB JARsAPP-INF/libAPP-INF/classesManifest Class-Path in EJB JARs)
Web application classloader (WARManifest Class-Path in WAR)
  • Web application classloader可以通过weblogic.xml中的prefer-web-inf-classes优先加载war中的类,找不到才向上请求
  • Filtering classloader并不会加载任何类,而是起到控制类加载优先级的作用。通过配置可以限制对于指定的类不再向上请求,也就是限制范围内加载
  • 配置prefer-application-packages/prefer-application-resources的话,prefer-web-inf-classes必须配置为false
  • 资源(resource)的加载顺序,在开启Filtering之后,顺序为App - Web - System(App、Web仍然是符合双亲委托的)

参考材料

  • http://docs.oracle.com/cd/E23943_01/web.1111/e13712/weblogic_xml.htm#WBAPP599
  • http://docs.oracle.com/cd/E12839_01/web.1111/e13706/classloading.htm#WLPRG284
  • http://tobato.iteye.com/blog/1845969
  • http://tobato.iteye.com/blog/1483020

Websphere共享库加载顺序问题

| Comments

问题描述

昨天收到有个童鞋发来的一个问题咨询,如下图所示。

问题截图

提到几个疑问:

  1. 配置如图所示,然后共享库和项目自身lib下都有一个“xxxx-common.jar”,如果项目用到jar包里面的一个类,将会是共享库的还是自身lib的呢?发现是用的lib里边的。
  2. 现网情况也是xxlib和lib下都有那jar包,但是根据日志来看,是用共享库的。

我的疑惑

现网的配置如何,暂时没查明。不过就开发环境的配置来看,我一直认为Parent First应该会走共享库的,目前的现象和我掌握的知识不匹配。

于是,我上网搜索了一下Websphere关于共享库的资料,主要的链接如下:

  • http://www.ibm.com/developerworks/cn/websphere/library/techarticles/haoaili/0512/
  • http://www-01.ibm.com/support/knowledgecenter/SSAW57_8.5.5/com.ibm.websphere.nd.doc/ae/tcws_sharedlib_nativelib.html?lang=zh
  • https://10.132.10.69:9043/ibm/help/index.jsp?topic=/com.ibm.ws.console.environment/ucws_rsharedlib_inst.html

通过这些链接资料的描述(不得不说,这些中文翻译很隐晦),但的确是可以解释目前的情况的。

关于Websphere共享库的理解

首先,Websphere的共享库和tomcat的共享库差别很大,而我却一直以为是差不多的。
tomcat的共享库是一个独立的类加载器,并且在多个Web应用中共享。好处是明显的,共享加载的类,优化内存使用。

其次,Webshpere的共享库非常灵(fu)活(za),有多种配置组合可以影响结果。具体如下:

共享库是可以选择和服务器关联或者和应用关联的

  • 和服务器关联,参考http://www-01.ibm.com/support/knowledgecenter/SSAW57_8.5.5/com.ibm.websphere.nd.doc/ae/tcws_sharedlib_server.html?lang=zh
  • 和应用关联,参考http://www-01.ibm.com/support/knowledgecenter/SSAW57_8.5.5/com.ibm.websphere.nd.doc/ae/tcws_sharedlib_app.html?lang=zh

共享库是可以选择是否使用隔离的类装入器(就是独立的类加载器)

设置参考下图所示:

请对此共享库使用隔离的类装入器

和共享库相关的类加载策略如下:

  • 如果选择和服务器关联,那么将忽略”请对此共享库使用隔离的类装入器”的选项,此时共享库路径将会添加到应用程序服务器(application server)类装入器加载路径上。
  • 如果选择和应用关联,并且没有设置”请对此共享库使用隔离的类装入器”,那么共享库路径将会添加到应用的类加载器加载路径上。此时共享库只有优化管理类库的作用,并不能减少重复加载类造成的内存占用。
  • 如果选择和应用关联,并且设置”请对此共享库使用隔离的类装入器”,那么共享库将作为独立的类加载器,并且各个应用之间共享这个共享库。此时共享库和tomcat的共享库类似,可以减少重复加载类造成的内存占用。

对于第三种情况,它的类加载顺序如下:

如果应用的类载入顺序选择“父类装入器装入的类最先”,即Parent First,那么顺序如下:

  • 检查相关联的库类装入器是否可以装入类。(共享库)
  • 检查它的父代类装入器是否可以装入类。(应用服务器及更高)
  • 检查应用程序或 WAR 模块类装入器是否可以装入类。(应用)

如果应用的类载入顺序选择“本地类装入器装入的类最先”,即Parent Last,那么顺序如下:

  • 检查应用程序或 WAR 模块类装入器是否可以装入类。(应用)
  • 检查相关联的库类装入器是否可以装入类。(共享库)
  • 检查它的父代类装入器是否可以装入类。(应用服务器及更高)

现象解释

  • 开发环境中,共享库和应用关联,并且没有设置”请对此共享库使用隔离的类装入器”,所以共享库路径将会添加到应用的类加载器加载路径上,相当于在一个类加载路径上存在同样的类,所以使用到lib中的是可能的。
  • 生产环境中配置尚未查明,如果共享库和应用关联,并且设置”请对此共享库使用隔离的类装入器”,按同样的载入顺序设置,即Parent First,那么是会加载到共享库的。
  • 如果同样是没有设置”请对此共享库使用隔离的类装入器”,那么情况如开发环境情况,使用到共享库中的也是可能的。
  • 对于同一个类加载路径上存在同样的类,具体会加载哪个是不确定的,所以上述情况都是合理的。所以应该把应用中重复的jar包移除。

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

| Comments

问题描述

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

分析过程

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

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

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

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

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

测试结果

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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编码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>/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
?䶮
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>/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)。

1
2
3
4
5
6
7
8
9
10
>/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失败原因分析

| Comments

问题描述

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

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

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

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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();
    }
  }
}

启动命令如下:

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

出错堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    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这个配置文件指定的。

1
2
3
4
5
6
7
8
9
10
# 格式如下: 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实现

1
2
3
4
5
6
7
8
9
10
11
12
13
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实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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中的扩展类加载器,使用这个变量就相当于覆盖了扩展类加载的路径。

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

1
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中加载到了。

1
2
3
4
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自己的特殊处理逻辑来的(未经证实),我稍微加了点中文注释,大家可以看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 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算法实现,就像下面那样,同样也是会出错的。有兴趣可以试试。

1
Cipher.getInstance("DES");

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

问题总结

  • IBM JDK对security.provider的处理有所不同,对于找不到的提供类,可能报错也可能不报错。目前只是一个特例,不代表在其他版本或其他JDK中存在。
  • 正常情况下(除非添加自己的实现或修改配置),security.provider都是能够被加载到的。
  • 使用-Djava.ext.dirs会修改扩展类加载路径,可能导致某些提供类找不到。
  • 有以下方式可以修复,仅供参考:

  • 对httpclient进行定制,跳过https注册或自定义实现,如目前的规避代码。缺点在于仅仅是规避,对其他库可能不适用。

  • 通过classpath代替java.ext.dirs变量,这是标准的启动方式。缺点在于脚本要重写。
  • 在java.ext.dirs添加原来的ext目录,是一个方便又能解决问题的手段。缺点在于需要修改脚本, 并可能由于备份文件被加载而造成混乱。
  • 通过修改java.security配置文件,屏蔽没法使用的提供类。不推荐,影响全局。
  • 把ext中相关的jar包拷贝到lib目录中。目前有个jar包是这样的,不过不推荐,在不理解系统加载机制的情况下,很容易造成混乱。
  • 给IBM提意见,修改一下实现方式。说说而已,别想了。