一个活生生的案例
本周帮同事排查了一个问题,比较诡异的是他通过测试,并没有找到根本原因,只是发现有对应的错误日志。 但是其实并没有将堆栈信息打印出来。很难看出问题。添加了 e.printStackTrace();
get exception in exter: / by zero
显示出完整的错误信息。
java.lang.ArithmeticException: / by zeroat com.exception.Test.f1(Test.java:24)at com.exception.Test.f2(Test.java:20)at com.exception.Test.main(Test.java:12)
public static void main(String[] args) {try {Test t1 = new Test();t1.f2();} catch (Exception e) {System.out.println("get exception in exter: " + e.getMessage());}}public void f2() {f1();}public void f1() {System.out.println(10 / 0);}
异常实现原理
我们编写了一个Demo
public static void main(String[] args) {try {System.out.println("try{}");} catch (Exception e) {e.printStackTrace();} finally {System.out.println("finally");}}
执行之后,生成.class文件, 通过 javap -v 生产对应的字节码文件。
其中有一个异常表,from、to、target表示字节码的行号,当行号在[from,to]之间出现type异常,就会跳转到target行字节码继续执行。对应代码 也就是cacth的部分。而19到24就是catch-> finally的部分。
Exception table:from to target type0 8 19 Class java/lang/Exception0 8 35 any19 24 35 any
异常性能分析
异常的整体步骤是 new创建异常对象、使用throw异常,打印异常调用链。
new 创建异常
如果我们的函数层级比较深的话,那么整个调用链是比较大。
public class Demo {public static void main(String[] args) {fe();}public static void fe() { fd(); }public static void fd() { fc(); }public static void fc() { fb(); }public static void fb() { fa(); }public static void fa() {RuntimeException e = new RuntimeException("oops!");// 打印strackTraceStackTraceElement[] stackTrace = e.getStackTrace();for (StackTraceElement element : stackTrace) {System.out.println(element);}throw e;}
}
异常抛出
当异常抛出的时候,之后能够catch的异常的栈可以解决,才不会向上抛异常,所以最好的方式及时处理抛出的异常,否则层级太深,对于性能以及问题排查都不好找。
最佳实践
在实际编码的时候,我们不仅仅需要在完成需求的同时,也需要考虑各种异常失败的情况,其实就是Deisgn for fail 。把失败处理当成一种错误处理也是一种业务逻辑,很多时候我们的逻辑代码都是在处理异常场景。比如检验参数、状态、数据异常、三方异常等等。所以在实际的编码中,我们需要考虑哪些地方可能会出现错误,那些地方不会。好的工程师的代码应对异常情况处理的更优雅。