广州轻工职业学校 刘魁元
广州市机电高级技工学校 余楷鑫
JAVA跨平台的特性深受JAVA程序员的喜爱,这是JAVA的优越性,但是正是为了实现跨平台的目的,JAVA和本地系统的各种内在联系变得很少,这大大约束了它的功能,比如与一些硬件设备的通信,往往要花很大的精力去编写动态函数库来管理设备端口,JDK从1.1版本开始提供了解决这个问题的技术标准:JNI标准;和许多解释执行的语言一样,JAVA提供了调用原生函数的机制,以加强JAVA平台的能力,JavaTMNative Interface(JNI)就是JAVA调用原生函数的机制。
事实上,很多JAVA核心代码内部就是使用JNI实现的,这些JAVA功能实际上是通过原生函数提供的。但是,使用JNI对于JAVA开发者来说简直是一场恶梦;如果你已经有了原生代码,使用JNI,你必须用C语言重新编写一个动态库,这个动态库的唯一功能就是使用JAVA能理解的C代码来调用目标原生函数。一般情况下,设备厂商提供的硬件接口都已经经过一定的封装和处理,不能直接使用JAVA程序通过端口和设备进行通信,JAVA若想与设备进行通信,就必须使用JNI的方式重新编写动态函数库来调用硬件设备,而这种方法的繁冗程度也可想而知,开发效率也不高,因此,人们一直都视JNI为禁地,轻易不愿涉足。
JNA(Java Native Access)是一个开源的JAVA框架,由SUN公司主导开发的,建立在经典的JNI基础之上的一个框架,它提供一组JAVA工具类用于在运行期动态访问系统本地库(native library:如Window的dll)而不需要编写任何Native/JNI代码。开发人员只要在一个JAVA接口中描述目标native library的函数与结构,JNA将自动实现JAVA接口到native function的映射。[1]JNA的项目地址:https://jna.dev.java.net/,JNA使JAVA调用原生函数就像.NET上的P/Invoke一样方便快捷,极大的提高程序员编写代码的效率。JNA使JAVA平台可以方便的调用原生函数,这大大扩展了JAVA平台的整合能力,简化了开发难度,又增强了JAVA与硬件设备通信的功能。
JNA是建立在JNI技术基础之上的一个JAVA类库,它使编程人员可以方便地使用JAVA直接访问动态链接库中的函数,从而实现对.dll/.so文件的访问。原来使用JNI,你必须手工用C写一个动态链接库,在C语言中映射JAVA的数据类型,而编写动态链接库的唯一用途就是使用JAVA能够理解的C代码来调用目标原生函数。同时编写JAVA和C代码的过程使开发的难度大大增加,而这个没其他用途的动态链接库的编写过程显得相当枯燥。JNI调用设备方法如图1所示。
JNA中,它提供了一个动态的C语言编写的转发器,可以自动实现JAVA和C的数据类型映射。作为程序员,不再需要编写C动态链接库,极大地简化了JAVA调用原生函数的过程。当然,这也意味着,使用JNA技术比使用JNI技术调用动态链接库会对性能略有影响,如可能在速度上会降低几倍,但影响并不大。从总体上来看,使用JNA是利远远大于弊的。JNA打破了JAVA和原生代码原本泾渭分明的界限,充分发挥各自擅长领域的分工合作,提高程序员开发的效率。从某种意义上讲,JNA从JNI中来,但却青出于蓝而胜于蓝,逐渐获得了广大开发人员的喜爱。其调用设备方法如图2所示。
表1 JAVA与C语言数据结构的对应关系
图1 JNI调用设备方法
图2 JNA调用设备方法
(1)当前路径是在项目下,而不是bin输出目录下。JNA在搜索dll路径的时候首先是从项目的根路径开始查找,然后再搜索当前操作系统的全局路径,其次搜索path指定的路径。
(2)JNA所使用的数据类型属于JAVA的数据类型,而原生函数中的数据类型是由使用的编程语言决定的,有可能是C、Delphi等语言的数据类型。JAVA与C语言数据结构的对应关系如表1所示。
Dll是C函数的集合、容器,这正和接口的概念吻合。JNA把一个dll/.so文件看做是一个JAVA接口,JNA通过调用接口来实现与第三方dll的通信。下面我们将以一个例子来说明如何调用dll中的函数。
(1)首先我们定义这样一个接口
(2)分析过程如下所示
如果dll是以stdcall方式输出函数,那么就继承StdCallLibrary。否则就继承默认的Library接口。接口内部需要一个公共静态常量:sdtapi。
通过这个常量,就可以获得这个接口的实例,从而使用接口的方法。也就是调用外部dll的函数!注意:1) Native.loadLibrary()函数有2个参数:第一个参数是dll或者.so文件的名字,但不带后缀名。这符合JNI的规范,因为带了后缀名就不可以跨操作系统平台了。第二个参数是本接口的Class类型。JNA通过这个Class类型,根据指定的dll/.so文件,动态创建接口的实例。2)接口中你只需要定义你需要的函数或者公共变量,不需要的可以不定义。
boolean USB_DevInit(int port);
参数和返回值的类型,应该和dll中的C函数的类型一致。这是JNA,甚至所有跨平台调用的难点。这里,C语言的函数参数是:int port;JNA中对应的JAVA类型也是int,所以我们在做跨平台的时候,在数据类型上的选择应该尽量做到简单,这有利于跨平台的实现。
我们已经见识了JNA的强大。但是,有些需求还是必须求助于JNI。JNA是建立在JNI技术基础之上的一个框架。使用JNI技术,不仅可以实现JAVA访问C函数,也可以实现C语言调用JAVA代码。而JNA只能实现JAVA访问C函数,作为一个JAVA框架,自然不能实现C语言调用JAVA代码。此时,你还是需要使用JNI技术。JNI是JNA的基础,是JAVA和C互操作的技术基础。
目前市场上的大多硬件厂商提供的开发包是原生函数,比如读写设备就是这个情况,一般设备厂商会提供两种类型的类库文件,windows系统的会包含.dll/.h/.lib文件,而linux会包含.so/.a文件,这里只讨论windows系统下的c/c++编译的dll文件调用方法。
现在来讨论这样一个问题,我们现要为JAVA项目添加IC卡读写器功能,设备厂商提供了一个fkc60.dll动态库,下面以其中的二个函数为例:
1)bool USB_DevInit(int port);
2)用途及说明:调用其它函数前先打开串口,成功返回true,失败返回false;
3)参数:port表示串行口,1为端口1,2为端口2,以此类推。
1)bool USB_BeepEx(int port,int ptype);
2)用途及说明:控制读写器发声;成功返回true,失败返回false;
3)参数:port表示串口号,1为端口1,2为端口2,以此类推,ptype表示发声类型0发短声,1发长声。
首先,你需要下载一个jna.jar包,就可以方便地调用动态链接库中的C函数了,在JAVA项目中引入jna.jar包,本例是把fkc60放在项目的lib目录下引入的。
最后执行可以看到控制台中打印串行口打开成功信息,并听到读写器发出了短声。
JNA技术相对于JNI技术确实提高了开发的效率,并且扩展了JAVA的功能,但它仍存在着一个缺陷,即破坏了JAVA程序的最重要优点:平台无关性,所以除非必须(不得不)使用JNA技术,一般还是提倡写100%纯JAVA程序,根据自己的经验和查阅的一些资料,把可以使用JNA的情况罗列如下:
1.需要直接操作物理设备,而没有相关的驱动程序;
2.用JAVA会产生系统难以支付的开销,如需要大量网络链接的场合;
3.存在大量可重用的C/C++代码,通过JNA可以减少开发工作量,避免重复开发。
[1]沈东良.深入浅出JNA—快速调用原生函数[J].程序员,2009,3.
[2]匿名.JNA—JNI终结者.