【问题标题】:Should generic or specialized exceptions be thrown in the JNI interface?JNI 接口中应该抛出通用异常还是专用异常?
【发布时间】:2020-11-05 15:38:14
【问题描述】:

场景

Java 代码库使用 C++ 库。实现了一个 JNI 接口,以便有一个 API 来使用 Java 调用访问本机方法。

到目前为止,这样做的方式是通过一个函数 Java 端:

private static native void useFancyNativeFunction() throws SomeCustomException;

JNI头文件中对应的方法包括错误处理,在需要时会抛出Java异常:

try
{
    // do amazing C++ things
}
catch ( const std::runtime_error& e )
{
    jclass Exception = env->FindClass( "util/somestuff/SomeCustomException" );
    env->ThrowNew( Exception, e.what() );
}

这意味着,如果在我们的本地代码的特定块中引发运行时异常,则会在由 JVM 封装的应用程序中引发并处理自定义 Java 异常。

问题

这引发了一个尚未解决的有趣讨论:

在这个上下文中抛出一个自定义异常意味着我们在 JNI 接口实现和我们特定的 Java 代码库之间创建了一个依赖关系。另一种方法是在本机引发通用 Java 异常,然后通过 Java 代码捕获它们,然后引发专门的异常。

所以在给定的场景中有两种选择:

  1. 本机引发专门的自定义 Java 异常:
// Java caller
try
{
    useFancyNativeFunction();
}
catch( SomeCustomException e )
{
    // treat custom exception directly here
}

// Java native call
private static native void useFancyNativeFunction() throws SomeCustomException;

// C++ JNI header
try
{
    // do amazing C++ things
}
catch ( const std::runtime_error& e )
{
    jclass Exception = env->FindClass( "util/somestuff/SomeCustomException" );
    env->ThrowNew( Exception, e.what() );
}
  1. 本地抛出通用 Java 异常,在 Java 类中捕获它,作为专用异常重新抛出:
// Java caller
try
{
    useFancyNativeFunction();
}
catch( RuntimeException e )
{
    // catch generic, throw specialized, handle elsewhere
    throw new SomeCustomException( e.getMessage() );
}

// Java native call
private static native void useFancyNativeFunction() throws RuntimeException;

// C++ JNI header
try
{
    // do amazing C++ things
}
catch ( const std::runtime_error& e )
{
    jclass Exception = env->FindClass( "java/lang/RuntimeException" );
    env->ThrowNew( Exception, e.what() );
}

哪个是首选,为什么?

其他信息

  • 在我们的例子中,这两个版本都没有缺点,因为只有一个 Java 代码库和一个 C++ 代码库。 JNI 接口不需要重复使用。
  • 依赖只存在于接口本身,而不存在于库中。后者正在被我们重用,但不涉及 Java。
  • 这两种选择都不可能对我们产生重大影响。我只是出于兴趣而询问。

【问题讨论】:

  • "在 JNI 接口实现和我们特定的 Java 代码库之间创建依赖关系" 这不一定是正确的做法,但我维护的 JNI(10 岁)和相应的 Java 代码有着千丝万缕的联系。我总是建议抛出一个特定的异常。
  • 这也是我目前的感受,但是有理由支持和反对。 例如从本机角度来看异常类的路径只是一个字符串,因此是潜在的错误来源。通用异常的路径总是相同的。我正在寻找更多理由和/或最佳实践建议。
  • @Bathsheba 你有任何偏爱的理由吗?
  • 它使 JNI 的行为更像普通的 Java 代码。我猜另一种方法是“存根”Java端的所有本机函数,捕获从JNI抛出的通用异常,然后将它们作为特定的异常类型重新抛出。有趣的是,我刚刚意识到您可以选择。我不这样做,因为我已经不得不调用 Java 代码。
  • 是的,您的选择是选项 2。在我的帖子中。到目前为止,我看到的唯一原因是库(和 JNI)在不同的 Java 代码库中重用。因为在这种情况下,Java 代码库需要正确实现自定义异常。否则,JNI 接口将尝试抛出一个不存在的异常,因为它通过类路径查找它。

标签: java c++ exception java-native-interface


【解决方案1】:

主要问题是您是想自己重用本机库,还是总是将其与 Java 包装器捆绑在一起。

通常,Java 包装器本身就有意义,独立于异常问题,它使本机功能看起来更像 Java。

异常对象的作用

异常对象旨在与调用者(调用堆栈的某处,捕获异常的地方)沟通某些方法调用失败的原因。对于典型的代码,这个原因是无关紧要的 (1),知道失败并能够生成合理的日志条目以及向用户发送消息就足够了。

本机代码通信失败

您选择通过让 C++ 顶层创建并抛出 Java 异常来从本机代码传达故障,这会创建对 Java 异常系统和您选择使用的特定异常类的依赖关系。

我看到了几个选项:

  • 使用像RuntimeException 这样的Java 标准异常。我们可以相信它会一直存在,因此这种依赖不会造成任何问题。
  • 使用您自己的异常类型,例如MyWonderfulLibraryException。我建议不要这样命名。这不是描述失败的原因,而是描述失败的位置。您必须确保异常类在您的 Java 包装库中可用。
  • 使用您自己的异常类型,例如NativeCppException。从技术上讲,它与 之前的选择,但恕我直言,将失败原因描述为在 Java 计算模型中无法恰当描述的原因更好。
  • 在不创建 Java 异常的情况下从本机代码传达故障,例如通过特殊的失败返回值。这可能比您当前的方法更容易(并且性能更高,并且创建的代码依赖项更少)。

与用户代码通信失败

用户代码应该在失败的情况下看到一个异常,一个描述失败原因的异常(主要用于记录目的)。

在您的情况下,原因隐藏在来自 C++ runtime_error 的文本中。您可能很想将其映射到不同的适当 Java 异常类型,但是“您不需要它 (YAGNI)”。

我的首选是 NativeCppException,它总结了 C++ 世界中可能发生的一切。一些调用者可能足够勇敢地捕捉到这样的异常,可能只有当他有可用的非本地替代方案时。

(脚注 1)

我知道对于个别异常类型的重要性存在不同的意见,但我还没有找到一个令人信服的论据来证明在野外经常看到的极其复杂的异常类型层次结构。

创建异常类型以供代码的某些部分使用,否则它是典型的 YAGNI 案例。

关于内部调用失败,方法通常分为三类:

  1. 方法没有回退策略,所以如果发生内部故障,整个方法都会失败。通常,这些方法允许异常在不干预的情况下传播。
  2. 方法具有回退策略,即使在某些内部调用失败后也允许成功,通常通过重试或切换到替代执行路径。如果存在这样的回退策略,则使用它通常是有意义的,而与失败原因无关。这些方法捕获特定块中出现的所有异常,然后激活回退,与失败原因无关。
  3. 方法有回退策略,只能在特定情况下应用,可以通过异常类型来区分情况。这些方法只捕获一些特定的异常类型,然后激活适当的回退。
  • 绝大多数方法都属于第一类(或者应该属于第一类,如果它们不是过度设计的)。

  • 对于一些方法,开发人员会创建后备策略。大多数情况下,不仅针对特定失败尝试该策略,而且针对任何失败都尝试该策略。

  • 在极少数情况下,失败原因对于选择原本不合适的回退很重要,例如如果数据库通过异常告诉我我的密码已过期,则代码可以将我重定向到密码更新过程,然后继续(有点做作的例子)。

实际的异常类型只在第三种方法类型中很重要,我敢打赌只有极少数的异常类型会以这种方式使用,所以我们有一个经典的 YAGNI 案例。

【讨论】:

  • 在不创建 Java 异常的情况下从本机代码传达失败,例如通过特殊的失败返回值。这可能比您当前的方法更容易(并且性能更高,并且创建的代码依赖性更少)。 这就是我对 JNI 代码所做的事情,正是出于这些原因 - 另一个重要的原因是:JNI 接口实际上是一个 C接口而不是 C++。 C 不会抛出异常。 (我也尽量减少原生代码的数量——如果原生代码在逻辑上创建了一个 Java 对象,我会编写一个 Java 包装器方法来创建一个空对象并将其传递给原生代码。)
  • @AndrewHenle 这种方法对我们来说不是一个可行的选择。有数百个本机函数被访问,其中一些在指针上操作,但其中许多返回各种类型的值。我认为尝试为每种类型定义一个错误状态对象并返回它没有任何好处,并且没有足够的优势来证明如此大的重构是合理的。这可能是使用 JNI 设计新项目时需要考虑的一个选项。
  • @AndrewHenle JNI 接口实际上是 C 接口而不是 C++。并且 C 不会抛出异常。 但它也不必。所描述的代码指示绑定的 JVM 实例抛出具有特定构造函数参数的特定类型的异常 - 这确实有效。
  • Ralf Kleberhoff 感谢您的详细回答。
猜你喜欢
  • 2011-04-27
  • 1970-01-01
  • 2013-03-14
  • 1970-01-01
  • 1970-01-01
  • 2016-06-03
  • 1970-01-01
  • 2012-06-28
相关资源
最近更新 更多