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