停止异常 '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 语句中从您的工厂方法调用,而不需要对现有用户代码进行任何更改。
请注意,如果堆栈跟踪不可写(不会更改),此方法将不起作用。
有时,我不想直接使用 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 语句中从您的工厂方法调用,而不需要对现有用户代码进行任何更改。
请注意,如果堆栈跟踪不可写(不会更改),此方法将不起作用。