JAVA的跨平臺(tái)的特性深受java程序員們的喜愛,但正是由于它為了實(shí)現(xiàn)跨平臺(tái)的目的,使得它和本地機(jī)器的各種內(nèi)部聯(lián)系變得很少,大大約束了它的功能,比如與一些硬件設(shè)備通信,往往要花費(fèi)很大的精力去設(shè)計(jì)流程編寫代碼去管理設(shè)備端口,而且有一些設(shè)備廠商提供的硬件接口已經(jīng)經(jīng)過一定的封裝和處理,不能直接使用java程序通過端口和設(shè)備通信,這種情況下就得考慮使用java程序去調(diào)用比較擅長(zhǎng)同系統(tǒng)打交道的第三方程序,從1.1版本開始的JDK提供了解決這個(gè)問題的技術(shù)標(biāo)準(zhǔn):JNI技術(shù).
JNI是Java Native Interface(Java本地接口)的縮寫,本地是相對(duì)于java程序來說的,指直接運(yùn)行在操作系統(tǒng)之上,與操作系統(tǒng)直接交互的程序.從1.1版本的JDK開始,JNI就作為標(biāo)準(zhǔn)平臺(tái)的一部分發(fā)行.在JNI出現(xiàn)的初期是為了Java程序與本地已編譯語(yǔ)言,尤其是C和C++的互操作而設(shè)計(jì)的,后來經(jīng)過擴(kuò)展也可以與c和c++之外的語(yǔ)言編寫的程序交互,例如Delphi程序.
使用JNI技術(shù)固然增強(qiáng)了java程序的性能和功能,但是它也破壞了java的跨平臺(tái)的優(yōu)點(diǎn),影響程序的可移植性和安全性,例如由于其他語(yǔ)言(如C/C++)可能能夠隨意地分配對(duì)象/占用內(nèi)存,Java的指針安全性得不到保證.但在有些情況下,使用JNI是可以接受的,甚至是必須的,例如上面提到的使用java程序調(diào)用硬件廠商提供的類庫(kù)同設(shè)備通信等,目前市場(chǎng)上的許多讀卡器設(shè)備就是這種情況.在這必須使用JNI的情況下,盡量把所有本地方法都封裝在單個(gè)類中,這個(gè)類調(diào)用單個(gè)的本地庫(kù)文件,并保證對(duì)于每種目標(biāo)操作系統(tǒng),都可以用特定于適當(dāng)平臺(tái)的版本替換這個(gè)文件,這樣使用JNI得到的要比失去的多很多.
現(xiàn)在開始討論上面提到的問題,一般設(shè)備商會(huì)提供兩種類型的類庫(kù)文件,windows系統(tǒng)的會(huì)包含.dll/.h/.lib文件,而linux系統(tǒng)的會(huì)包含.so/.a文件,這里只討論windows系統(tǒng)下的c/c++編譯的dll文件調(diào)用方法.
我把設(shè)備商提供的dll文件稱之為第三方dll文件,之所以說第三方,是因?yàn)镴NI直接調(diào)用的是按它的標(biāo)準(zhǔn)使用c/c++語(yǔ)言編譯的dll文件,這個(gè)文件是客戶程序員按照設(shè)備商提供的.h文件中的列出的方法編寫的dll文件,我稱之為第二方dll文件,真正調(diào)用設(shè)備商提供的dll文件的其實(shí)就是這個(gè)第二方dll文件.到這里,解決問題的思路已經(jīng)產(chǎn)生了,大慨分可以分為三步:
1>編寫一個(gè)java類,這個(gè)類包含的方法是按照設(shè)備商提供的.h文件經(jīng)過變形/轉(zhuǎn)換處理過的,并且必須使用native定義.這個(gè)地方需要注意的問題是java程序中定義的方法不必追求和廠商提供的頭文件列出的方法清單中的方法具有相同的名字/返回值/參數(shù),因?yàn)橐恍﹨?shù)類型如指針等在java中沒法模擬,只要能保證這個(gè)方法能實(shí)現(xiàn)原dll文件中的方法提供的功能就行了;
2>按JNI的規(guī)則使用c/c++語(yǔ)言編寫一個(gè)dll程序;
3>按dll調(diào)用dll的規(guī)則在自己編寫的dll程序里面調(diào)用廠商提供的dll程序中定義的方法.
我之前為了給一個(gè)java項(xiàng)目添加IC卡讀寫功能,曾經(jīng)查了很多資料發(fā)現(xiàn)查到的資料都是只說到第二步,所以剩下的就只好自己動(dòng)手研究了.下面結(jié)合具體的代碼來按這三個(gè)步驟分析.
1>假設(shè)廠商提供的.h文件中定義了一個(gè)我們需要的方法:
__int16 __stdcall readData( HANDLE icdev, __int16 offset, __int16 len, unsigned char *data_buffer );
a.__int16定義了一個(gè)不依賴于具體的硬件和軟件環(huán)境,在任何環(huán)境下都占16 bit的整型數(shù)據(jù)(java中的int類型是32 bit),這個(gè)數(shù)據(jù)類型是vc++中特定的數(shù)據(jù)類型,所以我自己做的dll也是用的vc++來編譯.
b.__stdcall表示這個(gè)函數(shù)可以被其它程序調(diào)用,vc++編譯的DLL欲被其他語(yǔ)言編寫的程序調(diào)用,應(yīng)將函數(shù)的調(diào)用方式聲明為__stdcall方式,WINAPI都采用這種方式.c/c++語(yǔ)言默認(rèn)的調(diào)用方式是__cdecl,所以在自己做可被java程序調(diào)用的dll時(shí)一定要加上__stdcall的聲明,否則在java程序執(zhí)行時(shí)會(huì)報(bào)類型不匹配的錯(cuò)誤.
c.HANDLE icdev是windows操作系統(tǒng)中的一個(gè)概念,屬于win32的一種數(shù)據(jù)類型,代表一個(gè)核心對(duì)象在某一個(gè)進(jìn)程中的唯一索引,不是指針,在知道這個(gè)索引代表的對(duì)象類型時(shí)可以強(qiáng)制轉(zhuǎn)換成此類型的數(shù)據(jù).
這些知識(shí)都屬于win32編程的范圍,更為詳細(xì)的win32資料可以查閱相關(guān)的文檔.
這個(gè)方法的原始含義是通過設(shè)備初始時(shí)產(chǎn)生的設(shè)備標(biāo)志號(hào)icdev,讀取從某字符串在內(nèi)存空間中的相對(duì)超始位置offset開始的共len個(gè)字符,并存放到data_buffer指向的無符號(hào)字符類型的內(nèi)存空間中,并返回一個(gè)16 bit的整型值來標(biāo)志這次的讀設(shè)備是否成功,這里真正需要的是unsigned char *這個(gè)指針指向的地址存放的數(shù)據(jù),而java中沒有指針類型,所以可以考慮定義一個(gè)返回字符串類型的java方法,原方法中返回的整型值也可以按經(jīng)過一定的規(guī)則處理按字符串類型傳出,由于HANDLE是一個(gè)類型于java中的Ojbect類型的數(shù)據(jù),可以把它當(dāng)作int類型處理,這樣java程序中的方法定義就已經(jīng)形成了:
String readData( int icdev, int offset, int len );
聲明這個(gè)方法的時(shí)候要加上native關(guān)鍵字,表明這是一個(gè)與本地方法通信的java方法,同時(shí)為了安全起見,此文方法要對(duì)其它類隱藏,使用private聲明,再另外寫一個(gè)public方法去調(diào)用它,同時(shí)要在這個(gè)類中把本地文件加載進(jìn)來,最終的代碼如下:
package test;
public class LinkDll
{
//從指定地址讀數(shù)據(jù)
private native String readData( int icdev, int offset, int len );
public String readData( int icdev, int offset, int len )
{
return this.readDataTemp( icdev, offset, len );
}
static
{
System.loadLibrary( "TestDll" );//如果執(zhí)行環(huán)境是linux這里加載的是SO文件,如果是windows環(huán)境這里加載的是dll文件
}
}
2>使用JDK的javah命令為這個(gè)類生成一個(gè)包含類中的方法定義的.h文件,可進(jìn)入到class文件包的根目錄下(只要是在classpath參數(shù)中的路徑即可),使用javah命令的時(shí)候要加上包名javah test.LinkDll,命令成功后生成一個(gè)名為test_LinkDll.h的頭文件.
文件內(nèi)容如下:
/* DO NOT EDIT THIS FILE - it is machine generated*/
#include <jni.h>
/* Header for class test_LinkDll */
#ifndef _Included_test_LinkDll #define
Included_test_LinkDll
#ifdef __cplusplus extern "C" { #endif
/*
* Class: test_LinkDll
* Method: readDataTemp
* Signature: (III)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_test_LinkDll_readDataTemp(JNIEnv *, jobject, jint, jint, jint);
#ifdef __cplusplus } #endif
#endif
可以看出,JNI為了實(shí)現(xiàn)和dll文件的通信,已經(jīng)按它的標(biāo)準(zhǔn)對(duì)方法名/參數(shù)類型/參數(shù)數(shù)目作了一定的處理,其中的JNIEnv*/jobjtct這兩個(gè)參數(shù)是每個(gè)JNI方法固有的參數(shù),javah命令負(fù)責(zé)按JNI標(biāo)準(zhǔn)為每個(gè)java方法加上這兩個(gè)參數(shù).JNIEnv是指向類型為JNIEnv_的一個(gè)特殊JNI數(shù)據(jù)結(jié)構(gòu)的指針,當(dāng)由C++編譯器編譯時(shí)JNIEnv_結(jié)構(gòu)其實(shí)被定義為一個(gè)類,這個(gè)類中定義了很多內(nèi)嵌函數(shù),通過使用"->"符號(hào),可以很方便使用這些函數(shù),如:
(env)->NewString( jchar* c, jint len )
可以從指針c指向的地址開始讀取len個(gè)字符封裝成一個(gè)JString類型的數(shù)據(jù).
其中的jchar對(duì)應(yīng)于c/c++中的char,jint對(duì)應(yīng)于c/c++中的len,JString對(duì)應(yīng)于java中的String,通過查看jni.h可以看到這些數(shù)據(jù)類型其實(shí)都是根據(jù)java和c/c++中的數(shù)據(jù)類型對(duì)應(yīng)關(guān)系使用typedef關(guān)鍵字重新定義的基本數(shù)據(jù)類型或結(jié)構(gòu)體.
具體的對(duì)應(yīng)關(guān)系如下:
Java類型 本地類型 描述
boolean jboolean C/C++8位整型
byte jbyte C/C++帶符號(hào)的8位整型
char jchar C/C++無符號(hào)的16位整型
short jshort C/C++帶符號(hào)的16位整型
int jint C/C++帶符號(hào)的32位整型
long jlong C/C++帶符號(hào)的64位整型e
float jfloat C/C++32位浮點(diǎn)型
double jdouble C/C++64位浮點(diǎn)型
Object jobject 任何Java對(duì)象,或者沒有對(duì)應(yīng)java類型的對(duì)象
Class jclass Class對(duì)象
String jstring 字符串對(duì)象
Object[] jobjectArray 任何對(duì)象的數(shù)組
boolean[] jbooleanArray 布爾型數(shù)組
byte[] jbyteArray 比特型數(shù)組
char[] jcharArray 字符型數(shù)組
short[] jshortArray 短整型數(shù)組
int[] jintArray 整型數(shù)組
long[] jlongArray 長(zhǎng)整型數(shù)組
float[] jfloatArray 浮點(diǎn)型數(shù)組
double[] jdoubleArray 雙浮點(diǎn)型數(shù)組
更為詳細(xì)的資料可以查閱JNI文檔.
需要注意的問題:test_LinkDll.h文件包含了jni.h文件;
3>使用vc++ 6.0編寫TestDll.dll文件,這個(gè)文件名是和java類中l(wèi)oadLibrary的名稱一致.
a>使用vc++6.0 新建一個(gè)Win32 Dynamic-Link Library的工程文件,工程名指定為TestDll
b>把源代碼文件和頭文件使用"Add Fiels to Project"菜單加載到工程中,若使用c來編碼,源碼文件后綴名為.c,若使用c++來編碼,源碼文件擴(kuò)展名為.cpp,這個(gè)一定要搞清楚,因?yàn)閷?duì)于不同的語(yǔ)言,使用JNIEnv指針的方式是不同的.
c>在這個(gè)文件里調(diào)用設(shè)備商提供的dll文件,設(shè)備商一般提供三種文件:dll/lib/h,這里假設(shè)分別為A.dll/A.lib/A.h.
這個(gè)地方的調(diào)用分為動(dòng)態(tài)調(diào)用和靜態(tài)調(diào)用靜態(tài)調(diào)用即是只要把被調(diào)用的dll文件放到path路徑下,然后加載lib鏈接文件和.h頭文件即可直接調(diào)用A.dll中的方法:
把設(shè)備商提供的A.h文件使用"Add Fiels to Project"菜單加載到這個(gè)工程中,同時(shí)在源代碼文件中要把這個(gè)A.h文件使用include包含進(jìn)來;
然后依次點(diǎn)擊"Project->settings"菜單,打開link選項(xiàng)卡,把A.lib添加到"Object/library modules"選項(xiàng)中.
具體的代碼如下:
//讀出數(shù)據(jù),需要注意的是如果是c程序在調(diào)用JNI函數(shù)時(shí)必須在JNIEnv的變量名前加*,如(*env)->xxx,如果是c++程序,則直接使用(env)->xxx
#include<WINDOWS.H>
#include<MALLOC.H>
#include<STDIO.H>
#include<jni.h>
#include "test_LinkDll.h"
#include "A.h"
JNIEXPORT jstring JNICALL Java_test_LinkDll_readDataTemp( JNIEnv *env, jobject jo, jint ji_icdev, jint ji_len )
{
//*************************基本數(shù)據(jù)聲明與定義******************************
HANDLE H_icdev = (HANDLE)ji_icdev;//設(shè)備標(biāo)志符
__int16 i16_len = (__int16)ji_len;//讀出的數(shù)據(jù)長(zhǎng)度,值為3,即3個(gè)HEX形式的字符
__int16 i16_result;//函數(shù)返回值
__int16 i16_coverResult;//字符轉(zhuǎn)換函數(shù)的返回值
int i_temp;//用于循環(huán)的中間變量
jchar jca_result[3] = { 'e', 'r', 'r' };//當(dāng)讀數(shù)據(jù)錯(cuò)誤時(shí)返回此字符串
//無符號(hào)字符指針,指向的內(nèi)存空間用于存放讀出的HEX形式的數(shù)據(jù)字符串
unsigned char* uncp_hex_passward = (unsigned char*)malloc( i16_len );
//無符號(hào)字符指針,指向的內(nèi)存空間存放從HEX形式轉(zhuǎn)換為ASC形式的數(shù)據(jù)字符串
unsigned char* uncp_asc_passward = (unsigned char*)malloc( i16_len * 2 );
//java char指針,指向的內(nèi)存空間存放從存放ASC形式數(shù)據(jù)字符串空間讀出的數(shù)據(jù)字符串
jchar *jcp_data = (jchar*)malloc(i16_len*2+1);
//java String,存放從java char數(shù)組生成的String字符串,并返回給調(diào)用者
jstring js_data = 0;
//*********讀出3個(gè)HEX形式的數(shù)據(jù)字符到uncp_hex_data指定的內(nèi)存空間**********
i16_result = readData( H_icdev, 6, uncp_hex_data );//這里直接調(diào)用的是設(shè)備商提供的原型方法.
if ( i16_result != 0 )
{
printf( "讀卡錯(cuò)誤......\n" );
//這個(gè)地方調(diào)用JNI定義的方法NewString(jchar*,jint),把jchar字符串轉(zhuǎn)換為JString類型數(shù)據(jù),返回到j(luò)ava程序中即是String
return (env)->NewString( jca_result, 3 );
}
printf( "讀數(shù)據(jù)成功......\n" );
//**************HEX形式的數(shù)據(jù)字符串轉(zhuǎn)換為ASC形式的數(shù)據(jù)字符串**************
i16_coverResult = hex_asc( uncp_hex_data, uncp_asc_data, 3 );
if ( i16_coverResult != 0 )
{
printf( "字符轉(zhuǎn)換錯(cuò)誤!\n" );
return (env)->NewString( jca_result, 3 );
}
//**********ASC char形式的數(shù)據(jù)字符串轉(zhuǎn)換為jchar形式的數(shù)據(jù)字符串***********
for ( i_temp = 0; i_temp < i16_len; i_temp++ )
jcp_data[i_temp] = uncp_hex_data[i_temp];
//******************jchar形式的數(shù)據(jù)字符串轉(zhuǎn)換為java String****************
js_data = (env)->NewString(jcp_data,i16_len);
return js_data;
}
動(dòng)態(tài)調(diào)用,不需要lib文件,直接加載A.dll文件,并把其中的文件再次聲明,代碼如下:
#include<STDIO.H>
#include<WINDOWS.H>
#include "test_LinkDll.h"
//首先聲明一個(gè)臨時(shí)方法,這個(gè)方法名可以隨意定義,但參數(shù)同設(shè)備商提供的原型方法的參數(shù)保持一致.
typedef int ( *readDataTemp )( int, int, int, unsigned char * );//從指定地址讀數(shù)據(jù)
//從指定地址讀數(shù)據(jù)
JNIEXPORT jstring JNICALL Java_readDataTemp( JNIEnv *env, jobject jo, jint ji_icdev, jint ji_offset, jint ji_len )
{
int i_temp;
int i_result;
int i_icdev = (int)ji_icdev;
int i_offset = (int)ji_offset;
int i_len = (int)ji_len;
jchar jca_result[5] = { 'e', 'r', 'r' };
unsigned char *uncp_data = (unsigned char*)malloc(i_len);
jchar *jcp_data = (jchar *)malloc(i_len);
jstring js_data = 0;
//HINSTANCE是win32中同HANDLE類似的一種數(shù)據(jù)類型,意為Handle to an instance,常用來標(biāo)記App實(shí)例,在這個(gè)地方首先把A.dll加載到內(nèi)存空間,以一個(gè)App的形式存放,然后取
得它的instance交給dllhandle,以備其它資源使用.
HINSTANCE dllhandle;
dllhandle = LoadLibrary( "A.dll" );
//這個(gè)地方首先定義一個(gè)已聲明過的臨時(shí)方法,此臨時(shí)方法相當(dāng)于一個(gè)結(jié)構(gòu)體,它和設(shè)備商提供的原型方法具有相同的參數(shù)結(jié)構(gòu),可互相轉(zhuǎn)換
readDataTemp readData;
//使用win32的GetProcAddress方法取得A.dll中定義的名為readData的方法,并把這個(gè)方法轉(zhuǎn)換為已被定義好的同結(jié)構(gòu)的臨時(shí)方法,
//然后在下面的程序中,就可以使用這個(gè)臨時(shí)方法了,使用這個(gè)臨時(shí)方法在這時(shí)等同于使用A.dll中的原型方法.
readData = (readDataTemp) GetProcAddress( dllhandle, "readData" );
i_result = (*readData)( i_icdev, i_offset, i_len, uncp_data );
if ( i_result != 0 )
{
printf( "讀數(shù)據(jù)失敗......\n" );
return (env)->NewString( jca_result, 3 );
}
for ( i_temp = 0; i_temp < i_len; i_temp++ )
{
jcp_data[i_temp] = uncp_data[i_temp];
}
js_data = (env)->NewString( jcp_data, i_len );
return js_data;
}
4>以上即是一個(gè)java程序調(diào)用第三方dll文件的完整過程,當(dāng)然,在整個(gè)過程的工作全部完成以后,就可以使用java類LinkDll中的public String radData( int, int, int )方法了,效果同直接使用c/c++調(diào)用這個(gè)設(shè)備商提供的A.dll文件中的readData方法幾乎一樣.
總結(jié):JNI技術(shù)確實(shí)是提高了java程序的執(zhí)行效率,并且擴(kuò)展了java程序的功能,但它也確確實(shí)實(shí)破壞了java程序的最重要的優(yōu)點(diǎn):平臺(tái)無關(guān)性,所以除非必須(不得不)使用JNI技術(shù),一般還是提倡寫100%純java的程序.根據(jù)自己的經(jīng)驗(yàn)及查閱的一些資料,把可以使用JNI技術(shù)的情況羅列如下:
1>需要直接操作物理設(shè)備,而沒有相關(guān)的驅(qū)動(dòng)程序,這時(shí)候我們可能需要用C甚至匯編語(yǔ)言來編寫該設(shè)備的驅(qū)動(dòng),然后通過JNI調(diào)用;
2>涉及大量數(shù)學(xué)運(yùn)算的部分,用java會(huì)帶來些效率上的損失;
3>用java會(huì)產(chǎn)生系統(tǒng)難以支付的開銷,如需要大量網(wǎng)絡(luò)鏈接的場(chǎng)合;
4>存在大量可重用的c/c++代碼,通過JNI可以減少開發(fā)工作量,避免重復(fù)開發(fā).
另外,在利用JNI技術(shù)的時(shí)候要注意以下幾點(diǎn):
1>由于Java安全機(jī)制的限制,不要試圖通過Jar文件的方式發(fā)布包含本地化方法的Applet到客戶端;
2>注意內(nèi)存管理問題,雖然在本地方法返回Java后將自動(dòng)釋放局部引用,但過多的局部引用將使虛擬機(jī)在執(zhí)行本地方法時(shí)耗盡內(nèi)存;
3>JNI技術(shù)不僅可以讓java程序調(diào)用c/c++代碼,也可以讓c/c++代碼調(diào)用java代碼.
注:有一個(gè)名叫Jawin開源項(xiàng)目實(shí)現(xiàn)了直接讀取第三方dll文件,不用自己辛苦去手寫一個(gè)起傳值轉(zhuǎn)換作用的dll文件,有興趣的可以研究一下.但是我用的時(shí)候不太順手,有很多規(guī)則限制,像自己寫程序時(shí)可以隨意定義返回值,隨意轉(zhuǎn)換類型,用這個(gè)包的話這些都是不可能的了,所以我的項(xiàng)目還沒開始就把它拋棄了.