Java SE 6 新特性:本地方法的 Instrumentation
在 1.5 版本的 instumentation 里,并没有对 Java 本地方法(Native Method)的处理方式,而且在 Java 标准的 JVMTI 之下,并没有办法改变 method signature, 这就使替换本地方法非常地困难。一个比较直接而简单的想法是,在启动时替换本地代码所在的动态链接库 —— 但是这样,本质上是一种静态的替换,而不是动态的 Instrumentation。而且,这样可能需要编译较大数量的动态链接库 —— 比如,我们有三个本地函数,假设每一个都需要一个替换,而在不同的应用之下,可能需要不同的组合,那么如果我们把三个函数都编译在同一个动态链接库之中,最多我们需要 8 个不同的动态链接库来满足需要。当然,我们也可以独立地编译之,那样也需要 6 个动态链接库——无论如何,这种繁琐的方式是不可接受的。
在 Java SE 6 中,新的 Native Instrumentation 提出了一个新的 native code 的解析方式,作为原有的 native method 的解析方式的一个补充,来很好地解决了一些问题。这就是在新版本的 java.lang.instrument 包里,我们拥有了对 native 代码的 instrument 方式 —— 设置 prefix。
假设我们有了一个 native 函数,名字叫 nativeMethod,在运行中过程中,我们需要将它指向另外一个函数(需要注意的是,在当前标准的 JVMTI 之下,除了 native 函数名,其他的 signature 需要一致)。比如我们的 Java 代码是:
package nativeTester; class nativePrefixTester{ … native int nativeMethod(int input); … } |
那么我们已经实现的本地代码是:
jint Java_nativeTester_nativeMethod(jclass thiz, jobject thisObj, jint input); |
现在我们需要在调用这个函数时,使之指向另外一个函数。那么按照 J2SE 的做法,我们可以按他的命名方式,加上一个 prefix 作为新的函数名。比如,我们以 "another_" 作为 prefix,那么我们新的函数是:
jint Java_nativeTester_another_nativePrefixTester(jclass thiz, jobject thisObj, jint input); |
然后将之编入动态链接库之中。
现在我们已经有了新的本地函数,接下来就是做 instrument 的设置。正如以上所说的,我们可以使用 premain 方式,在虚拟机启动之时就载入 premain 完成 instrument 代理设置。也可以使用 agentmain 方式,去 attach 虚拟机来启动代理。而设置 native 函数的也是相当简单的:
premain(){ // 或者也可以在 agentmain 里 … if (!isNativeMethodPrefixSupported()){ return; // 如果无法设置,则返回 } setNativeMethodPrefix(transformer,"another_"); // 设置 native 函数的 prefix,注意这个下划线必须由用户自己规定 … } |
在这里要注意两个问题。一是不是在任何的情况下都是可以设置 native 函数的 prefix 的。首先,我们要注意到 agent 包之中的 Manifest 所设定的特性:
Can-Set-Native-Method-Prefix
要注意,这一个参数都可以影响是否可以设置 native prefix,而且,在默认的设置之中,这个参数是 false 的,我们需要将之设置成 true(顺便说一句,对 Manifest 之中的属性来说都是大小写无关的,当然,如果给一个不是“true”的值,就会被当作 false 值处理)。
当然,我们还需要确认虚拟机本身是否支持 setNativePrefix。在 Java API 里,Instrumentation 类提供了一个函数 isNativePrefix,通过这个函数我们可以知道该功能是否可以实行。
二是我们可以为每一个 ClassTransformer 加上它自己的 nativeprefix;同时,每一个 ClassTransformer 都可以为同一个 class 做 transform,因此对于一个 Class 来说,一个 native 函数可能有不同的 prefix,因此对这个函数来说,它可能也有好几种解析方式。
在 Java SE 6 当中,Native prefix 的解释方式如下:对于某一个 package 内的一个 class 当中的一个 native method 来说,首先,假设我们对这个函数的 transformer 设置了 native 的 prefix“another”,它将这个函数接口解释成:
由 Java 的函数接口
和上述 prefix"another",去寻找本地代码中的函数
void Java_package_class_another_method(jclass theClass, jobject thiz); // 请注意 prefix 在函数名中出现的位置! |
一旦可以找到,那么调用这个函数,整个解析过程就结束了;如果没有找到,那么虚拟机将会做进一步的解析工作。我们将利用 Java native 接口最基本的解析方式,去找本地代码中的函数:
void Java_package_class_method(jclass theClass, jobject thiz); |
如果找到,则执行之。否则,因为没有任何一个合适的解析方式,于是宣告这个过程失败。
那么如果有多个 transformer,同时每一个都有自己的 prefix,又该如何解析呢?事实上,虚拟机是按 transformer 被加入到的 Instrumentation 之中的次序去解析的(还记得我们最基本的 addT???椠?? ransformer 方法吗?)。
假设我们有三个 transformer 要被加入进来,他们的次序和相对应的 prefix 分别为:transformer1 和“prefix1_”,transformer2 和 “prefix2_”,transformer3 和 “prefix3_”。那么,虚拟机会首先做的就是将接口解析为:
native void prefix1_prefix2_prefix3_native_method() |
然后去找它相对应的 native 代码。
但是如果第二个 transformer(transformer2)没有设定 prefix,那么很简单,我们得到的解析是:
native void prefix1_prefix3_native_method() |
这个方式简单而自然。
当然,对于多个 prefix 的情况,我们还要注意一些复杂的情况。比如,假设我们有一个 native 函数接口是:
native void native_method() |
然后我们为它设置了两个 prefix,比如 "wrapped_" 和 "wrapped2_",那么,我们得到的是什么呢?是
void Java_package_class_wrapped_wrapped2_method(jclass theClass, jobject thiz); // 这个函数名正确吗? |
吗?答案是否定的,因为事实上,对 Java 中 native 函数的接口到 native 中的映射,有一系列的规定,因此可能有一些特殊的字符要被代入。而实际中,这个函数的正确的函数名是:
void Java_package_class_wrapped_1wrapped2_1method(jclass theClass, jobject thiz); // 只有这个函数名会被找到 |
很有趣不是吗?因此如果我们要做类似的工作,一个很好的建议是首先在 Java 中写一个带 prefix 的 native 接口,用 javah 工具生成一个 c 的 header-file,看看它实际解析得到的函数名是什么,这样我们就可以避免一些不必要的麻烦。
另外一个事实是,与我们的想像不同,对于两个或者两个以上的 prefix,虚拟机并不做更多的解析;它不会试图去掉某一个 prefix,再来组装函数接口。它做且仅作两次解析。
总之,新的 native 的 prefix-instrumentation 的方式,改变了以前 Java 中 native 代码无法动态改变的缺点。在当前,利用 JNI 来写 native 代码也是 Java 应用中非常重要的一个环节,因此它的动态化意味着整个 Java 都可以动态改变了 —— 现在我们的代码可以利用加上 prefix 来动态改变 native 函数的指向,正如上面所说的,如果找不到,虚拟机还会去尝试做标准的解析,这让我们拥有了动态地替换 native 代码的方式,我们可以将许多带不同 prefix 的函数编译在一个动态链接库之中,而通过 instrument 包的功能,让 native 函数和 Java 函数一样动态改变、动态替换。
当然,现在的 native 的 instrumentation 还有一些限制条件,比如,不同的 transformer 会有自己的 native prefix,就是说,每一个 transformer 会负责他所替换的所有类而不是特定类的 prefix —— 因此这个粒度可能不够精确。