并非所有的开发者都清楚,时下最流行的两个程序运行环境(Java虚拟机JVM和.NET通用语言运行时CLR)事实上就是一组共享的类库。不论是JVM还是CLR,都为程序代码的执行提供了各种所需的功能服务,这其中包括内存管理、线程管理、代码编译(或Java特有的即时编译JIT)等等。由于这些特性的存在,在一个操作系统中,如果程序同时运行在JVM和CLR两种环境之上,由于任何一个进程都可以加载与之对应的任何共享类库,这使得相应的操作将变得非常繁琐。
然而,当话题讨论到这些问题的时候,大多数开发者都会停下来,向一侧仰着头,非常认真的问道“可是……这样的互操作对我们来说究竟有什么用?”
近些年来,基于Java平台的程序开发,一直都有为数众多的API类库和新技术为其提供强大的支持。与此同时,.NET的通用语言运行时CLR,天生就具备Windows操作系统所提供的那些丰富的编程支持。在Windows操作系统环境下,常有许多Windows编程中易于实现的功能目前却很难使用Java语言编程实现,然而有的时候,使用Java语言实现特定功能较之Windows编程却更为简洁。这是在Java编程中,使用Java本地接口JNI技术实现互操作时的通常看法,同时这对于Java的开发者来说也应当是非常熟悉。可能会让开发者感觉有所陌生的,是那些尝试在Java虚拟机中实现.NET编程语言特性的想法,例如在最新的.NET 3.0中,包含工作流、WPF和InfoCard等广受关注的特性,或是在.NET过程中使用Java虚拟机提供的工具,比如说部署Java语言编写的那些包含复杂业务逻辑的Spring组件,或者实现通过ASP.NET访问JMS消息队列这样的功能。
加载动态链接库以及与底层代码托管环境进行交互,是解决互操作问题所面临的两个不同问题,然而,每一项操作都为之提供了标准的应用程序接口来完成这样的功能。举例来说,下面列出的非托管C++代码来自于Java本地接口JNI的官方文档,目的是利用标准过程(相关的代码句柄在JNIHosting子目录里以InProcInterop方案的一部分存在,构建它的最好方法是在命令行里用指向JDK 1.6目录位置的JAVA_HOME环境变量来操作。
)创建基于Java虚拟机的函数调用:
#include "stdafx.h" #include int _tmain(int argc, _TCHAR* argv[]) { JavaVM *jvm; /* 表示一个Java虚拟机 */ JNIEnv *env; /* 指向本地方法调用接口 */ JavaVMInitArgs vm_args; /* JDK或JRE 6的虚拟机初始化参数 */ JavaVMOption options[4]; int n = 0; options[n++].optionString = "-Djava.class.path=."; vm_args.version = JNI_VERSION_1_6; vm_args.nOptions = n; vm_args.options = options; vm_args.ignoreUnrecognized = false; /* 加载或初始化Java虚拟机,返回Java本地调用接口 * 指向变量 env */ JNI_CreateJavaVM(&jvm, (void**)&env, &vm_args); // 传入C++所需的参数 /* 使用Java本地接口调用 Main.test 方法 */ jclass cls = env->FindClass("Main"); jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V"); env->CallStaticVoidMethod(cls, mid, 100); /* 完成工作 */ jvm->DestroyJavaVM(); return 0; } |
在编译上述代码时,Java开发工具包JDK中的include和include\win32目录将被添加在C++程序的include路径中,并且JDK中lib目录下的jvm.lib必须位于目标代码连接器的路径之中。程序运行时,默认情况下程序的主类Main.class作为程序执行的入口类,与上述文件位于相同的目录之中,并且保证Java运行环境JRE中的jvm.dll动态链接库存在,一般来说这个动态链接库是存在于系统环境变量的PATH路径之中。(jvm.dll通常不需要手动添加在PATH路径中,因为java.exe将会动态的查找jvm.dll动态链接库的位置,并在找到链接库后记录下它的位置。)
同样,.NET通用语言运行时CLR提供自有的应用程序调用接口,作为本地API接口来实现同样的功能,代码如下:
#include "stdafx.h" #include int _tmain(int argc, _TCHAR* argv[]) { ICLRRuntimeHost* pCLR = (ICLRRuntimeHost*)0; HRESULT hr = CorBindToRuntimeEx(NULL, L"wks", STARTUP_CONCURRENT_GC, CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (PVOID*)&pCLR); if (FAILED(hr)) return -1; hr = pCLR->Start(); if (FAILED(hr)) return -1; DWORD retval = 0; hr = pCLR->ExecuteInDefaultAppDomain(L"HelloWorld.exe", L"Hello", L"Main", NULL, &retval); if (FAILED(hr)) return -1; hr = pCLR->Stop(); if (FAILED(hr)) return -1; return (int)retval; } |
当程序开发者使用非托管的C++代码编写应用成为可能,即可以加载CLR和JVM这两种不同的运行时环境来完成处理过程,这使得大部分业务逻辑的程序编写陷入开发者不敢去涉及的境地。然而吸引人的是,这可以作为锻炼编程技巧与能力的一种方式,对于我们大多数人,在这个过程中都会找到一系列的替代方案。
首先,比如说,CLR和JVM两种技术都支持非托管代码的“Calling Down”操作(在Java虚拟机中,被称作Java本地接口,而在.NET的CLR中,被称作P/Invoke调用),这样的机制使得开发者可以在其中一个运行环境下定义功能方法,通过少量的“Trampoline(弹簧床)”编码,将程序迁移到另一个运行时环境下编译执行。例如,在Java程序中,通过本地方法接口JNI实现函数的调用操作较为繁琐,并且需要记录配置文档(比如,可以参见Liang或者Gordon的书,或者JDK中JNI的文档。)。而在实现C++本地代码调用的过程中,较为繁琐的操作是使用微软Visual Studio 2005中提供的C++/CLI或Visual Studio 2003提供的C++托管代码,来进行代码编译的过程。
在这个步骤中,复杂之处在于程序运行时,需要确保Java虚拟机得到访问动态链接库的路径。这项工作可以分为两部分来完成:首先,当Java类函数的本地方法被程序加载时,需要询问Java虚拟机是否通过Runtime.loadLibrary()操作来请求加载共享库函数。值得注意的是,本地类库请求是在没有指定文件拓展名的情况下完成这样的操作。不指定拓展名,是因为不同的操作系统往往使用不同的约定来共享类库,所以只需指定共享类库名称即可。比如在Windows操作系统下,共享类库具有.DLL后缀,然而在Unix或Linux操作系统之下,共享类库常用的约定是使用类似于libNAME.so这样的名称。就这方面来讲,Java虚拟机首先需要在特定的操作系统中查询共享类库的约定惯例。在Windows操作系统之下,针对于加载类库的LoadLibrary()函数,官方文档中有明确的API接口说明,但所需的类库通常都包含在操作系统的安装目录中(在Windows操作系统中即为C:\WINDOWS 和 C:\WINDOWS\SYSTEM32目录),或是当前的工作目录,或者已经包含在环境变量PATH的设定之中。对于Java虚拟机的类库调用,也需要在其他两个目录中查找,即在由java.library.path系统参数指定的目录中,或是JRE运行环境所在目录的lib\i386路径之下。通常来说,推荐使用的方法是在自定义属性java.library.path中指定本地代码执行参数(在Java虚拟机启动的时候,可以设置好系统参数的路径),或者指定在JRE运行环境的i386目录中。在这个特定的例子中,很容易想象的到,指定Java虚拟机的系统参数常常是出乎开发者预期的事情(因为有时可能会有数目众多的应用服务需要设置),所以有时动态链接函数库需要被Servlet容器或应用服务器复制到Java运行环境下的函数库Lib之中。当DLL动态链接库被应用程序发现时,事实上这种所谓“混合模式”的.NET动态链接方式(即同时管理托管和非托管的代码),将会强制CLR通用语言运行时在进程启动时自动绑定,并且使得.NET通用语言运行时提供的全部功能,都集中体现在Java本地接口的动态链接库提供的操作之中。
值得一提的是,.NET应用可以通过Trampoline(弹簧床)机制,调用Java程序代码,并使用非托管的动态链接库。然而,Java虚拟机不包含.NET所具有的那些Bootstrapping引导等神奇的机制(即“一次编写,到处运行”的特性),在进程调用中,非托管的动态链接库需要正确的加载Java虚拟机,通过与先前一样的方式来使用相同的API程序调用接口。一旦Bootstrapping引导机制就位,使用Java本地接口的反射机制,就像API调用允许类库加载,对象创建和方法调用的过程一样。通过.NET CLR程序代码来访问非托管的动态链接库,实现起来仅是如何去调用P/Invoke接口的过程,并且接口调用过程具备详尽的文档说明。
如果所有这些工作,看起来需要占用很多的时间来完成,那一定会有人帮你想到更简洁的解决方法。幸运的是,已有相关的工具和技术让这个过程变得非常简单。
首先来看一款开源的工具包JACE(http://jace.sourceforge.net),JACE可以简化JNI本地调用的互操作过程,其设计目的是使得编写符合JNI规范的代码变得轻松简单,特别是对于Java虚拟机的Bootstrapping引导机制方面。JACE的功能相对完善,并且JACE为非托管的C++代码提供支持,这样可能意味着我们仍然需要反过头来以Windows动态链接库的方式编写各种“不安全”的代码。
另外还有一个叫做IKVM的开源类库,现在已经成为Mono项目的一个部分。IKVM在JVM(现在(和可预见的未来)IKVM只会从CLR到JVM,不会反过来。)和CLR之间搭建了桥梁,为Java与.NET互操作提供了与其他已提到解决方案不同的实现途径。IKVM的实现并非是将Java字节码翻译成CIL代码,所以不需要将JVM加载到同一个进程之中。这包含一些有趣的含义,既然Java虚拟机没有被加载,在代码中就不需要考虑Java虚拟机所需的运行机制:即不需要Hotspot技术,不具备JMX监测程序(这意味着没有Java控制台来监测你的Java代码运行)等等。当然,既然所有的代码将转化为CIL语言,就可以利用.NET CLR通用语言运行时的所有益处,这些功能包括:CLR通用语言运行时的JIT即时编译技术,CLR性能监视器统计等功能。自从IKVM可以执行字节码翻译之后,这样的效果就对于CLR的开发者来说就变得相对透明。
然而,我们也可能真的需要加载Java虚拟机环境,并且代码的过程代理需要在程序中释放,就像Codemesh的JuggerNET工具(JuggerNET是Java-C++代理工具的.NET版本。)生成的代码那样。它提供了两个功能:可以与.NET完善集成的Java本地接口调用API,使其可以更方便的使用.NET环境创建Java应用程序,并且提供.NET代码生成器产生.NET的代理程序,用来配置必须的参数并且执行Java对象中定义的函数方法。这样,使用JuggerNET在.NET应用中加载JVM程序的示例代码应该符合下面的过程:
/* * Copyright 1999-2006 by Codemesh, Inc. ALL RIGHTS RESERVED. */ using System; using Codemesh.JuggerNET; // // 下面的代码设定JVM环境并且在程序中进行Java调用。 // // 使用的Java虚拟机由平台依赖的业务逻辑决定。 // 在这个例子中,也可以使用JvmPath属性来设置程序将要使用的JVM。 public class Application { public static void Main( string[] argv ) { try { //-------------------------------------------------------------------- // 下面的代码提供了访问一个对象的途径,你可以使用这个对象来初始化运行时设置。 // IJvmLoader loader = JvmLoader.GetJvmLoader(); //-------------------------------------------------------------------- // 配置Java设置 // // 设置classpath参数为当前的工作目录 loader.ClassPath = "."; // 在classpath中添加CWD的父目录 loader.AppendToClassPath( ".." ); // 设置堆栈的最大值 loader.MaximumHeapSizeInMB = 256; // 设置一组 -D 选项 loader.DashDOption[ "myprop" ] = "myvalue"; loader.DashDOption[ "prop_without_value" ] = null; // 指定 TraceFile记录文件.如果不指定,所有的记录输出将会加入到 stderr标准错误之中 loader.TraceFile = ".\\trace.log"; //-------------------------------------------------------------------- // 你可以将这一项置空,在第一个代理操作执行时,或是可以精确加载Java虚拟机的时候, // 使用配置设置来去除程序对于JVM环境的需求。 // 如果有错误发生,将会抛出一个异常。 // loader.Load(); } catch( System.Exception ) { Console.WriteLine( "!!!!!!!!!!!!!!! we caught an exception !!!!!!!!!!!!!!!!" ); } Console.WriteLine( "*************** we're leaving Main() ****************" ); return; } } |
.NET到Java代码生成的代理机制中,具备一定的编程技巧,因为存在一些手动设置来指定哪一个Java类和包应该被设为代理,实现这样的过程可以使用JuggerNET的GUI工具来指定描述包和类清单的模型文件,或者可以使用Ant脚本(这意味着一部分或全部的.NET程序发布需要使用Java的Ant工具来实现,对于互操作项目来说,这并非是完全不切合实际的),通过使用"
/* * Copyright 1999-2006 by Codemesh, Inc. ALL RIGHTS RESERVED. */ using System; using Codemesh.JuggerNET; using Java.Lang; using Java.Util; /// /// 使用.NET类型来定义数据成员。 /// 通过拓展序列化的代理接口,我们自动为.NET类型产生被称为"peer"的参数。 /// 序列化接口在代码生成器中进行标记, /// 并且使用Java同等的类型来保持.NET实例的序列化信息。 /// public class MyDotNetClass : Java.Io.Serializable { public int field1 = 0; public int field2 = 1; public string strField = " public MyDotNetClass() { } public MyDotNetClass( int f1, int f2, string s ) { field1 = f1; field2 = f2; strField = s; } public override string ToString() { return "MyDotNetClass[field1=" + field1 + ", field2=" + field2 + ", strField='" + strField + "']"; } } /// /// 另一个.NET的类型继承自Serializable, /// 但是声明为不同类型的数据元素。 /// public class MyDotNetClass2 : Java.Io.Serializable { public int[] test = new int[] { 0, 1, 2 }; public MyDotNetClass2() { } public MyDotNetClass2( int f1, int f2 ) { test[ 0 ] = f1; test[ 1 ] = f2; } public override string ToString() { System.Text.StringBuilder result = new System.Text.StringBuilder(); result.Append( "MyDotNetClass2[test=[" ); for (int i = 0; i < test.Length; i++) { if( i != 0 ) result.Append( "," ); result.Append( "" + test[i] ); } result.Append( "]]" ); return result.ToString(); } } /// /// 这个类型阐明了如何实现等同序列化的目标。 /// 通过为.NET类型添加JavaPeer属性。 /// 创建相似的用法来继承Java.Io.Serializable /// 但是有些不很方便的地方是,在需要使用 /// 在 /// /// /// 第一个属性指定保持数据的Java类型, /// 第二个属性指定如何序列化.NET实例来生成Java实例及其逆过程。 /// [JavaPeer(PeerType= "com.codemesh.peer.SerializablePeer", PeerMarshaller= "Codemesh.JuggerNET.ReflectionPeerValueMarshaller")] public class PureDotNetType { private char ch = 'a'; /// /// 一个字段的设置来帮助我们阐明从Java中读出的实际信息。 /// public char CharProperty { set { ch = value; } } public override string ToString() { return "PureDotNetType[ch='" + ch + "']"; } } /// /// 类型阐明了控制同等序列化细节的字段属性。 /// [JavaPeer(PeerType="com.codemesh.peer.SerializablePeer", PeerMarshaller="Codemesh.JuggerNET.ReflectionPeerValueMarshaller")] public class PureDotNetType2 { /// /// 在去除编组之后的字段值将一直保持是'42',因为它的值没有被序列化或反序列化。 /// [NonSerialized] public int NotUsed = 42; /// /// 在去除编组之后的字段值将一直保持是空值,因为它的值没有被序列化或反序列化。 /// [JavaPeer(Ignore=true)] public string AlsoNotUsed = null; /// /// 这个字段的值经过序列化或反序列化, /// 但是对于Java,这个字段是归类在'CustomFieldName'之下。 /// 你可能通常不会关心Java的名称,但是如果Java程序可以访问peer对象, /// 并且需要访问自己的数据,则可以对其加以关注。 /// [JavaPeer(Name="CustomFieldName")] public int OnlyUsedField = 2; public override string ToString() { return "PureDotNetType2[NotUsed=" + NotUsed + ", AlsoNotUsed=" + ( AlsoNotUsed == null ? "null" : AlsoNotUsed ) + ", OnlyUsedField=" + OnlyUsedField + "]"; } } public class Peer { public static void Main( string[] args ) { try { IJvmLoader loader = JvmLoader.GetJvmLoader(); if( args.Length > 1 && args[ 0 ].Equals( "-info") ) ;//loader.PrintLdLibraryPathAndExit(); // 生成哈希表的实例 Java.Util.Hashtable ht = new Java.Util.Hashtable(); // 创建一些纯.NET实例 object obj1 = new MyDotNetClass(); object obj2 = new MyDotNetClass2( 7, 9 ); PureDotNetType obj3 = new PureDotNetType(); PureDotNetType2 obj4 = new PureDotNetType2(); obj3.CharProperty = 'B'; // 这两个值将在我们的哈希表中得到对象返回值后被消除 obj4.NotUsed = 511; obj4.AlsoNotUsed = "test"; // 这个值将会被保留,但是在Java代码中将会以另外一个名称出现 obj4.OnlyUsedField = 512; // 将.NET 实例放入Java哈希表, // 请注意这里没有可用的Java原始类型提供给.NET类型, // .NET对象状态被拷贝到通用的Java实例之中。 ht.Put( "obj1", obj1 ); ht.Put( "obj2", obj2 ); ht.Put( "obj3", obj3 ); ht.Put( "obj4", obj4 ); // 这是一个真实的测试! // 现在我们尝试去得到最初的.NET信息。 object o1 = ht.Get( "obj1" ); Console.WriteLine( "o1={0}", o1.ToString()); object o2 = ht.Get( "obj2" ); Console.WriteLine( "o2={0}", o2.ToString()); object o3 = ht.Get( "obj3" ); Console.WriteLine( "o3={0}", o3.ToString()); object o4 = ht.Get( "obj4" ); Console.WriteLine( "o4={0}", o4.ToString()); Console.WriteLine( "ht={0}", ht.ToString() ); } catch( JuggerNETFrameworkException jnfe ) { Console.WriteLine( "Exception caught: {0}\n{1}\n{2}", jnfe.GetType().Name, jnfe.Message, jnfe.StackTrace ); } } } |
总的来说,在上述的程序互操作过程之中,在不考虑单一运行环境的速度优势情况下(在单一过程中的数据移动,远比网络传输中的数据移动速度更快,甚至高于快速比特),程序互操作过程包含以下的一些优点:
集中化。在许多情况下,我们希望特定资源(比方说代码中的数据库序列标识符)只存在于一个且仅此一个进程之中,来避免复杂的进程间代码同步的实现。
可靠性。较少的硬件相关性,以及整个系统单一的硬件损耗,使得系统很少会有受到攻击的可能性。
结构化要求。在某些情况下,现有的结构化模型将要求所有程序处理过程替代已有的处理过程,比如说,应用程序的现有用户接口如果使用ASP.NET编写,并且应用程序部分的互操作性,用以实现为EJB消息驱动Bean在JMS消息队列中的消息传送处理过程。则在本地程序中传送消息给Java服务,并且仅是释放消息到JMS队列之中,这样的过程就显得有些多余,特别是在假定JMS客户端代码非常简洁的时候,程序实现代价较高。将JMS的客户端代码放入ASP.NET进程之中(Codemesh为JuggerNET代理实现JMS消息客户端提供了特别的版本),来实现与现有程序架构保持一致的简洁途径。
此外,并非是所有的互操作解决方案都将通过in-proc方法来实现,但其中一些会使用这样的方法,并且开发者无需害怕这样的想法,即便是提供这些操作的工具有着非常大的使用价值。
Ted Neward是大规模企业应用系统方面的独立咨询人。也是Java、.NET和XML服务相关主题的会议上的演讲人,致力于Java与.NET的互操作技术。在Java与.NET方面,他曾撰写过几本广受认可的书籍,其中包括最近出版的《高效企业级Java开发》一书。
"The Java Native Interface" (Liang)
"Java Native Interface" (Gordon)
The JNI page at the Java SE website (http://java.sun.com/javase/6/docs/technotes/guides/jni/index.html)
"Customizing the Common Language Runtime" (Pratschner)
"Shared Source CLI" (Stutz, Neward, Shilling)
The C++/CLI Language Specification (ECMA International)