从 Java 调用 .NET 代码:所有方法排名(作者全程见证)

发布: (2026年2月22日 GMT+8 04:30)
8 分钟阅读
原文: Dev.to

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

我直言不讳:我们见过团队尝试此方法,几周后就放弃了。

JNIC++ 内存管理.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. 其他开源 / 商业选项

ToolStatusNotes
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 次调用(典型复杂操作)
REST20‑50 ms300‑750 ms
gRPC5‑15 ms75‑225 ms
进程执行200 ms+不实际(每次调用都要生成进程)
进程内<1 ms~10‑15 ms

问题不在于 哪个最快 —— 而在于 哪种权衡符合你的需求

8. Decision Guide – Three Questions

  1. 每个用户请求的跨运行时调用次数是多少?

    • 1‑5 次 → REST 或 gRPC
    • 10 次以上 → 评估进程内桥接
  2. 你的延迟容忍度是多少?

    • 秒级 → REST(最简单)
    • 百毫秒级 → gRPC
    • 十毫秒级 → 进程内
  3. 这将如何演进?

    • 计划迁移出 .NET → REST(以后最容易替换)
    • 两个平台都是永久的 → 投资进程内桥接(例如 JNBridge Pro、Javonet)

选择与您的性能需求、运营复杂性和长期策略相匹配的方法。

In Tighter Integration

大多数团队最终选择 RESTgRPC,这对大多数架构来说是正确的选择。

我真的想听听大家正在使用的集成模式。Java/.NET 互操作领域比业界承认的要常见——只是不常被提及,因为没有人对此感到兴奋。请在评论中留下你的设置。

Disclosure: 我在 JNBridge 工作,该公司生产 JNBridgePro —— 本文讨论的集成工具之一。我力求对所有方法进行诚实的比较,包括我们的产品并非最佳选择的情况。

0 浏览
Back to Blog

相关文章

阅读更多 »

商店3

gradle 任务 runQuantumtype: JavaExec { dependsOn prepareLibDir, classes systemProperty 'org.gradle.scan.acceptTerm', 'true' doFirst { setTmpDir buildFileSystem'...