前言
Java异常是在Java应用中的警报器,在出现异常的情况下,可以帮助我们程序猿们快速定位问题的类型以及位置。但是一般在我们的项目中,由于经验阅历等多方面的原因,依然有若干的童鞋在代码中没有正确的使用异常打印方法,导致在项目的后台日志中,没有收到日志或者日志信息不完整等情况的发生,这些都给项目埋下了若干隐患。本文将深入分析在异常日志打印过程中的若干情况,并给出若干的使用建议。
1. Java异常Exception的结构分析
我们通常所说的Exception主要是继承于Throwable而来,可以参见如下的结构图示:
主要的Throwable分为异常和错误两种,然后异常Exception和错误Error做为基类,分别被具体个性化以及衍生出NullPointerException、EOFException等等异常信息类。
基于Java中的源代码来分析,Error和Exception仅仅是继承了Throwable,做了构造函数的拓展,没有进行额外方法的延展;Exception输出的主要核心方法都是定义在Throwable中的,感兴趣的童鞋可以尝试阅读JDK的源代码。
下面将介绍一下关键的几个异常类方法:
1、getMessage(): String
输出异常的描述信息
2、getLocalizedMessage()
输出本地化的描述信息,一般此方法可被子类所覆盖,缺省实现与getMessage()输出信息一致
3、printStackTrace()
将异常栈打印到输出流中,此为一类方法,默认打印到console控制台,也可以显式指定输出流。
4、fillInStackTrace()
将当前的异常栈保存到一个Throwable中,返回这个Throwable。大部分情况下,用在保留异常栈嵌套调用的情况,尝试保留完整的异常栈,无需使用该方法。
2. Error vs Exception
Error在Java体系中定义为不可控制的问题,往往用来描述系统错误或者底层的问题,比如虚拟机错误,例如内存空间不足,方法调用栈溢等。我们以上图中列举出的内存溢出错误(*Error)为例,它是在JVM层面发生的错误,已经游离于java应用层之外;在应用程序层面是无法进行捕获,且无法从错误中恢复的。一般一旦发了类似问题,一般都是直接宕机,应用停止正常的工作,需要重新启动或者修复问题之后,方可重新正常工作。
Exception一般发生在应用层,即在由项目中的Java代码层面引发的问题,且可以尝试进行捕获,此类问题不会影响到应用程序的正常工作的,即不会导致宕机现象的发生。我们在工作或者代码中常见的都是Exception衍生出来的各类异常。
这里需要强调说明一下,JVM是Java语言的运行环境和平台,但是并不是Java语言体系的一个部分;在JVM平台上,还可以运行Groovy, JPython, JRuby, Closure,Scala等等遵守Java语言规范(JavaLanguage Specification)的编程语言,故我们可以将Error理解为脱离Java应用之外的问题。
3. Exception中的运行时异常(RuntimeException)和受控异常(checked exception)
运行时异常(RuntimeException)是指在运行之时发生的异常,无需显式地进行捕获;如果程序中发生类似的异常,JVM会直接抛出此类异常,并打出响应的异常栈信息。此类异常也通常被称为unchecked exception, 未受控异常。
受控异常(checked Exception)是我们最常见的异常种类,在代码中使用的异常基本上都是此类异常,此类异常会在代码编译阶段由Java编译器进行语法检查,如果未显式进行异常捕获,则会报出相应的编译异常信息。
4. 如何在代码中正确打印异常信息
下面我们将通过一系列的例子来说明上述几个Exception中方法的使用技巧。
Case 1: getMessage()/getLocalizedMessage()
1
2
3
4
5
6
7
8
9
|
public void testCase1() {
System.out.println( "We are going to do something interesting...." );
try {
throw new NullPointerException( "I am an exception in the code." );
} catch (Exception e) {
System.out.println( "We got unexpected:" + e.getMessage());
System.out.println( "We got unexpected:" + e.getLocalizedMessage());
} } |
输出结果:
1
2
3
|
We are going to do testing interesting....
We got unexpected in getMessage==> I am an exception in the code. We got unexpected in getLocalizedMessage==> I am an exception in the code. |
基于结果来分析, 上述两个方法只是将异常对象中的Message打印出来,这些信息对于我们追踪问题和调试帮助有限。
Case 2:e.printStackTrace()
1
2
3
4
5
6
7
8
|
public void testCase2() {
System.out.println( "We are going to do something interesting...." );
try {
throw new Exception( "I am an exception in the code." );
} catch (Exception e) {
e.printStackTrace(); } } |
运行结果:
运行结果图
printStackTrace()
可以打印出整个异常栈,但是异常栈信息将输出到默认的输出流中,绝大多数情况下是系统的控制台,而在实际项目中,都是需要将异常栈输出到日志文件的,如果不显式指定,则会丢失异常信息,在日志文件中无从追查。
Case 3: 基于log4j/slf4j等输出到日志文件
在实际项目中,一般会使用log4j/log4j2/JDK logging/slf4j/logback等日志系统来存放日志,那如何来讲日志的异常栈存入日志文件呢,我们来看示例。
1
2
3
4
5
6
7
8
|
public void testCase3() {
System.out.println( "We are going to do something interesting...." );
try {
throw new NullPointerException( "abcedfeg" );
} catch (Exception e) {
logger.info( "Unexpected error in " , e);
} } |
我们需要到日志文件中,找到相应的异常信息,异常信息如下:
1
2
3
4
|
12 : 24 : 45.387 [main] INFO org.demo.TestException - Unexpected error in
java.lang.NullPointerException: abcedfeg at org.demo.TestException.testCase3(TestException.java: 39 )
at org.demo.TestException.main(TestException.java: 12 )
|
我们可以发现,整个异常栈信息由两个部分组成:
>> 异常中的message, 类似getMessage()
输出的信息,
>> 使用logger.info
之类的方法,将异常信息写入到日志流中.
以下为log4j的声明,这里以slf4j为例来示例:
1 |
private static final Logger logger = LoggerFactory.getLogger(TestException. class );
|
Case 4: fillInStackTrace()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class FillInExceptionTest {
public static void main(String[] args) {
FillInExceptionTest fit = new FillInExceptionTest();
try {
fit.outerMehtod(); } catch (Exception e) {
System.out.println( "\n==========I am the one evil separation line==============" );
e.printStackTrace(); } } public void innerMethod() throws Exception {
throw new Exception( "I got exception in an inner method." );
} public void outerMehtod() throws Exception {
try {
innerMethod(); //invoke inner method.
} catch (Exception e) {
e.printStackTrace(); throw (Exception)e.fillInStackTrace();
} } } |
运行结果:
基于上述的运行结果可知,fillInStackTrace()
只提取了当下的异常栈信息,而非完整的异常栈信息,这个就是此方法带给我们的特殊之处。
如果我们需要在最外层将完整的异常栈打印出来,该如何做呢? 将下述的语句:
1 |
throw (Exception)e.fillInStackTrace();
|
替换为:
1 |
throw e;
|
重新运行程序,我们就可以在最外层得到完整的异常栈信息了。
5. 总结
在本文中,我们介绍了异常类的继承体系,不同类型的异常区别与使用场景;并将基于代码示例展示了如何使用Exception的若干方法,利用这些方法来保留尽可能多的日志信息,方便我们后续针对日志中的异常信息,追查和解决问题。
getStackTrace和printStackTrace的区别
在JAVA中收到程序报错,将堆栈信息打印出来是一个好习惯,但是在catch到exception之后,发现有两个方法都和堆栈信息有关,一个是getStackTrace,一个是printStackTrace,那么他们的区别是什么?
我们从源码的角度来分析:
(1)getStackTrace()
- public StackTraceElement[] getStackTrace() {
- return getOurStackTrace().clone();
- }
- private synchronized StackTraceElement[] getOurStackTrace() {
- // Initialize stack trace field with information from
- // backtrace if this is the first call to this method
- if (stackTrace == UNASSIGNED_STACK ||
- (stackTrace == null && backtrace != null) /* Out of protocol state */) {
- int depth = getStackTraceDepth();
- stackTrace = new StackTraceElement[depth];
- for (int i=0; i < depth; i++)
- stackTrace[i] = getStackTraceElement(i);
- } else if (stackTrace == null) {
- return UNASSIGNED_STACK;
- }
- return stackTrace;
- }
- public void printStackTrace() {
- printStackTrace(System.err);
- }
- /**
- * Prints this throwable and its backtrace to the specified print stream.
- *
- * @param s {@code PrintStream} to use for output
- */
- public void printStackTrace(PrintStream s) {
- printStackTrace(new WrappedPrintStream(s));
- }
- private void printStackTrace(PrintStreamOrWriter s) {
- // Guard against malicious overrides of Throwable.equals by
- // using a Set with identity equality semantics.
- Set<Throwable> dejaVu =
- Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
- dejaVu.add(this);
- synchronized (s.lock()) {
- // Print our stack trace
- s.println(this);
- StackTraceElement[] trace = getOurStackTrace();
- for (StackTraceElement traceElement : trace)
- s.println("\tat " + traceElement);
- // Print suppressed exceptions, if any
- for (Throwable se : getSuppressed())
- se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);
- // Print cause, if any
- Throwable ourCause = getCause();
- if (ourCause != null)
- ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
- }
- }
getStackTrace()返回的是通过getOurStackTrace方法获取的StackTraceElement[]数组,而这个StackTraceElement是ERROR的每一个cause by的信息。
printStackTrace()返回的是一个void值,但是可以看到其方法内部将当前传入打印流锁住,然后同样通过getOurStackTrace方法获取的StackTraceElement[]数组,只不过printStackTrace()方法直接打印出来了。而getStackTrace()则是得到数组,使用者可以根据自己的需求去得到打印信息,相比printStackTrace()会更细一些。
这里指的一提的是slf4j的log方法,如果放入Exception对象,则会自动打印出其堆栈信息,不必要再专门去转流写入日志。