打破其他人代码中未处理的异常

Breaking on unhandled exceptions in other people's code

注意:整个讨论都是关于未经检查的异常。已检查的异常与我在这里谈论的内容无关。

所以,我将我的 Intellij IDEA 调试器配置为仅在未处理的异常时中断。

当然,如果没有一些额外的爱,这是行不通的,因为 try-with-resources 等语言结构捕获并重新抛出,从而导致调试器在抛出异常的地方中断,而是在异常被重新抛出的那一点上,这是无用的,但我已经经历了投入所有必要的额外爱的麻烦(我会免除你的细节)并且我已经让事情工作得相当好。

因此,当我的代码中的任何地方抛出异常时,我永远不必通过仔细研究 post-mortem 日志中的堆栈跟踪来 猜测 出了什么问题;让调试器在 throw 语句处停止,我可以 查看 出了什么问题。

这在大多数情况下都运行良好;具体来说,只要涉及的所有代码都是 my 代码,它就可以正常工作。不幸的是,有时我也不得不处理其他人的代码。

当我调用 Jim 的函数,而 Jim 的函数又调用我的函数,而我的函数抛出异常时,此异常通常不会被视为未处理的异常,因为 Jim 的函数通常包含 try-catch。当这种情况发生时,根据 Jim 在他的 try-catch 语句中所做的事情,调试器要么在 Jim 的代码中的某个地方停止,要么根本不会停止,如果幸运的话,日志中会有堆栈跟踪。无论哪种情况,我的目标都不会实现:调试器不会在 throw 语句处停止。

例如,如果我用 Swing 注册了一个观察者,Swing 调用了我的观察者,而我的观察者抛出了一个未处理的异常就我而言,这个异常对于Swing来说肯定是不会unhandled的,因为Swing至少在它的Event Dispatcher Thread的主循环中有一个try-catch。因此,调试器永远不会中断 throw 语句。

那么,我的问题是:

我能做些什么来说服调试器在我认为未处理的异常时停止吗?

换句话说:有什么方法可以让调试器知道我的代码的边界是什么,以便它可以在遇到越过这些边界的异常时停止?

请注意,我不一定有更改 throw 语句的自由:我可能会依次调用第三个库,它可能会抛出异常,或者我可能会调用一些代码我的是通用的,所以它的 throw 语句需要保持原样,因为可能存在一些测试代码以确保它在正确的情况下抛出预期的异常。

如果重要的话,我正在使用 IntelliJ IDEA。

所以,一个月后,这就是我解决这个问题的方法。

(还好没有涉及到任何习俗thread-pool。)

DL;DR 版本:

  1. 在代码的每个 entry-point 处,添加一条语句,通过 special-purpose“调试”class 重定向执行流程,捕获您抛出的所有异常代码并重新抛出它们。
  2. 使用调试器的“捕获 class 过滤器”功能来指定您希望调试器不仅在未捕获的异常时停止,而且在捕获到异常时停止 special-purpose”调试" class.

有一些微妙之处,所以如果您尝试这样做,请务必阅读长版。

长版:

请注意,我在这里以包名为例,因为我需要进一步参考它。可以使用任何适合自己的包名,只要一致即可。

这里是special-purpose“调试”class:

package mikenakis.debug;

import java.util.function.Supplier;

public final class Debug
{
    public static boolean expectingException;

    private Debug()
    {
    }

    /**
     * Invokes a given {@link Runnable}, allowing the debugger to
     * stop at the throwing statement of any exception that is 
     * thrown by the {@link Runnable}, even if the caller of this
     * method has a catch-all clause.
     *
     * For this to work, the debugger must be configured to stop
     * not only on uncaught exceptions, but also on caught
     * exceptions if (and only if) they are caught within this
     * class.
     *
     * @param procedure0 the {@link Runnable} to invoke.
     */
    public static void boundary( Runnable procedure0 )
    {
        if( expectingException )
        {
            procedure0.run();
            return;
        }
        //noinspection CaughtExceptionImmediatelyRethrown
        try
        {
            procedure0.run();
        }
        catch( Throwable throwable )
        {
            throw throwable;
        }
    }

    /**
     * Invokes a given {@link Supplier} and returns the result,
     * allowing the debugger to stop at the throwing statement of
     * any exception that is thrown by the {@link Supplier}, even
     * if the caller of this method has a catch-all clause.
     *
     * For this to work, the debugger must be configured to stop
     * not only on uncaught exceptions, but also on caught
     * exceptions if (and only if) they are caught within this
     * class.
     *
     * @param function0 the {@link Supplier} to invoke.
     * @param <T>       the type of the result returned by the
     *                  {@link Supplier}.
     *
     * @return whatever the {@link Supplier} returned.
     */
    public static <T> T boundary( Supplier<T> function0 )
    {
        if( expectingException )
            return function0.get();
        //noinspection CaughtExceptionImmediatelyRethrown
        try
        {
            return function0.get();
        }
        catch( Throwable throwable )
        {
            throw throwable;
        }
    }
}

注意:expectingException 标志用于测试某些 methods-under-test 在某些情况下抛出某些异常的测试代码。显然,在这种情况下,我们希望 method-under-test 抛出的异常在调试器不停止的情况下传播到测试方法,因此此标志用于暂时禁用 Debug [=150= 的功能].

这里是如何配置 IntelliJ IDEA 调试器以在被 Debug class:

  • 转到“运行”->“查看断点...”
    • 在“Java 异常断点”下:
      • 您应该已经有一个“任何异常”条目,其中“捕获的异常”是未检查,“未捕获的异常”是checked 以便您的调试器始终在任何地方抛出任何未捕获的异常时停止。我们现在将再添加一个异常断点,这样您的调试器就可以在我上面显示的 Debug class 中的任何方法捕获的捕获异常时停止。
      • 添加一个Java异常断点。
      • 将例外 class 设置为 java.lang.Throwable
        • PEARL:在“输入例外 Class”对话框的“按名称搜索”选项卡中,如果选中 'Include non-project items' 框并开始键入 'Throwable' 或 'java.lang.Throwable',IntelliJ IDEA 将 fail/refuse 找到 Throwable,大概是因为它已被他们已经提供的 'Any exception' 条目覆盖,他们无法想象为什么有人会需要为同一异常添加一个条目 class。幸运的是,他们阻止你找到 Throwable 的机制是 half-assed,因此可以绕过它,如下所示:
          • 切换到 'Project' 选项卡而不是 'Search by Name' 选项卡。
          • 导航至 'External Libraries',然后导航至您的 JDK,然后导航至 java.base 模块,然后导航至 java.lang 程序包
          • 找到 Throwable 并 select 它。
      • 确保检查.
      • “捕获异常”
      • 选中“捕获 class 个过滤器:”复选框并输入 mikenakis.debug.Debug
      • PEARL:IntelliJ IDEA 的“断点”对话框包含许多需要 class 名称的文本框,在这些文本框中的任何一个中,您都可以输入任何随机垃圾,而 IntelliJ IDEA会欣然接受,丝毫没有暗示是错误的。然后,当你期望它坏掉的时候,它会悄悄地坏掉。为避免这种情况,切勿在任何这些文本框中键入 class 名称,始终使用导航功能找到保证存在的 class。 (如果您决定重命名或移动 class,祝您好运。)

以下是您在每个 entry-point 代码中必须执行的操作:

这是您可能编写的从外部代码调用的一段代码示例。这个特定的示例是一个 WindowListener,您将向 AWT/Swing 注册,然后 AWT/Swing 将调用它并捕获它抛出的任何异常。请注意我们如何强制执行流程通过 Debug class.

private final WindowListener windowListener = new WindowAdapter()
    {
        @Override public void windowActivated( WindowEvent e )
        {
            Debug.boundary( () -> onWindowActivationStateChanged( true ) );
        }
        @Override public void windowDeactivated( WindowEvent e )
        {
            Debug.boundary( () -> onWindowActivationStateChanged( false ) );
        }
    };

private void onWindowActivationStateChanged( boolean active )
{
    throw new RuntimeException( "Ooops, I had an accident!" );
}

如果您的 onWindowActivationStateChanged() 方法无意中抛出:

  • 没有Debug.boundary():

    • 你甚至可能没有意识到它抛出的事实,除非你一直关注日志,而你不太可能这样做,因为 AWT/Swing 你的注意力可能集中在window 您正在开发的 GUI 应用程序,而不是 IDE.
    • 的日志记录面板
    • 在任何情况下,您的调试器都不会中断抛出语句,这正是整个考验的目标。
  • Debug.boundary()和Intellij IDE上面描述的调试器配置:

    • 调试器将在抛出语句时中断。