在Java中,构造异常对象是"十分"耗时的,其原因是在默认情况下,创建异常对象时会调用父类Throwable
的fillInStackTrace()
方法生成栈追踪信息,JDK中的源码如下:
public synchronized Throwable fillInStackTrace() {
if (stackTrace != null ||
backtrace != null /* Out of protocol state */ ) {
fillInStackTrace(0); // native方法
stackTrace = UNASSIGNED_STACK;
}
return this;
}
在我自己做的测试中,new
一个带有栈追踪信息的Exception
对象要比创建不带追踪信息的对象慢50倍以上。虽然打印调用栈能够精确定位到错误发生的代码所在行,但我们可以考虑一下,真的有必要让所有异常都生成这些信息吗?
我们在开发业务系统的过程中一般都会使用异常机制来实现错误处理逻辑,这些异常通常都可以分成两大类:
业务异常
这些是我们自定义的、可以预知的异常,抛出这种异常并不表示系统出了问题,而是正常业务逻辑上的需要,例如用户名密码错误、参数错误等。系统异常
往往是运行时异常,比如数据库连接失败、IO失败、空指针等,这种异常的产生多数表示系统存在问题,需要人工排查定位。
其实对于业务异常,我们只需要简单的知道一个描述问题的字符串即可,栈追踪信息对我们的意义并不大。而对于系统异常,追踪信息才是排查错误不可或缺的参考。因此我们可以想办法控制一下,创建业务异常时不生成调用栈追踪信息以降低开销,系统异常则正常生成。
其实方法非常简单,在我们自定义异常时,只需要重写父类的一个带有4个参数的构造方法即可,此方法在Exception
和RuntimeException
类中都存在:
protected RuntimeException(String message, Throwable cause,
boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
这几个参数的意义如下:
- message
异常的描述信息,也就是在打印栈追踪信息时异常类名后面紧跟着的描述字符串 - cause
导致此异常发生的父异常,即追踪信息里的caused by
- enableSuppress
关于异常挂起的参数,这里我们永远设为false
即可 - writableStackTrace
表示是否生成栈追踪信息,只要将此参数设为false
, 则在构造异常对象时就不会调用fillInStackTrace()
例如,业务异常可以这样定义:
public class XXXException extends RuntimeException {
/**
* 仅包含message, 没有cause, 也不记录栈异常, 性能最高
* @param msg
*/
public XXXException(String msg) {
this(msg, false);
}
/**
* 包含message, 可指定是否记录异常
* @param msg
* @param recordStackTrace
*/
public EngineException(String msg, boolean recordStackTrace) {
super(msg, null, false, recordStackTrace);
}
/**
* 包含message和cause, 会记录栈异常
* @param msg
* @param cause
*/
public EngineException(String msg, Throwable cause) {
super(msg, cause, false, true);
}
}
即通过使用父类中4参数的构造方法精确控制异常类的行为。当我们想要创建"轻量级"异常时,使用第一个构造方法即可;如果我们想将系统级异常封装成一下,并希望在日志中打印栈追踪时,就使用第三个构造方法。
PS: 只有在高并发系统中做上述优化才会有明显效果。如果抛异常不频繁的话也不会有明显效果,因为即便是慢50倍,实际也是纳秒级的区别,对一个请求处理来说微不足道。