昨天和一部zzz一起研究解决一个java调用第三方dll的问题,从零开始学习了jni


技术的应用,现在总结如下。

       事情的起因是一部的一个项目需要用到一个爱国者提供的基于U盘的加密技术。对方提供了U盘和一个dll动态链接库hiddenIO.dll。在U盘的隐藏区域内可以储存USB-Key信息,通过这个dll里的两个方法可以使用c/c++编写程序在U盘的隐藏区域读写信息,对方提供了示例代码。由于一部的项目是基于SWT/

RCP 技术的,所以需要在java程序中调用这两个方法。





       目前java与dll交互的技术主要有3种:jni,jawin和jacob。Jni(Java Native Interface)是sun提供的java与系统中的原生方法交互的技术(在windows/linux系统中,实现java与native method互调)。目前只能由c/c++实现。后两个都是sourceforge上的开源项目,同时也都是基于jni技术的 windows系统上的一个应用库。Jacob(Java-Com Bridge)提供了java程序调用microsoft的com对象中的方法的能力。而除了com对象外,jawin(Java/Win32 integration project)还可以win32-dll动态链接库中的方法。就功能而言:jni >> jawin>jacob,其大致的结构如下图:





jni技术体系功能结构图


就易用性而言,正好相反:jacob>jawin>>jni。


Jvm封装了各种操作系统实际的差异性的同时,提供了jni技术,使得开发者可以通过java程序(代码)调用到操作系统相关的技术实现的库函数,从而与其他技术和系统交互,使用其他技术实现的系统的功能;同时其他技术和系统也可以通过jni提供的相应原生接口开调用java应用系统内部实现的功能。


在windows系统上,一般可执行的应用程序都是基于native的PE结构,windows上的jvm也是基于native结构实现的。Java应用体系都是构建于jvm之上。





Windows系统上的java体系Jni对于应用本身来说,可以看做一个代理模式。对于开发者来说,需要使用c/c++来实现一个代理程序(jni程序)来实际操作目标原生函数,java程序中则是jvm通过加载并调用此jni程序来间接地调用目标原生函数。




Jni调用过程示意图






Jni程序开发的一般操作步骤如下:


l         编写java中的调用类


l         用javah生成c/c++原生函数的头文件


l         c/c++中调用需要的其他函数功能,实现原生函数(原则上可以调用任何资源)


l         将项目依赖的所有原生库和资源加入到java项目的java.library.path


l         生成java程序


l         发布java应用和dll库




Jni程序开发示例:


1、  在eclipse项目中新建一个TestHello.java,输入以下内容:

public 
 class TestHello { 
 

    
 static { 
 
      System. 
 loadLibrary ("TestHello"); 
 
   } 
 

    
 public 
 static 
 native 
 void hello(String msg); 
 
    
 
    
 public 
 static 
 void main(String[] args) { 
 
      
 
      
 hello ("Hello,Kimm!"); 
 
      
 
   } 
 

 } 
 
 编译生成TestHello.class文件。 
 
 2、  在命令行下使用javah TestHello命令,生成TestHello.h头文件(就是jni代理stub的接口)。 
 
 3、  在VC6中新建一个项目TestHello, 项目类型为Win32 Dynamic-Link Library。点击OK。 
 

    
 


 在弹出的窗口中选择A simple DLL project,点击Finish。 
 

 
 

 打开项目所在的文件目录,将步骤2中生成的TestHello.h文件复制到此目录。点击左边中间的FileView,切换到文件浏览模式。在Header Files上点击右键,选择Add Files to Folder…。 
 
 选择TestHello.h文件,点击OK。 
 



 打开StdAfx.h文件,再最后面添加: 
 
 #include <jni.h> 
 
 #include "TestHello.h" 
 
 
 
 打开TestHello.cpp文件,在最后面添加一段代码: 
 
 JNIEXPORT void JNICALL Java_TestHello_hello(JNIEnv * env, jclass obj, jstring jMsg) 
 
 { 
 
     const char *strMsgPtr = env->GetStringUTFChars( jMsg , 0);     
 

        MessageBox( 0, strMsgPtr,"Message box from VC++ ", 0 ); 
 

        env->ReleaseStringUTFChars( jMsg, strMsgPtr);   
 

 } 
 


 在VC的菜单上选择Tools-Options…,打开选项对话框。在Directories文件夹,添加上jdk所在文件夹下的include和include/win32文件夹。 
 


 点击VC上的菜单项Build-Build All,生成TestHello.dll。 
 
 4、  将VC项目Debug文件夹中的TestHello.dll复制到TestHello.class所在的文件夹下。 
 
 5、  在命令行下输入java TestHello,弹出MessageBox对话框。调用win32 api成功。


如何用JNI技术提 高Java的性能详解 发布时间:2006.03.01 08:22     来源:赛迪网Java开发者论坛    作者:灰色依旧阻碍Java获得广泛应用的一个主要因素是Java程序的运行效率。Java是介于解释型和编译型 之间的一种语言,同样的程序,如果用编译型语言C来实现,其运行速度一般要比Java快一倍以上。Java具有平台无关性,这使人们在开发企业级应用的时 候总是把它作为主要候选方案之一,但是性能方面的因素又大大削弱了它的竞争力。为此,提高Java的性能就显得十分重要。

问题的提出 Sun公司及Java的支持者们为提高Java的运行速度已经做出了许多努力,其中大多数集中在程序设计的方法和模式选择方面。由于算法和设计模式的优化 是通用的,对Java有效的优化算法和设计模式,对其他编译语言也基本同样适用,因此不能从根本上改变Java程序与编译型语言在执行效率方面的差异。 JIT(Just In Time,及时编译)技术是个比较好的思想。它的基本原理是:首先通过Java编译器把Java源代码编译成平台无关 的二进制字节码。然后在Java程序真正执行之前,系统通过JIT编译器把Java的字节码编译为本地化机器码。最后,系统执行本地化机器码,节省了对字 节码进行解释的时间。这样做的优点是大大提高了Java程序的性能,缩短了加载程序的时间;同时,由于编译的结果并不在程序运行间保存,因此也节约了存储 空间。缺点是由于JIT编译器对所有的代码都想优化,因此同样也占用了很多时间。 动态优化技术是提高Java性能的另一个尝试。该技术试图通过把Java源程序直接编译成机器码,以充分利用Java动态编译和静态编译技术来提高Java的性能。该方法把输入的Java源码或字节码转换为经过高度优化的可执行代码和动态库 (Windows中的. dll文件或Unix中的. so文件)。该技术能大大提高程序的性能,但却破坏了Java的可

移植性

JNI 技术 实际上,有一种通常为我们忽视的技术可以在很大程度上解决这个难题,那就是JNI(Java Native Interface, Java本地化方法)。主张采用纯Java的人们通常反对本地化代码的使用,他们认为在Java程序执行的过程中调用C/C++程序会影响程序的可

移植性 和安全性。还有一些人认为JNI只是对过去混合编程技术的简单扩展,其实际目的是为了充分利用大量原有的C程序库。 其实,我们不必拘泥于严格的平台独立性限制,因为采用JNI技术只 是针对一些严重影响Java性能的代码段,该部分可能只占源程序的极少部分,所以几乎可以不考虑该部分代码在主流平台之间移植的工作量。同时,也不必过分 担心类型匹配问题,我们完全可以控制代码不出现这种错误。此外,也不必担心安全控制问题,因为Java安全模型已扩展为允许非系统类加载和调用本地方法。 根据Java规范,从JDK 1. 2开始,FindClass将设法找到与当前的本地方法关联的类加载器。如果平台相关代码属于一个系统类,则无需涉及任何类加载器; 否则,将调用适当的类加载器来加载和链接已命名的类。换句话说,如果在Java程序中直接调用C/C++语言产生的机器码,该部分代码的安全性就由 Java虚拟机控制。

JNI 实现步骤 编写JNI代码的大致流程如下图所示:

JNI实现流程图 1. 首先编写需要JNI功能的Java类源文件。其中,需要JNI实 现的方法应当用native关键字声明。在该类中,用System. loadLibrary()方法加载需要的动态链接库。关键代码如下: //Compute.java …… public class Compute { public native double comp (double [] params); …… static { // 调用动态链接库 System. loadLibrary(“mathlib”); } …… } 2. 将该类源文件用Java类编译器编译成二进制字节码文件。由于采用了native关键字声明,编译器会忽视没有代码体的JNI方法部分。 3. 利用javah -jni *.class 生成相关JNI方法的头文件。我们可以手工生成该文件,但是由于Java虚拟机是根据一定的命名规范完成对JNI方 法的调用,所以手工编写头文件需要特别小心。 上述文件产生的头文件部分代码如下: //Compute. h …… extern “C” { JNIEXPORT jdouble JNICALL Java_Compute_comp (JNIEnv *, jobject, jdoubleArray); } …… 可以看出,JNI函数名称分为三部分:首先是Java关键字,供Java虚拟机识别;然后是调用者类名称(全限定的类名,其中用下划线代替名称分隔符);最后是对应的方法名称,各段名称之间用下划线分割。 JNI函数的参数也由三部分组成: 首先是JNIEnv *,是一个指向JNI运 行环境的指针;第二个参数随本地方法是静态还是非静态而有所不同——非静态本地方法的第二个参数是对对象的引用,而静态本地方法的第二个参数是对其 Java 类的引用; 其余的参数对应通常 Java 方法的参数,参数类型需要根据一定规则进行映射。 4. 根据头文件编写相应方法的实现代码。由于篇幅所限,具体的实现部分在此不再赘述。在编码过程中,需要注意变量的长度问题,例如Java的整型变量长度为 32位,而C语言为16位,所以要仔细核对变量类型映射表,防止在传值过程中出现问题。 5. 利用C/C++编译器将JNI实现代码编译成动 态链接库。调用者类中需要显式调用该链接库。 在Win32环境下,可以利用Visual C ++或其他能产生DLL文件的C/C++编译器将实现代码编译成动态链接库。笔者利用的是Microsoft.NET Framework的编译器。编译指令如下,其中%Java_HOME%是笔者的jdk安装目录变量: cl -I%Java_HOME%/include -I%Java_HOME%/include/win32 -LD jnicomp. c -Femathlib. dll 在Sun Soloaris下,相应指令为: cc -G -I/usr/local/java/include -I/usr/local/java/include/solaris jnicomp. c / -o mathlib. so 注意,编译的时候需要用I指令包含必要的库文件路径。 经过上述处理,就基本上完成了一个包含本地化方法的Java类的开发。

JNI 技术的应用 一些主要的Java技术,如JDBC和RMI,大部分都采用JNI方式实现。但是,采用JNI确实会影响程序的平台无关性,所以只能在特别需要的地方才能使用。通常来说,如果遇到下面的情况,我们可以考虑JNI: ● 需要直接操作物理设备,而没有相关的驱动程序,这时候我们可能需要用C甚至汇编语言来编写该设备的驱动,然后通过JNI调用; ● 涉及大量数学运算的部分,用Java会带来些效率上的损失; ● 用Java会产生系统难以支付的开销,如需要大量网络链接的场合; ● 存在大量可重用的C/C++代码,通过JNI可以减少开发工作量,避免重复开发。 另外,在利用JNI技术的 时候要注意以下几点: ● 由于Java安全机制的限制,不要试图通过Jar文件的方式发布包含本地化方法的Applet到客户端; ● 注意内存管理问题,虽然在本地方法返回 Java 后将自动释放局部引用,但过多的局部引用将使虚拟机在执行本地方法时耗尽内存; ● JNI技术不仅可以让Java程序调用C/C++代码,也可以让C/C++代码调用Java代码