并非所有的开发者都清楚时下最流行的两个程序运行环境(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 中包含工作流WPF和InfoCard等广受关注的特性或是在NET过程中使用Java虚拟机提供的工具比如说部署Java语言编写的那些包含复杂业务逻辑的Spring组件或者实现通过ASPNET访问JMS消息队列这样的功能
加载动态链接库以及与底层代码托管环境进行交互是解决互操作问题所面临的两个不同问题然而每一项操作都为之提供了标准的应用程序接口来完成这样的功能举例来说下面列出的非托管C++代码来自于Java本地接口JNI的官方文档目的是利用标准过程(相关的代码句柄在JNIHosting子目录里以InProcInterop方案的一部分存在构建它的最好方法是在命令行里用指向JDK 目录位置的JAVA_HOME环境变量来操作
)创建基于Java虚拟机的函数调用
#include stdafxh
#include
int _tmain(int argc _TCHAR* argv[])
{
JavaVM *jvm; /* 表示一个Java虚拟机 */
JNIEnv *env; /* 指向本地方法调用接口 */
JavaVMInitArgs vm_args; /* JDK或JRE 的虚拟机初始化参数 */
JavaVMOption options[]; int n = ;
options[n++]optionString = Djavaclasspath=;
vm_argsversion = JNI_VERSION__;
vm_argsnOptions = n;
vm_argsoptions = options;
vm_argsignoreUnrecognized = false;
/* 加载或初始化Java虚拟机返回Java本地调用接口
* 指向变量 env */
JNI_CreateJavaVM(&jvm (void**)&env &vm_args); // 传入C++所需的参数
/* 使用Java本地接口调用 Maintest 方法 */
jclass cls = env>FindClass(Main);
jmethodID mid = env>GetStaticMethodID(cls test (I)V);
env>CallStaticVoidMethod(cls mid );
/* 完成工作 */
jvm>DestroyJavaVM();
return ;
}
在编译上述代码时Java开发工具包JDK中的include和include\win目录将被添加在C++程序的include路径中并且JDK中lib目录下的jvmlib必须位于目标代码连接器的路径之中程序运行时默认情况下程序的主类Mainclass作为程序执行的入口类与上述文件位于相同的目录之中并且保证Java运行环境JRE中的jvmdll动态链接库存在一般来说这个动态链接库是存在于系统环境变量的PATH路径之中(jvmdll通常不需要手动添加在PATH路径中因为javaexe将会动态的查找jvmdll动态链接库的位置并在找到链接库后记录下它的位置)
同样NET通用语言运行时CLR提供自有的应用程序调用接口作为本地API接口来实现同样的功能代码如下
#include stdafxh
#include
int _tmain(int argc _TCHAR* argv[])
{
ICLRRuntimeHost* pCLR = (ICLRRuntimeHost*);
HRESULT hr = CorBindToRuntimeEx(NULL Lwks
STARTUP_CONCURRENT_GC CLSID_CLRRuntimeHost IID_ICLRRuntimeHost
(PVOID*)&pCLR);
if (FAILED(hr))
return ;
hr = pCLR>Start();
if (FAILED(hr))
return ;
DWORD retval = ;
hr = pCLR>ExecuteInDefaultAppDomain(LHelloWorldexe LHello LMain NULL &retval);
if (FAILED(hr))
return ;
hr = pCLR>Stop();
if (FAILED(hr))
return ;
return (int)retval;
}
如同Java本地接口JNI的示例一样
上面的示例假定应用程序HelloWorld
exe在执行时与
NET编译(这儿
因为我们期望正用到的(ExecuteInDefaultAppDomain)这个特殊的宿主API能有一个调用它里面Hello的类
这个类要有一个名为Main的方法
以将一个字符串当作声明处理并返回整数值
注意这和传统的C#或者VB
NET的入口通道有所不同
)都位于当前目录之下
由于
NET通用语言运行时CLR与操作系统具备更紧密的集成关系
所以CLR的动态链接库路径不需要手动设置在环境变量的PATH路径之中(关于CLR启动程序如何进行工作处理
详细内容请参考《CLI的共享源代码实现》一书)
当程序开发者使用非托管的C++代码编写应用成为可能即可以加载CLR和JVM这两种不同的运行时环境来完成处理过程这使得大部分业务逻辑的程序编写陷入开发者不敢去涉及的境地然而吸引人的是这可以作为锻炼编程技巧与能力的一种方式对于我们大多数人在这个过程中都会找到一系列的替代方案
首先比如说CLR和JVM两种技术都支持非托管代码的Calling Down操作(在Java虚拟机中被称作Java本地接口而在NET的CLR中被称作P/Invoke调用)这样的机制使得开发者可以在其中一个运行环境下定义功能方法通过少量的Trampoline(弹簧床)编码将程序迁移到另一个运行时环境下编译执行例如在Java程序中通过本地方法接口JNI实现函数的调用操作较为繁琐并且需要记录配置文档(比如可以参见Liang或者Gordon的书或者JDK中JNI的文档)而在实现C++本地代码调用的过程中较为繁琐的操作是使用微软Visual Studio 中提供的C++/CLI或Visual Studio 提供的C++托管代码来进行代码编译的过程
在这个步骤中复杂之处在于程序运行时需要确保Java虚拟机得到访问动态链接库的路径这项工作可以分为两部分来完成首先当Java类函数的本地方法被程序加载时需要询问Java虚拟机是否通过RuntimeloadLibrary()操作来请求加载共享库函数值得注意的是本地类库请求是在没有指定文件拓展名的情况下完成这样的操作不指定拓展名是因为不同的操作系统往往使用不同的约定来共享类库所以只需指定共享类库名称即可比如在Windows操作系统下共享类库具有DLL后缀然而在Unix或Linux操作系统之下共享类库常用的约定是使用类似于libNAMEso这样的名称就这方面来讲Java虚拟机首先需要在特定的操作系统中查询共享类库的约定惯例在Windows操作系统之下针对于加载类库的LoadLibrary()函数官方文档中有明确的API接口说明但所需的类库通常都包含在操作系统的安装目录中(在Windows操作系统中即为C:\WINDOWS 和 C:\WINDOWS\SYSTEM目录)或是当前的工作目录或者已经包含在环境变量PATH的设定之中对于Java虚拟机的类库调用也需要在其他两个目录中查找即在由javalibrarypath系统参数指定的目录中或是JRE运行环境所在目录的lib\i路径之下通常来说推荐使用的方法是在自定义属性javalibrarypath中指定本地代码执行参数(在Java虚拟机启动的时候可以设置好系统参数的路径)或者指定在JRE运行环境的i目录中在这个特定的例子中很容易想象的到指定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()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是JavaC++代理工具的NET版本)生成的代码那样它提供了两个功能可以与NET完善集成的Java本地接口调用API使其可以更方便的使用NET环境创建Java应用程序并且提供NET代码生成器产生NET的代理程序用来配置必须的参数并且执行Java对象中定义的函数方法这样使用JuggerNET在NET应用中加载JVM程序的示例代码应该符合下面的过程
/*
* Copyright by Codemesh Inc ALL RIGHTS RESERVED
*/
using System;
using CodemeshJuggerNET;
//
// 下面的代码设定JVM环境并且在程序中进行Java调用
//
// 使用的Java虚拟机由平台依赖的业务逻辑决定
// 在这个例子中也可以使用JvmPath属性来设置程序将要使用的JVM
public class Application
{
public static void Main( string[] argv )
{
try
{
//
// 下面的代码提供了访问一个对象的途径你可以使用这个对象来初始化运行时设置
//
IJvmLoader loader = JvmLoaderGetJvmLoader();
//
// 配置Java设置
//
// 设置classpath参数为当前的工作目录
loaderClassPath = ;
// 在classpath中添加CWD的父目录
loaderAppendToClassPath( );
// 设置堆栈的最大值
loaderMaximumHeapSizeInMB = ;
// 设置一组 D 选项
loaderDashDOption[ myprop ] = myvalue;
loaderDashDOption[ prop_without_value ] = null;
// 指定 TraceFile记录文件如果不指定所有的记录输出将会加入到 stderr标准错误之中
loaderTraceFile = \\tracelog;
//
// 你可以将这一项置空在第一个代理操作执行时或是可以精确加载Java虚拟机的时候
// 使用配置设置来去除程序对于JVM环境的需求
// 如果有错误发生将会抛出一个异常
//
loaderLoad();
}
catch( SystemException )
{
ConsoleWriteLine( !!!!!!!!!!!!!!! we caught an exception !!!!!!!!!!!!!!!! );
}
ConsoleWriteLine( *************** were leaving Main() **************** );
return;
}
}
NET到Java代码生成的代理机制中具备一定的编程技巧因为存在一些手动设置来指定哪一个Java类和包应该被设为代理实现这样的过程可以使用JuggerNET的GUI工具来指定描述包和类清单的模型文件或者可以使用Ant脚本(这意味着一部分或全部的NET程序发布需要使用Java的Ant工具来实现对于互操作项目来说这并非是完全不切合实际的)通过使用
/*
* Copyright by Codemesh Inc ALL RIGHTS RESERVED
*/
using System;
using CodemeshJuggerNET;
using JavaLang;
using JavaUtil;
///
/// 使用NET类型来定义数据成员
/// 通过拓展序列化的代理接口我们自动为NET类型产生被称为peer的参数
/// 序列化接口在代码生成器中进行标记
/// 并且使用Java同等的类型来保持NET实例的序列化信息
///
public class MyDotNetClass : JavaIoSerializable
{
public int field = ;
public int field = ;
public string strField = ;
public MyDotNetClass()
{
}
public MyDotNetClass( int f int f string s )
{
field = f;
field = f;
strField = s;
}
public override string ToString()
{
return MyDotNetClass[field= + field + field= + field + strField= + strField + ];
}
}
///
/// 另一个NET的类型继承自Serializable
/// 但是声明为不同类型的数据元素
///
public class MyDotNetClass : JavaIoSerializable
{
public int[] test = new int[] { };
public MyDotNetClass()
{
}
public MyDotNetClass( int f int f )
{
test[ ] = f;
test[ ] = f;
}
public override string ToString()
{
SystemTextStringBuilder result = new SystemTextStringBuilder();
resultAppend( MyDotNetClass[test=[ );
for (int i = ; i < testLength; i++)
{
if( i != )
resultAppend( );
resultAppend( + test[i] );
}
resultAppend( ]] );
return resultToString(); }
}
///
/// 这个类型阐明了如何实现等同序列化的目标
/// 通过为NET类型添加JavaPeer属性
/// 创建相似的用法来继承JavaIoSerializable
/// 但是有些不很方便的地方是在需要使用Serializable的时候
/// 在PureDotNetType处不能使用生成的实例
/// JavaPeer属性列出了两个不同的属性
/// 分别是PeerType 和 PeerMarshaller
/// 第一个属性指定保持数据的Java类型
/// 第二个属性指定如何序列化NET实例来生成Java实例及其逆过程
///
[JavaPeer(PeerType= demeshpeerSerializablePeer
PeerMarshaller= CodemeshJuggerNETReflectionPeerValueMarshaller)]
public class PureDotNetType
{
private char ch = a;
///
/// 一个字段的设置来帮助我们阐明从Java中读出的实际信息
///
public char CharProperty
{
set { ch = value; }
}
public override string ToString()
{
return PureDotNetType[ch= + ch + ];
}
}
///
/// 类型阐明了控制同等序列化细节的字段属性
///
[JavaPeer(PeerType=demeshpeerSerializablePeer
PeerMarshaller=CodemeshJuggerNETReflectionPeerValueMarshaller)]
public class PureDotNetType
{
///
/// 在去除编组之后的字段值将一直保持是因为它的值没有被序列化或反序列化
///
[NonSerialized]
public int NotUsed = ;
///
/// 在去除编组之后的字段值将一直保持是空值因为它的值没有被序列化或反序列化
///
[JavaPeer(Ignore=true)] public string AlsoNotUsed = null;
///
/// 这个字段的值经过序列化或反序列化
/// 但是对于Java这个字段是归类在CustomFieldName之下
/// 你可能通常不会关心Java的名称但是如果Java程序可以访问peer对象
/// 并且需要访问自己的数据则可以对其加以关注
///
[JavaPeer(Name=CustomFieldName)]
public int OnlyUsedField = ;
public override string ToString()
{
return PureDotNetType[NotUsed= + NotUsed +
AlsoNotUsed= + ( AlsoNotUsed == null ? null : AlsoNotUsed ) +
OnlyUsedField= + OnlyUsedField + ];
}
}
public class Peer
{ public static void Main( string[] args )
{
try
{
IJvmLoader loader = JvmLoaderGetJvmLoader();
if( argsLength > && args[ ]Equals( info) )
;//loaderPrintLdLibraryPathAndExit();
// 生成哈希表的实例
JavaUtilHashtable ht = new JavaUtilHashtable();
// 创建一些纯NET实例
object obj = new MyDotNetClass();
object obj = new MyDotNetClass( );
PureDotNetType obj = new PureDotNetType();
PureDotNetType obj = new PureDotNetType();
objCharProperty = B;
// 这两个值将在我们的哈希表中得到对象返回值后被消除
objNotUsed = ;
objAlsoNotUsed = test;
// 这个值将会被保留但是在Java代码中将会以另外一个名称出现
objOnlyUsedField = ;
// 将NET 实例放入Java哈希表
// 请注意这里没有可用的Java原始类型提供给NET类型
// NET对象状态被拷贝到通用的Java实例之中
htPut( obj obj );
htPut( obj obj );
htPut( obj obj );
htPut( obj obj );
// 这是一个真实的测试!
// 现在我们尝试去得到最初的NET信息
object o = htGet( obj );
ConsoleWriteLine( o={} oToString());
object o = htGet( obj );
ConsoleWriteLine( o={} oToString());
object o = htGet( obj );
ConsoleWriteLine( o={} oToString());
object o = htGet( obj );
ConsoleWriteLine( o={} oToString());
ConsoleWriteLine( ht={} htToString() );
}
catch( JuggerNETFrameworkException jnfe )
{
ConsoleWriteLine( Exception caught: {}\n{}\n{} jnfeGetType()Name
jnfeMessage jnfeStackTrace );
}
}
}
总的来说在上述的程序互操作过程之中在不考虑单一运行环境的速度优势情况下(在单一过程中的数据移动远比网络传输中的数据移动速度更快甚至高于快速比特)程序互操作过程包含以下的一些优点
集中化在许多情况下我们希望特定资源(比方说代码中的数据库序列标识符)只存在于一个且仅此一个进程之中来避免复杂的进程间代码同步的实现
可靠性较少的硬件相关性以及整个系统单一的硬件损耗使得系统很少会有受到攻击的可能性
结构化要求在某些情况下现有的结构化模型将要求所有程序处理过程替代已有的处理过程比如说应用程序的现有用户接口如果使用ASPNET编写并且应用程序部分的互操作性用以实现为EJB消息驱动Bean在JMS消息队列中的消息传送处理过程则在本地程序中传送消息给Java服务并且仅是释放消息到JMS队列之中这样的过程就显得有些多余特别是在假定JMS客户端代码非常简洁的时候程序实现代价较高将JMS的客户端代码放入ASPNET进程之中(Codemesh为JuggerNET代理实现JMS消息客户端提供了特别的版本)来实现与现有程序架构保持一致的简洁途径
此外并非是所有的互操作解决方案都将通过inproc方法来实现但其中一些会使用这样的方法并且开发者无需害怕这样的想法即便是提供这些操作的工具有着非常大的使用价值
关于作者
Ted Neward是大规模企业应用系统方面的独立咨询人也是JavaNET和XML服务相关主题的会议上的演讲人致力于Java与NET的互操作技术在Java与NET方面他曾撰写过几本广受认可的书籍其中包括最近出版的《高效企业级Java开发》一书
资源
The Java Native Interface (Liang)
Java Native Interface (Gordon)
The JNI page at the Java SE website ()
Customizing the Common Language Runtime (Pratschner)
Shared Source CLI (Stutz Neward Shilling)
The C++/CLI Language Specification (ECMA International)