JavaSE(十三)跨語言調用

JNI(Java native interface)

JNI簡介

  JNI即爲Java本地方法接口,目的是爲了實現Java語言與操作系統動態庫之間的交互。

Java的平臺無關性

  Java是一種與軟硬件平臺無關的語言,之所以能實現這一點,是因爲與平臺相關的操作都由JRE屏蔽了,爲了屏蔽平臺相關的操作,JRE本身是平臺相關的,所以不同的平臺需要安裝相應運行平臺的JRE(JRE包含JVM與Java自帶的標準包)。

JNI用途

  通過程序對操作系統進行操作,須要通過操作系統提供的系統調用,而操作系統提供的系統調用幾乎都不是Java所寫,所以Java自帶的標準包中,應該有很多是提供給Java程序調用的Java接口,而實現的語言卻是與操作系統有關的,如用C語言實現。然而對於一些針對具體硬件有擴展的操作系統(如提供了全新的或新增了系統調用),Java自帶的標準包沒有相應的接口能夠對操作系統這些擴展的功能進行操作,我們就要自己負責實現,通過Java語言調用非Java語言實現的系統調用。Java語言與本地動態庫交互調用的技術就是JNI技術(其中包括Java調用本地動態庫方法以及動態庫調用java實現的方法,但主要是前者)
  在Java代碼中,使用了JNI技術就意味着失去了Java的平臺無關性(當然,你的擴展若能被JRE認可併成爲JRE的一部分,那就另當別論了),所以使用JNI時需要慎之又慎,可能使用到JNI技術的大概有以下場景:

  1. 需要訪問的系統特徵和設備在JDK中沒有提供api(在嵌入式系統中比較常見,如基於Android系統的特殊定製的手機)。
  2. 出於代碼安全性考慮,最核心的代碼可能用其他語言實現,因爲Java的class文件能很輕易地被反編譯成Java源碼,而很難通過二進制碼得到源語言的源碼。
  3. 性能要求,如果實現相同功能的Java代碼比其他語言編寫的代碼要慢很多,而業務上對性能的要求Java無法滿足(如多媒體處理、遊戲邏輯等)。
  4. 對於某些算法或模塊,其他語言有成熟的、穩定的實現庫,而Java沒有,避免耗費大量的開發和測試資源。

C語言動態庫

  c語言通過gcc編譯成動態庫,命令爲gcc -shared -fPIC -o libxx.so -L 被依賴庫路徑 -l 被依賴庫名 xx1.c xx2.c,-l選項可以沒有,這樣就不知道編譯出來的動態庫到底依賴了哪些庫,如果用-l指定了被依賴的庫,那麼編譯出的動態庫就覺得自己依賴了那些庫,可能實際上並不依賴這些庫。ldd libxx.so就可以查看動態庫依賴了哪些其他庫,當然只能看見-l指定的那些庫。加載動態庫有兩種方式,一種是啓動時加載,一種是運行時加載
  啓動時加載需要在啓動時加載所有依賴的動態庫,所以程序應該知道自己依賴了哪些庫,這就要在鏈接的過程中指定被依賴的動態庫。爲了保證這些被依賴的庫是完整的,這就要求在鏈接的時候指定的庫能夠保證沒有未定義的符號,如果出現的符號在動態庫中無法找到,就不能完成鏈接過程。當a程序依賴b庫,而b庫依賴c庫時,如果ldd b庫找到了c庫,那麼鏈接時只需要指定b庫,ld a程序就會找到c庫和b庫,如果ldd b庫找不到c庫,那麼鏈接時必須同時指定b庫和c庫。啓動時加載的庫,其中的符號對程序和其他動態庫都是可見的。
  運行時加載動態庫對於動態庫中函數的調用全部使用指針,所以鏈接的時候不存在符號,便無需在鏈接的時候指定動態庫,但需要鏈接標準庫中的dl庫,dl庫提供了動態加載庫的方法。運行時加載加載動態庫的方法爲void *dlopen(const char *libpath, int mode),當加載成功時返回動態庫操作的句柄,否則返回NULL。mode主要分爲兩組,第一組爲RTLD_LAZY和RTLD_NOW,另一組爲RTLD_GLOBAL和RTLD_LOCAL,組內選項是互斥的,多組選擇用或運算符連接。RTLD_LAZY表示在加載動態庫的時候不解析動態庫中未定義的符號(當程序調用該動態庫的某個方法時,如果這個方法沒有使用被依賴庫的符號,依然可以,如果使用了,就會查看其他帶RTLD_GLOBAL打開的動態庫中的符號,如果依然找不到就會報錯),RTLD_NOW表示在加載動態庫的時候解析出所有符號,存在着無法解析的符號就會返回NULL。RTLD_GLOBAL表示加載的動態庫中的符號對於其他庫是可見的,RTLD_LOCAL表示加載的動態庫中的符號對於其他庫不可見。在加載某個庫的時候會同時加載該庫依賴的庫(ldd可查的依賴庫),找不到庫或找不到被依賴的庫都返回NULL。使用完動態庫可以用int dlclose(void * handle)卸載動態庫,真正地卸載成功返回0。對於某個符號的指針可以通過void * dlsym(void * handle, const char *symbol)獲取,返回值可能是變量或者函數的指針,然後就可以通過符號調用函數了。

JNI接口的定義和調用

  JNI接口的定義是很簡單的,跟抽象函數的定義相似,只有函數頭聲明,沒有函數體,聲明的函數用native關鍵字修飾即可
  JNI接口的調用也比較簡單,跟普通接口的調用幾乎完全一致(實際上我們平時調用的很多JRE的接口就是JNI接口),只不過在調用JNI接口之前,一定要加載實現了被調用接口的動態庫(調用JRE中的JNI接口之所以不用加載相應的動態庫,我想是在虛擬機啓動的時候就自動加載了這些動態庫或者在定義這些JNI接口的類的靜態塊中加載了這些動態庫)。個人建議在定義JNI接口的類的靜態塊中加載相應的動態庫,這樣在調用JNI接口時就完全跟調用普通接口一樣了。當然,可能定義JNI接口的類中還定義了其他非JNI接口,甚至這些類中大部分接口都是非JNI接口,而應用根本只調用到了非JNI接口,這時加載的動態庫雖然沒有用到,但一樣消耗了系統資源,會造成資源浪費,儘管如此,大多數情況,依然推薦在定義JNI接口的類的靜態塊中加載動態庫,也可以把native方法全部設置爲private的,然後對外提供一個代理方法,在代理方法中先加載庫,然後調用native方法。
  加載動態庫的方法爲System類的兩個靜態方法任選其一,分別爲System.load(String path)和System.loadLibrary(String libraryName), path爲動態庫的絕對路徑,加載時虛擬機會根據該絕對路徑來尋找動態庫,而libraryName爲動態庫的名稱(windows下的xx.dll,linux下爲libxx.so的動態庫名稱爲xx),虛擬機會在System.getProperty(“java.library.path”)的路徑中尋找名爲librarayName的動態庫。
  關於依賴加載,比如a庫依賴b庫,當ldd a庫不可見b庫的時候,只會加載a庫,所以在後面不能使用a庫中依賴b庫的方法,就算在之前手動加載過b庫也不行,因爲底層加載動態庫是指定的RTLD_LOCAL模式。當ldd a庫可見b庫的時候,加載完a庫會繼續加載b庫(b庫必須也在java.library.path路徑下),這樣就可以使用a庫中的所有方法。建議將所有自定義的動態庫都放在一個目錄下通過System.loadLibraray來加載
  jni動態庫的操作,底層c語言實現是使用dl庫運行時加載的方法。loadLibrary底層就是調用了dlopen,模式爲RTLD_LAZY | RTLD_LOCAL,緊接着調用dlsym獲取JNI_OnLoad方法,如果存在該方法就調用之(在虛擬機關閉時調用JNI_OnUnload方法,所以可以在JNI_OnUnload中做資源釋放)。

底層代碼的幾個關鍵數據結構

  JNI數據類型、java數據類型與類型描述符對於關係如下表:

Java數據類型 JNI數據類型 類型描述符 說明
boolean jboolean Z JNI_TRUE與JNI_FALSE
byte jbyte B
char jchar C
short jshort S
int jint I
long jlong J
float jfloat F
double jdouble D
void void V
java.lang.String jstring 同java類
java.lang.Class jclass 同java類
java類 jobject L+類全名+; 類全名之間以’/'分割
Xxx[] jxxxArray [+各元素類型描述符 n維數組用連續n個[表示,父類爲jarray
方法 jmethodID (參數值列表) 返回值描述

  在編寫底層代碼的時候(一般是C與C++語言),有幾個重要的數據結構定義在jni.h文件中,下面先做個介紹。
  表示java中native方法與動態庫本地方法的對應關係的結構體:

typedef struct {
    char *name; // java的native方法名
    char *signature; // java的native方法的簽名(類型描述符中的方法)
    void *fnPtr; // 底層函數實現在進程空間的虛擬地址
} JNINativeMethod;

  表示java虛擬機的結構體JavaVM(C與C++定義不同),一個進程對應一個JavaVM實體:

#ifdef __cplusplus
typedef JavaVM_ JavaVM; // 針對C++
#else
typedef const struct JNIInvokeInterface_ *JavaVM; // 針對C
#endif

struct JNIInvokeInterface_ {
    jint (JNICALL *DestroyJavaVM)(JavaVM *vm); // 摧毀虛擬機

    jint (JNICALL *GetEnv)(JavaVM *vm, void **penv, jint version); // 獲取JNI環境,通過參數penv得到返回值,成功返回JNI_OK
};

struct JavaVM_ {
    const struct JNIInvokeInterface_ *functions;
#ifdef __cplusplus
    jint DestroyJavaVM() {
        return functions->DestroyJavaVM(this);
    }
    ... // JNIInvokeInterface_ 的方法這裏都有快捷調的方法
#endif
};

  還有表示JNI環境的數據接哦古JNIEnv(C與C++定義不同),一個線程對應一個JNIEnv實體:

#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif

struct JNINativeInterface_ {
    jint (JNICALL *GetVersion)(JNIEnv *env); // 獲取JNI版本,值爲JNI_VERSION_1_6、JNI_VERSION_1_8、JNI_VERSION_9、JNI_VERSION_10等
    jint (JNICALL *GetJavaVM)(JNIEnv *env, JavaVM **vm); // 獲取虛擬機

    jclass (JNICALL *FindClass)(JNIEnv *env, const char *name); // 根據類的全路徑名獲取java類
    jclass (JNICALL *GetSuperclass)(JNIEnv *env, jclass sub); // 根據子類獲取父類
    jboolean (JNICALL *IsAssignableFrom)(JNIEnv *env, jclass sub, jclass sup); // sub類是否可以安全地強轉爲sup類
    jclass (JNICALL *GetObjectClass)(JNIEnv *env, jobject obj); // 獲取某個對象所屬類
    jboolean (JNICALL *IsInstanceOf)(JNIEnv *env, jobject obj, jclass clazz); // 某個實體是否屬於某類
    
    jint (JNICALL *RegisterNatives)(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods); // 註冊methods數組
    jint (JNICALL *UnregisterNatives)(JNIEnv *env, jclass clazz); // 註銷
    
    jobject (JNICALL *NewObject)(JNIEnv *env, jclass clazz, jmethodID methodID, ...); // 調用指定的構造方法創建對象
    jobject (JNICALL *NewObjectV)(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
    jobject (JNICALL *NewObjectA)(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
    
    jmethodID (JNICALL *GetMethodID)(JNIEnv *env, jclass clazz, const char *name, const char *sig); // 獲取類的某個非靜態方法Id,sig爲方法類型描述符
    jmethodID (JNICALL *GetStaticMethodID)(JNIEnv *env, jclass clazz, const char *name, const char *sig); // 獲取類的某個靜態方法Id
    jxxx (JNICALL *CallXxxMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); // 調用對象的非靜態方法,xxx可取object、void、int等基本類型
    jxxx (JNICALL *CallXxxMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
    jxxx (JNICALL *CallXxxMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
    jxxx (JNICALL *CallStaticXxxMethod)(JNIEnv *env, jclass clazz, jmethodID methodID, ...); // 調用類的靜態方法,xxx可取object、void、int等基本類型
    jxxx (JNICALL *CallStaticXxxMethodV)(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
    jxxx (JNICALL *CallStaticXxxMethodA)(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
    
    jfieldID (JNICALL *GetFieldID)(JNIEnv *env, jclass clazz, const char *name, const char *sig); // 獲取某個類的某個非靜態域的Id,sig爲域類型描述符,獲取構造器方法name爲"<init>",sig的返回值爲void
    jfieldID (JNICALL *GetStaticFieldID)(JNIEnv *env, jclass clazz, const char *name, const char *sig); // 獲取某個類的某個靜態域的Id
    jxxx (JNICALL *GetXxxField)(JNIEnv *env, jobject obj, jfieldID fieldID); // 獲取某個對象的某個域,xxx可取object、int等基本類型
    jxxx (JNICALL *GetStaticXxxField)(JNIEnv *env, jclass clazz, jfieldID fieldID); // 獲取類的某個靜態域,xxx可取object、int等基本類型
    void (JNICALL *SetXxxField)(JNIEnv *env, jobject obj, jfieldID fieldID, jxxx val); // 設置某個對象的域
    void (JNICALL *SetStaticXxxField)(JNIEnv *env, jclass clazz, jfieldID fieldID, jxxx val); // 設置某個類的靜態域
    
    jstring (JNICALL *NewString)(JNIEnv *env, const jchar *unicode, jsize len); // 以utf-16方式創建字符串
    jstring (JNICALL *NewStringUTF)(JNIEnv *env, const char *utf); // 以utf-8方式創建字符串
    jsize (JNICALL *GetStringLength)(JNIEnv *env, jstring str); 
    jsize (JNICALL *GetStringUTFLength)(JNIEnv *env, jstring str);
    const jchar *(JNICALL *GetStringChars)(JNIEnv *env, jstring str, jboolean *isCopy); // 將jstring類型轉化爲jchar數組以便c語言處理
    const char* (JNICALL *GetStringUTFChars)(JNIEnv *env, jstring str, jboolean *isCopy);
    void (JNICALL *ReleaseStringChars)(JNIEnv *env, jstring str, const jchar *chars); // 釋放字符串,當調用了GetStringChars後應該釋放
    void (JNICALL *ReleaseStringUTFChars)(JNIEnv *env, jstring str, const char* chars);

    jsize (JNICALL *GetArrayLength)(JNIEnv *env, jarray array);
    jobjectArray (JNICALL *NewObjectArray)(JNIEnv *env, jsize len, jclass clazz, jobject init);
    jxxxArray (JNICALL *NewXxxArray)(JNIEnv *env, jsize len);
    jobject (JNICALL *GetObjectArrayElement)(JNIEnv *env, jobjectArray array, jsize index);
    jxxx * (JNICALL *GetBooleanArrayElements)(JNIEnv *env, jbooleanArray array, jboolean *isCopy); // 獲取的是數組的頭指針
    void (JNICALL *SetObjectArrayElement)(JNIEnv *env, jobjectArray array, jsize index, jobject val);

    jint (JNICALL *Throw)(JNIEnv *env, jthrowable obj); // 拋出異常,成功返回JNI_OK,此函數之後應該return,在調用函數中檢查是否有異常
    jint (JNICALL *ThrowNew)(JNIEnv *env, jclass clazz, const char *msg); // 拋出某一類型異常並附帶msg,成功返回JNI_OK
    jthrowable (JNICALL *ExceptionOccurred)(JNIEnv *env); // 
    jboolean (JNICALL *ExceptionCheck)(JNIEnv *env); // 是否有異常
    void (JNICALL *ExceptionDescribe)(JNIEnv *env); // 將異常信息推送到錯誤流
    void (JNICALL *ExceptionClear)(JNIEnv *env); // 清除異常,處理異常後應該清除
    void (JNICALL *FatalError)(JNIEnv *env, const char *msg);
};

struct JNIEnv_ {
    const struct JNINativeInterface_ *functions;
#ifdef __cplusplus
    jclass FindClass(const char *name) {
        return functions->FindClass(this, name);
    }
    ... // JNINativeInterface_ 的方法這裏都有快捷調的方法
#endif    
}

native函數的註冊

  虛擬機中保存着一組已註冊的native方法,他可以根據native方法直接找到對應的本地函數的地址,當java調用某個native方法時,就會在已註冊的native方法中找到相應的本地函數的地址並調用,如果找不到,就根據一定的映射規則在所有已加載的動態庫中(加載的動態庫返回的句柄都被統一的管理着)尋找相應方法,找不到就拋異常,找到了就調用並將之也註冊進去,這樣下次調用就可以在已註冊的native方法中找到它。
  jni方法的註冊分爲主動註冊和被動註冊,被動註冊就是在調用native方法的時候如果該方法沒有註冊就去已加載的動態庫中根據一定的對應規則找到相應的方法並進行註冊。主動註冊就是在註冊表中直接註冊,因爲加載庫的時候調用了JNI_OnLoad方法,所以主動註冊就可以在JNI_OnLoad中註冊。
  主動註冊是通過JNIEnv結構體中的一個函數指針 JNIEnv結構體中有一個函數指針名字叫RegisterNatives,這就是進行native方法註冊的方法,該方法可以一次註冊同一個類的numMethods個native方法,被註冊的native函數在數組gMethods中說明。
  被動註冊最關鍵就是找到native方法對應的實現方法。這個對應關係就是native方法名與動態庫方法的簽名,動態庫方法的簽名=Java_類所在完整包名用(用_分割)_類名_方法名,如當調用com.ejie.risk包下ClassA類中的native方法func1方法時,動態庫中函數簽名爲Java_com_ejie_risk_ClassA_func1方法將會被調用。對於有多個重載方法被native修飾時(多個重載方法中只有一個native方法時不適用),需要通過參數來反映動態庫中的函數簽名,參數簽名前用雙下劃線隔開,動態庫方法的簽名=Java_類所在完整包名用(用_分割)_類名_方法名__所有參數簽名直接相連,對於無參數的函數,參數簽名爲空,對於基本類型簽名,有對應關係,如int爲I,long爲J,double爲D等,對象簽名爲L類全路徑用下劃線隔開_2,如下舉例:

com.ejie.risk.ClassA類下的native方法 對應動態庫下的方法簽名
func2() Java_com_ejie_risk_ClassA_func2__
func2(int a, long b, double c) Java_com_ejie_risk_ClassA_func2__IJD
func2(ClassA a, String b) Java_com_ejie_risk_ClassA_func2__Lcom_ejie_risk_ClassA_2Ljava_lang_String_2
func2(int i, String s) Java_com_ejie_risk_ClassA_func2__ILjava_lang_String_2

JNI動態庫的實現與動態庫反調java代碼

  JNI調用的動態庫可以是多種語言實現的(因爲JNI是和動態庫交互,不是和編程語言交互,不同語言生成動態庫的性質是相同的,只是不同語言生成的動態庫的方法的簽名和各種編程語言的編譯器有關),但必須保證生成的動態庫中方法的簽名要和java的native方法保持對應關係。由於java的native方法是通過動態庫中的方法簽名來尋找對應關係的,這就要求生成動態庫的源語言中的方法命名有講究,須滿足根據源語言方法生成的動態庫中的方法簽名要是native方法希望的。
  動態庫加載時自動調用的方法JNI_OnLoad,其方法頭爲JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserve),其中返回值爲支持的最低版本。
  native方法對應的底層方法的第一個參數爲JNIEnv *env,第二個參數爲jclass或者jobject(視是否爲static方法而定),後面就依次爲參數。如JNIEXPORT jint JNICALL Java_Math_sub(JNIEnv *, jobject, jint, jint); 對應的就是Math類中的非靜態方法native int sub(int a, int b)。

腳本執行

解析執行

  Java定義了與腳本語言進行交互的方式(框架與SPI),但這需要相應的腳本引擎支持(針對特定腳本的庫,是對SPI的實現)
  腳本引擎工廠ScriptEngineFactory可用來創建腳本引擎實例,它包含了名字(如JavaScript、groovy)、MimeType(如application/javascript、text/javascript)和擴展(一般是文件擴展名如js、groovy)三個重要元素,每一個元素都可以有多個實例,如多個名字。ScriptEngineManager可以通過SPI服務發現機制來對腳本引擎工廠進行註冊,也提供了手動註冊腳本引擎工廠的接口,這兩種註冊機制的架構是不一樣的。ScriptEngineManager還提供了獲取腳本引擎ScriptEngine的接口(實際上是先獲取到腳本引擎工廠,再通過工廠獲取到的腳本引擎),有了腳本引擎就可以與腳本進行交互。
  腳本引擎通過其eval(…)方法來進行腳本的解析執行,具體解析方案與腳本引擎有關(腳本引擎應該按腳本語言規範進行設計)。對於解釋性語言,一般遵循以下解釋執行的方案:對於單個腳本的解析執行,先解析整個腳本,當解析到符號定義(類、函數和全局變量)時腳本引擎會對符號進行記憶,緊接着會執行全局代碼;同一個引擎可以對多個腳本進行解析執行(解析腳本1,執行腳本1,解析腳本2,執行腳本2…),後解析出的符號會覆蓋先解析出的同名符號,後解析的腳本在執行過程中,可以使用先解析的腳本中的符號(因爲解析出的符號是以腳本引擎爲單位的)。

public class ScriptEngineManager  {
    public ScriptEngineManager(); // 在創建ScriptEngineManager實例的過程中會進行SPI的服務發現,並將發現的腳本引擎工廠註冊到正在創建的ScriptEngineManager
    
    // 服務發現註冊引擎工廠與手動註冊的機制是分開的,其中手動註冊的優先級更高
    // 服務發現將所有發現的引擎工廠通過一個列表進行管理,在通過名字、MimeType或擴展獲取引擎工廠時,遍歷列表中各個工廠的這些屬性從而找到匹配的引擎工廠
    // 手動註冊會根據註冊時指定的名字、MimeType或擴展用三個Map分別進行管理,在通過這些要素獲取引擎工廠時,直接根據這對應Map的key來找到匹配的引擎工廠
    public List<ScriptEngineFactory> getEngineFactories(); // 基於服務發現已註冊的所有工廠
    public void registerEngineName(String name, ScriptEngineFactory factory); // 根據名字手動註冊引擎工廠
    public void registerEngineMimeType(String type, ScriptEngineFactory factory); // 根據MimeType手動註冊引擎工廠
    public void registerEngineExtension(String extension, ScriptEngineFactory factory); // 根據擴展手動註冊引擎工廠
    
    public ScriptEngine getEngineByName(String shortName); // 根據名字獲取引擎工廠並創建一個腳本引擎
    public ScriptEngine getEngineByMimeType(String mimeType); // 根據MimeType獲取引擎工廠並創建一個腳本引擎
    public ScriptEngine getEngineByExtension(String extension); // 根據擴展獲取引擎工廠並創建一個腳本引擎
    
    public void setBindings(Bindings bindings); // 設置全局Bindings
    public Bindings getBindings(); // 獲取全局Bindings,通過getEngineByXxx()獲取ScriptEngine的時候會將該Bindings會設置爲ScriptEngine的ScriptContext.GLOBAL_SCOPE下的Bindings
    public void put(String key, Object value); // 設置全局Bindings的鍵值
    public Object get(String key); // 獲取全局Bindings的鍵對應的值
}

// 腳本上下文主要用於控制引擎與腳本的交互
public interface ScriptContext {    
	// Bindings繼承自Map,用於與腳本中的全局變量進行綁定,key對應變量名,value對應變量值
    public List<Integer> getScopes(); // 獲取所有的作用範圍,默認有ScriptContext.NGINE_SCOPE與ScriptContext.GLOBAL_SCOPE
    public void setBindings(Bindings bindings, int scope); // 設置指定作用域的Bindings
    public Bindings getBindings(int scope); // 獲取指定作用域的Bindngs
    public void setAttribute(String name, Object value, int scope); // 設置指定作用域的Bindings的key-value值
    public Object getAttribute(String name, int scope); // 獲取指定作用域的Bindings的key對應value
    public Object getAttribute(String name); // 獲取Bindings的key對應的value,先從ScriptContext.NGINE_SCOPE作用域找,找不到再從ScriptContext.GLOBAL_SCOPE作用域找
    public int getAttributesScope(String name); // 獲取包含key的Bindings所在的作用域(ScriptContext.NGINE_SCOPE作用域優先級比ScriptContext.GLOBAL_SCOPE高),所有作用域都找不到返回-1
    public Object removeAttribute(String name, int scope); // 移除指定作用域的Bindings下的指定key
    
    // 重定向腳本的輸入流、輸出流和錯誤流
    public Writer getWriter();
    public Writer getErrorWriter();
    public Reader getReader();
    public void setWriter(Writer writer);
    public void setErrorWriter(Writer writer);
    public void setReader(Reader reader);
}

// 用於與腳本進行交互
public interface ScriptEngine  {
	// 執行的腳本可以是腳本內容,也可以是包含腳本內容的Reader(含有編碼格式的流)
    public Object eval(String script, ScriptContext context); // 執行腳本並指定與腳本進行交互控制的腳本上下文
    public Object eval(Reader reader , ScriptContext context);
    public Object eval(String script); // eval(script, getContext())
    public Object eval(Reader reader);
    public Object eval(String script, Bindings n); // 對getContext()獲取的上下文進行克隆,並將克隆的上下文的ScriptContext.NGINE_SCOPE作用域的Bindings替換爲指定的n,用此克隆的上下文進行交互控制
    public Object eval(Reader reader , Bindings n);
    
    public ScriptContext getContext(); // 獲取該引擎的上下文
    public void setContext(ScriptContext context);
    
    // 操作ScriptContextContext的快捷方式
    public Bindings createBindings(); // 創建一個Bindings實例
    public Bindings getBindings(int scope); // getContext().getBindings(scope)
    public void setBindings(Bindings bindings, int scope); // getContext().setBindings(bindings, scope)
    public void put(String key, Object value); // getBindings(ScriptContext.NGINE_SCOPE).put(key, value)
    public Object get(String key); // getBindings(ScriptContext.ENGINE_SCOPE).get(key)
    
    public ScriptEngineFactory getFactory(); // 獲取創建該腳本引擎的引擎工廠
}

方法調用

  某些腳本引擎不但可以解析執行腳本,還可以執行已解析腳本中的函數(腳本引擎必須已經記憶了該函數,也就是對函數所在腳本已經進行了解析),這樣的腳本引擎一定實現了Invocable接口。
  通過Invocable不但可以直接調用腳本中的函數,還可以獲取到接口的實現類(實際上是一個代理,該代理將接口的方法與腳本中的方法進行綁定),之後通過該接口就可以調用到腳本方法。

public interface Invocable  {
	public Object invokeFunction(String name, Object... args); // 執行名爲name函數
	public Object invokeMethod(Object thiz, String name, Object... args); // 執行thiz對象的name方法
    public <T> T getInterface(Class<T> clasz); // T爲一個接口,會將接口T中的各個方法與已解析腳本中的同名全局函數進行綁定
	public <T> T getInterface(Object thiz, Class<T> clasz); // T爲一個接口,會將接口T中的各個方法與對象thiz代表的腳本中的對象的方法進行綁定
}

腳本編譯

  某些腳本引擎出於對執行效率的考慮,可以將腳本代碼編譯爲某種中間格式,這樣的腳本引擎一定實現了Compilable接口。通過已編譯腳本CompiledScript對象的eval(…)方法可以執行腳本,只有需要多次調用的腳本纔有進行編譯的必要

// 腳本引擎若實現了該接口,則腳本引擎能夠將腳本進行編譯
public interface Compilable {
    public CompiledScript compile(String script);
    public CompiledScript compile(Reader script);
}

// 已編譯的腳本
public abstract class CompiledScript {
    public Object eval(ScriptContext context) ; // 執行已編譯的腳本並指定與腳本進行交互控制的腳本上下文
    public Object eval(Bindings bindings); // 對getEngine().getContext()獲取的上下文進行克隆,並將克隆的上下文的ScriptContext.NGINE_SCOPE作用域的Bindings替換爲指定的bindings,用此克隆的上下文進行交互控制
    public Object eval() throws ScriptException; // eval(getEngine().getContext())
    public abstract ScriptEngine getEngine(); // 創建該已編譯腳本的腳本引擎,得到的腳本引擎一定實現了Compilable接口
}

代碼編譯

  Java源代碼必須通過編譯得到class文件格式後才能被JVM所識別,所以Java項目通常是以class文件格式進行發佈,但有時候也需要在運行過程中再對某些源代碼進行編譯,如JSP。
  在Java中,用接口JavaCompiler代表一個Java編譯器,ToolProvider的靜態方法getSystemToolClassLoader()可以基於服務發現機制獲取到JavaCompiler的實現類。JavaCompiler的run(…)方法可以像使用javac命令一樣編譯源代碼,通過這種方法來編譯源代碼非常簡單,但對編譯細節的控制力弱,爲了對編譯過程進行更強的控制,可以使用CompilationTask方案。CompilationTask方案首先根據JavaCompiler.getTask(…)方法來獲取到一個編譯任務,然後直接調用call方法來進行同步編譯,或將編譯任務傳遞給線程池來進行異步編譯,由於此方案的API非常複雜,一般又很少用到,這裏不再做更多的介紹。

public interface JavaCompiler extends Tool, OptionChecker {
	// 繼承自Tool,前三個流參數分別代表輸入、輸出和錯誤的重定向流,編譯不需要輸入流,所以in一定爲null,out和err爲null時默認爲標準輸入和標準出錯流
	// 參數arguments和javac命令的參數列表相同(不包括命令javac),編譯成功返回0,失敗返回非0
	int run(InputStream in, OutputStream out, OutputStream err, String... arguments); 
	
    CompilationTask getTask(Writer out,
                            JavaFileManager fileManager,
                            DiagnosticListener<? super JavaFileObject> diagnosticListener,
                            Iterable<String> options, // 選項迭代器,如Arrays.asList("-g", "-d")
                            Iterable<String> classes, // 在編譯過程中用於處理註解的所有類的類全名
                            Iterable<? extends JavaFileObject> compilationUnits); // 所有編譯單元,如本地編譯的文件列表
    StandardJavaFileManager getStandardFileManager(
    						DiagnosticListener<? super JavaFileObject> diagnosticListener, 
    						Locale locale, 
    						Charset charset);
   
    // 編譯任務,可以直接調用call方法來進行同步編譯,或將任務傳遞給線程池來進行異步編譯 						
    interface CompilationTask extends Callable<Boolean> {
        Boolean call(); // 編譯任務執行成功返回true,失敗返回false
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章