并非所有的开发者都清楚,时下最流行的两个程序运行环境(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的官方文档,目的是利用标准过程1创建基于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;
}
如同Java本地接口JNI的示例一样,上面的示例假定应用程序HelloWorld.exe在执行时与.NET编译2都位于当前目录之下。由于.NET通用语言运行时CLR与操作系统具备更紧密的集成关系,所以CLR的动态链接库路径不需要手动设置在环境变量的PATH路径之中(关于CLR启动程序如何进行工作处理,详细内容请参考《CLI的共享源代码实现》一书)。
当程序开发者使用非托管的C++代码编写应用成为可能,即可以加载CLR和JVM这两种不同的运行时环境来完成处理过程,这使得大部分业务逻辑的程序编写陷入开发者不敢去涉及的境地。然而吸引人的是,这可以作为锻炼编程技巧与能力的一种方式,对于我们大多数人,在这个过程中都会找到一系列的替代方案。
首先,比如说,CLR和JVM两种技术都支持非托管代码的“Calling Down”操作(在Java虚拟机中,被称作Java本地接口,而在.NET的CLR中,被称作P/Invoke调用),这样的机制使得开发者可以在其中一个运行环境下定义功能方法,通过少量的“Trampoline(弹簧床)”编码,将程序迁移到另一个运行时环境下编译执行。例如,在Java程序中,通过本地方法接口JNI实现函数的调用操作较为繁琐,并且需要记录配置文档3。而在实现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在JVM4和CLR之间搭建了桥梁,为Java与.NET互操作提供了与其他已提到解决方案不同的实现途径。IKVM的实现并非是将Java字节码翻译成CIL代码,所以不需要将JVM加载到同一个进程之中。这包含一些有趣的含义,既然Java虚拟机没有被加载,在代码中就不需要考虑Java虚拟机所需的运行机制:即不需要Hotspot技术,不具备JMX监测程序(这意味着没有Java控制台来监测你的Java代码运行)等等。当然,既然所有的代码将转化为CIL语言,就可以利用.NET CLR通用语言运行时的所有益处,这些功能包括:CLR通用语言运行时的JIT即时编译技术,CLR性能监视器统计等功能。自从IKVM可以执行字节码翻译之后,这样的效果就对于CLR的开发者来说就变得相对透明。
然而,我们也可能真的需要加载Java虚拟机环境,并且代码的过程代理需要在程序中释放,就像Codemesh的JuggerNET工具5生成的代码那样。它提供了两个功能:可以与.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
/// 但是有些不很方便的地方是,在需要使用 Serializable的时候,
/// 在 PureDotNetType处不能使用生成的实例。
/// JavaPeer属性列出了两个不同的属性:
/// 分别是 PeerType 和 PeerMarshaller。
/// 第一个属性指定保持数据的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 );
}
}
}
总的来说,在上述的程序互操作过程之中,在不考虑单一运行环境的速度优势情况下(在单一过程中的数据移动,远比网络传输中的数据移动速度更快,甚至高于快速比特),程序互操作过程包含以下的一些优点:
此外,并非是所有的互操作解决方案都将通过in-proc方法来实现,但其中一些会使用这样的方法,并且开发者无需害怕这样的想法,即便是提供这些操作的工具有着非常大的使用价值。
Ted Neward是大规模企业应用系统方面的独立咨询人。也是Java、.NET和XML服务相关主题的会议上的演讲人,致力于Java与.NET的互操作技术。在Java与.NET方面,他曾撰写过几本广受认可的书籍,其中包括最近出版的《高效企业级Java开发》一书。