应该在 JNI 接口中抛出通用异常还是专用异常?

Should generic or specialized exceptions be thrown in the JNI interface?

场景

A 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() );
}

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

问题

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

在此上下文中抛出自定义异常意味着我们在 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 class 中捕获它,作为专用异常重新抛出:
// 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() );
}

哪个是首选,为什么?

附加信息

主要问题是您是要 re-use 单独使用本机库,还是始终将其与 Java 包装器捆绑在一起。

很多时候,Java 包装器本身就有意义,独立于异常问题,它使本机功能看起来更 Java-like。

异常对象的作用

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

本机代码通信失败

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

我看到几个选项:

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

向用户代码传达失败

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

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

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

(脚注 1)

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

创建异常类型是为了供您的代码的某些部分使用,否则就是class典型的 YAGNI 案例。

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

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

  • 对于一些方法,开发人员创建了后备策略。大多数情况下,不仅针对特定故障,而且针对任何故障都尝试该策略并无害处。

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

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