Java 调用托管 DLL 的方式
基本上就两种方式:
- C# 开发 COM 组件,Java 调用。
- 用 C++/CLI 编写 DLL,暴露接口给 Java,内部再调用 C# DLL 的接口,起到桥接的作用。
用 COM 组件的方式简单直接,只是不够便携,需要注册才可使用,注册后 DLL 不可移动文件位置,否则要重新注册,不过也算不上大问题。
用 C++/CLI 方案就繁琐些,需要了解一点 C++/CLI 语法。但是对于 Java 来说就像调用纯 C DLL 一样简单。
Java 加载托管 DLL 的问题
在实践 C++/CLI 方案中,碰到一个让人在意的问题:托管 DLL 放在哪里才能被 Java 加载?
在开发 C++/CLI 工程时,正常做法都会引用 C# 工程:
1 |
因为只有 using 后才能在代码中调用托管接口,但这产生了一个问题,C++ DLL 在被加载时会启动 CLR,而 CLR 只会从当前进程目录和 GAC 中加载托管 DLL。如果主程序是 C++ 等程序倒不会有问题,因为 exe 和 dll 文件都在一个目录下,自然可以顺利加载。但是到了 Java 这里情况就不同了,比如应用目录是这样:
1 | app.jar |
因为 Java 应用并不是传统的 exe 可执行文件,它最终是由java.exe
作为宿主进程运行的,而java.exe
文件在%JAVA_HOME%\bin
目录下,CLR 会从这里加载托管 DLL,这就逼迫我们必须将托管 DLL 放置在%JAVA_HOME%\bin
目录下,很有侵入性。
我想到有两种解决方案:
在开发 C++/CLI 工程时,不要强引用 C# DLL 工程,这样生成的 DLL 文件不会在初始化时去加载托管 DLL。然后在代码中动态加载托管 DLL,大概是这样:
1
2
3
4
5
6
7
8
9
10String^ currentDir = Path::GetDirectoryName(Assembly::GetExecutingAssembly()->Location);
String^ dllPath = Path::Combine(currentDir, "CSharp.dll");
Assembly::LoadFrom(dllPath);
// 动态查找接口
static MethodInfo^ GetMethod(String^ name)
{
Type^ type = lib->GetType("mynamespae.Demo");
return type->GetMethod(name);
}这样做的缺点就是在开发时会痛苦,因为无法直接使用托管 DLL 的接口了,完全靠动态加载,显得比较繁琐。好处是能将 DLL 放在 jar 包同一个目录下。
将托管 DLL 打包到资源中,在加载 C++ DLL 之前先将托管 DLL 释放到
%JAVA_HOME%\bin
目录,然后再加载 C++ DLL 文件。不过这样会污染 Java 目录。
我个人推荐第一种方式,牺牲一点开发时的便捷性,提高环保性。
相关阅读
实现通过COM组件方式实现java调用C#写的DLL文件的完整demo
Java调用C#的DLL的坑
使用 C++/CLI 进行 .NET 编程