Exception怎么正确打印异常日志

6,768 阅读5分钟

目的

  • 自己之前处理异常太随意,每次写代码都太关注业务逻辑,相反异常的处理就显得很随意,导致线上出现异常很难排查,那么怎么正确打印异常的日志,才能很方便的定位异常,规范异常日志打印会大大提高线上排查问题的效率。

流水线形式加异常日志

  • 开发业务逻辑的时候,完全不考虑异常,等全部开发完成,在流水线的补充异常的处理机制。统一为所有方法打上 try…catch…捕获所有异常记录日志。

错误1:全部交由框架处理

  • 全部交给框架去处理,业务逻辑中不处理。

比较好的方式

  • 框架可以做兜底工作。异常上升到最上层逻辑还是无法处理的话,可以以统一的方式进行异常转换处理那些未知异常。
  • 对于自定义的业务异常,提取异常中的错误码和消息等信息,转换为合适的 API 包装体返回给 API 调用方。注意规范定义简言赅的异常信息。
  • 对于无法处理的系统异常,以 Error 级别的日志记录异常和上下文信息(比如 URL、参数、用户 ID)后,转换为普适的“服务器忙,请稍后再试”异常信息,同样以 API 包装体返回给调用方。

错误2:捕获了异常后直接生吞

  • 直接try-catch,然后就不管了.....占用内存大,日志可读性差。
try{

    //可能存在异常的代码

    ...

}catch (Exception e){

    e.printStackTrace();

}

比较好的方式

  • 通常情况下,生吞异常的原因,可能是不希望自己的方法抛出受检异常,只是为了把异常“处理掉”
  • 也可能是想当然地认为异常并不重要或不可能产生。
  • 但不管是什么原因,不管是你认为多么不重要的异常,都不应该生吞,哪怕加一个日志也好。
  • 直接丢弃异常不记录、不抛出。这样的处理方式还不如不捕获异常,因为被生吞掉的异常一旦导致 Bug,就很难在程序中找到蛛丝马迹。

错误3:丢弃异常的原始信息

  • 自认为是自己知道的异常,只记录自己组装的异常信息。
@GetMapping("wrong")

public void wrong(){

    try {

        //读文件操作

        readFile();

    } catch (IOException e) {

        //原始异常信息丢失  

        throw new RuntimeException("读取文件发生IO异常了!");

    }

}
  • 或者只记录了异常消息,却丢失了异常的类型、栈等重要信息。
@GetMapping("wrong")

public void wrong(){

    try {

        //读文件操作

        readFile();

    } catch (IOException e) {  

        //只保留了异常消息,栈没有记录 

        log.error("文件读取错误, {}", e.getMessage()); 

        throw new RuntimeException("读取文件发生异常了!");

    }

}

比较好的方式

  • 如上异常只知道文件读取错误的Message,至于为什么读取错误、是不是文件不存在,还是没权限,完全不知道。需要打印完整的异常信息。
@GetMapping("right1")

public void right1(){

    try {

        //读文件操作

        readFile();

    } catch (IOException e) {  

        log.error("文件读取错误", e); 

        throw new RuntimeException("读取文件发生IO异常了!");

    }

}

或者

@GetMapping("right2")

public void right2(){

    try {

        //读文件操作

        readFile();

    } catch (IOException e) {  

        throw new RuntimeException("读取文件发生IO异常了", e);

    }

}

错误4:抛出异常时不指定任何消息

@GetMapping("wrong")

public void wrong(){

    try {

        //读文件操作

        readFile();

    } catch (IOException e) { 

        //没有指定任何异常信息 

        throw new RuntimeException();

    }

}
  • 这么写可能觉得永远不会走到这个逻辑,永远不会出现这样的异常。但,这样的异常一旦出现就很难定位。

错误5:打印日志的写法问题

  • 异常信息直接使用+e的写法。反正我之前这样写过。
int i=0;

int y=2;

try {

    int z=y/i;

} catch (Exception e) {

    log.info( "异常信息" ,e);

    log.info( "异常信息:" +e);

}

+e的写法能记录出异常信息,但是对于异常不好定位。

错误6:小心 finally 中的异常

  • 有些时候,我们希望不管是否遇到异常,逻辑完成后都要释放资源,这时可以使用 finally 代码块而跳过使用 catch 代码块。
  • 但要千万小心 finally 代码块中的异常,因为资源释放处理等收尾操作同样也可能出现异常。比如:
 @GetMapping( "wrong" )

public void wrong() {

    try {

        log.info( "try" );

        //finally异常会覆盖掉try里面的异常

        throw new RuntimeException( "try" );

    } finally {

        log.info( "finally" );

        throw new RuntimeException( "finally" );

    }

}

结果

[13:34:42.247] [http-nio-45678-exec-1] [ERROR] [.a.c.c.C.[.[.[/].[dispatcherServlet]:175 ] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: finally] with root cause

java.lang.RuntimeException: finally

至于异常为什么被覆盖,因为一个方法无法出现两个异常。修复方式是,finally 代码块自己负责异常捕获和处理。

@GetMapping("right")

public void right() {

    try {

        log.info("try");

        throw new RuntimeException("try");

    } finally {

        log.info("finally");

        try {

            throw new RuntimeException("finally");

        } catch (Exception ex) {

            log.error("finally", ex);

        }

    }

}

或者可以把 try 中的异常作为主异常抛出,使用 addSuppressed 方法把 finally 中的异常附加到主异常上:

@GetMapping("right2")

public void right2() throws Exception {

    Exception e = null;

    try {

        log.info("try");

        throw new RuntimeException("try");

    } catch (Exception ex) {

        e = ex;

    } finally {

        log.info("finally");

        try {

            throw new RuntimeException("finally");

        } catch (Exception ex) {

            if (e!= null) {

                e.addSuppressed(ex);

            } else {

                e = ex;

            }

        }

    }

    throw e;

}

结果:

java.lang.RuntimeException: try

  at org.geekbang.time.commonmistakes.exception.finallyissue.FinallyIssueController.right2(FinallyIssueController.java:69)

  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

  ...

  Suppressed: java.lang.RuntimeException: finally

    at org.geekbang.time.commonmistakes.exception.finallyissue.FinallyIssueController.right2(FinallyIssueController.java:75)

    ... 54 common frames omitted

错误7:不要把异常定义为静态变量

public class Exceptions {

    public static BusinessException ORDEREXISTS = new BusinessException("兑换失败异常!", 3001);

...

}

然后两个地方异常抛出

@GetMapping("wrong")

public void wrong() {

    try {

        exceptionOne();

    } catch (Exception ex) {

        log.error("exception One error", ex);

    }

    try {

        exceptionTwo();

    } catch (Exception ex) {

        log.error("exception Two error", ex);

    }

}

private void exceptionOne() {

    //这里有问题

    throw Exceptions.ORDEREXISTS;

}



private void exceptionTwo() {

    //这里有问题

    throw Exceptions.ORDEREXISTS;

}

exceptionTwo抛出的异常很有可能是exceptionOne抛出的异常,正确的是每次new一个新的。

public class Exceptions {

    public static BusinessException exceptionExists(){

        return new BusinessException("兑换失败异常!", 3001);

    }

}

备注:参照极客时间 朱晔《Java 业务开发常见错误100例》