如何解决是否应在JNI接口中引发通用或专用异常?
场景
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代码捕获它们,然后抛出专门的异常。
因此,在给定的情况下,有两个选择:
- 本地抛出专门的自定义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() );
}
- 本机抛出通用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。
- 这两种选择都不会对我们产生重大影响。我只是出于兴趣而问。
解决方法
主要问题是您是想重用本机库,还是总是将其与Java包装器捆绑在一起。
通常,Java包装程序独立于异常情况而有意义,这使得本机功能看起来更像Java。
异常对象的作用
异常对象用于与调用方(在调用堆栈的某个地方,捕获了异常)进行通信,告知某些方法调用失败的原因。对于典型的代码,该原因是无关紧要的(1),足以了解该故障并能够生成有意义的日志条目以及一条消息给用户。
与本机代码通信失败
您选择通过C ++顶层创建和引发Java异常来与本机代码进行通信,从而创建对Java异常系统和您选择使用的特定异常类的依赖。
我看到一些选择:
- 使用Java标准异常,例如
RuntimeException
。我们可以相信它会一直存在,因此依赖不会造成任何问题。 - 使用您自己的异常类型,例如
MyWonderfulLibraryException
。我建议不要这样命名。这不是描述故障的原因,而是故障的位置。您必须确保Java包装器库中提供了异常类。 - 使用您自己的异常类型,例如
NativeCppException
。从技术上讲,它与 是以前的选择,但是恕我直言更好地将失败原因描述为Java计算模型中无法适当描述的原因。 - 在不创建Java异常(例如通过特殊的失败返回值。与您当前的方法相比,这可能更容易(并且性能更高,并且创建的代码依赖性更少)。
通信失败的用户代码
在失败的情况下,用户代码应该看到一个异常,该异常描述了失败的原因(主要用于记录目的)。
在您的情况下,原因隐藏在来自C ++ runtime_error
的文本中。您可能会试图将其映射到其他适当的Java异常类型中,但是会“您将不需要它(YAGNI)”。
我的首选是NativeCppException
之类的东西,它总结了C ++世界中可能发生的一切。某些呼叫者可能很勇敢地捕捉到这样的异常,可能只有在他有一个非本机可用的替代者的情况下。
(脚注1)
我知道对于单个异常类型的重要性存在不同的观点,但是我还没有找到令人信服的论据来证明通常在野外看到的极其复杂的异常类型层次结构。
将创建异常类型以供代码的某些部分使用,否则,这是经典的YAGNI案例。
关于内部调用失败,方法通常分为三类:
- 该方法没有后备策略,因此在发生某些内部故障的情况下,整个方法都会失败。通常,这些方法会让异常在不经过干预的情况下不断蔓延。
- 方法具有一个后备策略,即使在某些内部调用失败之后也可以成功,通常通过重试或切换到其他执行路径来实现。如果存在这种后备策略,则通常可以使用它,而与故障原因无关。这些方法捕获特定块中出现的所有异常,然后激活后备,而与失败原因无关。
- 方法具有一个仅适用于特定情况的后备策略,并且可以通过异常类型来区分这些情况。这些方法仅捕获某些特定的异常类型,然后激活适当的后备。
-
绝大多数方法都属于第一类(或者应该归为此类,如果不是过度设计的话)。
-
对于某些方法,开发人员可以创建后备策略。通常,不仅针对特定故障,而且针对任何故障尝试该策略都无害。
-
在极少数情况下,失败原因与选择原本不适当的后备有关,例如如果数据库通过异常告诉我密码已经过期,则代码可以将我重定向到密码更新过程,然后继续执行(一个人为设计的示例)。
实际的异常类型仅在第三种方法类型中起作用,我敢打赌,只有极少数的异常类型以这种方式使用,所以我们有YAGNI的经典案例。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。