停止异常 'factory' 方法出现在堆栈跟踪中

Stop exception 'factory' methods showing up in stack trace

有时,我不想直接使用 new 创建异常,而是想将创建异常委托给某种工厂或构建器方法。

例如,我正在创建一个使用 Lombok 的 @Builder:

的异常 class
@Builder public class MyException extends Exception

在代码中使用 build() 抛出异常:

throw MyException.build();

build() 方法出现在堆栈跟踪中(@Builder 是第 9 行):

exception.MyException$MyExceptionBuilder.build(MyException.java:9)

显然 Lombok 使用 return new MyException 生成构建方法,但它在堆栈跟踪中无关紧要

可以从堆栈跟踪中删除构建器方法吗?否则,在查看控制台输出时,似乎构建方法失败了。

这是已知的代码味道还是有更好的方法来使用构建器处理异常?

正如您所发现的,问题在于 Throwable 的堆栈跟踪是在构建时确定的,而不是在抛出时确定的。这意味着任何充当 Throwable 工厂的方法最终都会出现在堆栈跟踪中,除非您明确删除它。

有多种方法可以解决这个问题。可能最简单的如下:

throw (MyException) MyException.build().fillInStackTrace();

fillInStackTrace() 方法是 Throwable 上的 public 方法。它与 Throwable 的构造函数最初用于填充堆栈跟踪的方法相同,但可以在其他地方再次调用它以覆盖它。在与 throw 语句相同的行上调用它会导致堆栈跟踪按需要设置。

但是,也有一些缺点:

  • 它依赖于 MyException class 的所有用户在抛出异常时记住执行此操作。如果他们忘记了,堆栈跟踪将是错误的。
  • 需要一个难看的演员表
  • 堆栈跟踪实际上被填充了两次(一次是在构造异常时,一次是在您调用 fillInStackTrace() 时)。这是一种本机方法,可能会很慢,因此这里会影响性能(尽管您可能会争辩说,因为您处于 'exceptional' 环境中,所以您不在乎性能是否最佳)。

我认为解决所有这些缺点的更好方法应该是这样的:


// Remove the @Builder annotation from the exception class,
// and instead bundle all of the complex details in a "details"
// object that is passed to the exceptions constructor.
//
// You can either save the details object as a field (as below),
// extracting the relevant info in the exception's methods,
// or extract everything in the constructor body.

@RequiredArgsConstructor
class MyException extends Exception {
    private final ExceptionDetails details;
}

// Instead, the details class has the builder

@Builder class ExceptionDetails {}

// You can now throw exceptions like this:

throw new MyException(ExceptionDetails.builder()/*...etc...*/.build());

使用这种方法,由于异常构造函数是在与 throw 相同的行上直接调用的,因此堆栈跟踪将按预期自动填充。不需要强制转换,开发人员在使用异常 class 时必须 记住 没有什么特别的事情要做 – 使用 details 对象和构建器有点不寻常和特殊,但是他们被迫并通过构造函数签名提醒他们这样做。

在将异常创建逻辑委托给自然会抛出异常的方法时,我使用类似的方法去除不需要的堆栈跟踪元素。

/**
 * <p>Changes the stack trace of exception (if it has stack trace) so that it ends with last
 * entry before entering a method of the class given with <code>className</code> argument.</p>
 * <p>If the <code>className</code> is not found within stack trace it is unchanged.</p>
 * <p>The method is intended to be used with exception utility and factory classes to avoid
 * polluting stack trace with unneeded entries.</p>
 * @param throwable {@link Throwable} to edit, cannot be null
 * @param className {@link String} as returned by {@link Class#getName()}, cannot be null
 * @param <T> type of {@link Throwable} argument
 * @return {@link Throwable} that was passed in as throwable argument
 */
    public static <T extends Throwable> T reduceStackFrame( T throwable, String className ) {
        StackTraceElement[] trace = throwable.getStackTrace();
        int length = trace.length;
        if( length != 0 ) {
            int reduction = -1;
            for( int i = length - 1; i >= 0; --i ) {
                if( className.equals( trace[i].getClassName() ) ) {
                    reduction = i + 1;
                    break;
                }
            }
            if( reduction > 0 ) {
                StackTraceElement[] reduced = new StackTraceElement[length - reduction];
                System.arraycopy( trace, reduction, reduced, 0, length - reduction );
                throwable.setStackTrace( reduced );
            }
        }
        return throwable;
    }

优点是它可以在 return 语句中从您的工厂方法调用,而不需要对现有用户代码进行任何更改。

请注意,如果堆栈跟踪不可写(不会更改),此方法将不起作用。