从 Java 调用 .NET 代码:所有方法排名(作者全程见证)
Source: Dev.to
(未提供需要翻译的正文内容。如需翻译,请粘贴完整的文本。)
典型场景
- Legacy C# pricing engine – 公司不会重写
- .NET‑specific libraries – 没有 Java 等价物
- Windows APIs – 你的 Java 应用突然需要调用它们
- .NET service – 过于紧耦合,难以包装成干净的 API
本能的想法是问 “为什么不直接用 Java 重写?”
答案通常是 金钱和时间,于是你选择集成。
1. 将 .NET 代码封装在 ASP.NET Core Web API 中
C# 端
[ApiController]
[Route("api/[controller]")]
public class PricingController : ControllerBase
{
[HttpPost("calculate")]
public ActionResult Calculate([FromBody] PriceRequest request)
{
var engine = new PricingEngine();
return Ok(engine.CalculatePrice(request));
}
}
Java 端
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:5000/api/pricing/calculate"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
表现良好的情况
- 每个用户请求只需要少量调用。
- 清晰的职责划分;团队可以独立工作。
- 部署和监控都很方便。
容易出问题的情况
- 单个业务操作需要大量 .NET 方法调用。
- 示例:一次定价计算需要 10‑15 个独立的 C# 调用。
- 按每次 HTTP 往返约 20 ms 计算,纯网络开销就会达到 200‑300 ms,还未开始任何计算。
2. 基于 HTTP/2 的二进制序列化 (gRPC)
比 REST 更快,使用 Protocol Buffers 的强类型契约。
ManagedChannel channel = ManagedChannelBuilder
.forAddress("localhost", 5001)
.usePlaintext()
.build();
PricingServiceGrpc.PricingServiceBlockingStub stub =
PricingServiceGrpc.newBlockingStub(channel);
PriceResult result = stub.calculatePrice(
PriceRequest.newBuilder()
.setProductId("WIDGET-123")
.build());
相较于 REST 的改进
- 每次调用大约 5‑15 ms,而不是 25‑75 ms。
- 强类型契约可以在编译时捕获破坏性更改,而不是等到生产环境。
仍然受限于
- 网络延迟。如果调用模式需要多次往返,你仍会累计毫秒级延迟。
- 现在需要在两个代码库之间维护 .proto 文件。
3. 编写 C++ 加载 .NET CLR 并通过 JNI 暴露给 Java
我直言不讳:我们见过团队尝试此方法,几周后就放弃了。
将 JNI、C++ 内存管理 与 .NET 托管 API 组合在一起,会导致调试噩梦。一次引用管理不当就可能在几乎没有诊断信息的情况下使整个 JVM 崩溃。
- 何时合适 – 高度专用的嵌入式系统。
- 对于典型的企业集成 – 工程成本很少能得到合理的回报。
4. 从 Java 运行 .NET 子进程
ProcessBuilder pb = new ProcessBuilder(
"dotnet", "run",
"--project", "PricingEngine",
"--", "WIDGET-123", "100"
);
Process process = pb.start();
BufferedReader reader = new BufferedReader(
new InputStreamReader(process.getInputStream()));
String result = reader.readLine();
适用场景
- 批处理操作、计划任务、以及任何不经常调用 .NET 的场景。
- 能够容忍 200 ms+ 启动时间的场景。
不适用场景
- 交互式工作负载。每次调用的 .NET 运行时启动成本使其不适合面向用户的操作。
5. 进程内桥接(JNBridge Pro 的作用)
完整披露——这正是 JNBridge Pro 所做的。
JVM 和 CLR 在 同一进程 中运行,使用生成的代理类将 .NET 对象作为本机 Java 对象访问。
// This is actually calling C# under the hood
PricingEngine engine = new PricingEngine();
PriceResult result = engine.calculatePrice("WIDGET-123", 100);
优势
- 微秒级调用延迟,而非毫秒级。
- 对于每个操作需要数十次跨运行时调用的工作负载,性能差异非常显著。
权衡
- 部署更复杂(一个进程中包含两个运行时、两个垃圾回收器)。
- 商业授权费用。
- 最适合已经测量出性能瓶颈并确认网络开销是问题所在的团队。
当我们告诉客户不要使用它时
- 如果每个请求只进行 2‑3 次 .NET 调用,REST 或 gRPC 更简单,延迟差异对用户影响不大。
- 在不需要时,使用合适的工具,而不是购买我们的产品。
6. 其他开源 / 商业选项
| Tool | Status | Notes |
|---|---|---|
| jni4net | 实际上已被放弃(≈2015) | 不支持现代 .NET 版本。 |
| IKVM | 存在社区维护的分支 | 将 .NET 程序集编译为 Java 字节码;在大量反射或 P/Invoke 时会遇到困难。 |
| Javonet | 商业 | 技术路线不同;值得评估——竞争有助于生态系统健康。 |
7. Numbers from Real Deployments
(Your mileage will vary based on payload size and network conditions.)
| 方法 | 单次调用延迟 | 15 次调用(典型复杂操作) |
|---|---|---|
| REST | 20‑50 ms | 300‑750 ms |
| gRPC | 5‑15 ms | 75‑225 ms |
| 进程执行 | 200 ms+ | 不实际(每次调用都要生成进程) |
| 进程内 | <1 ms | ~10‑15 ms |
问题不在于 哪个最快 —— 而在于 哪种权衡符合你的需求。
8. Decision Guide – Three Questions
-
每个用户请求的跨运行时调用次数是多少?
- 1‑5 次 → REST 或 gRPC
- 10 次以上 → 评估进程内桥接
-
你的延迟容忍度是多少?
- 秒级 → REST(最简单)
- 百毫秒级 → gRPC
- 十毫秒级 → 进程内
-
这将如何演进?
- 计划迁移出 .NET → REST(以后最容易替换)
- 两个平台都是永久的 → 投资进程内桥接(例如 JNBridge Pro、Javonet)
选择与您的性能需求、运营复杂性和长期策略相匹配的方法。
In Tighter Integration
大多数团队最终选择 REST 或 gRPC,这对大多数架构来说是正确的选择。
我真的想听听大家正在使用的集成模式。Java/.NET 互操作领域比业界承认的要常见——只是不常被提及,因为没有人对此感到兴奋。请在评论中留下你的设置。
Disclosure: 我在 JNBridge 工作,该公司生产 JNBridgePro —— 本文讨论的集成工具之一。我力求对所有方法进行诚实的比较,包括我们的产品并非最佳选择的情况。