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
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章