目的
- 自己之前处理异常太随意,每次写代码都太关注业务逻辑,相反异常的处理就显得很随意,导致线上出现异常很难排查,那么怎么正确打印异常的日志,才能很方便的定位异常,规范异常日志打印会大大提高线上排查问题的效率。
流水线形式加异常日志
- 开发业务逻辑的时候,完全不考虑异常,等全部开发完成,在流水线的补充异常的处理机制。统一为所有方法打上 try…catch…捕获所有异常记录日志。
错误1:全部交由框架处理
- 全部交给框架去处理,业务逻辑中不处理。
比较好的方式
- 框架可以做兜底工作。异常上升到最上层逻辑还是无法处理的话,可以以统一的方式进行异常转换处理那些未知异常。
- 对于自定义的业务异常,提取异常中的错误码和消息等信息,转换为合适的 API 包装体返回给 API 调用方。注意规范定义简言赅的异常信息。
- 对于无法处理的系统异常,以 Error 级别的日志记录异常和上下文信息(比如 URL、参数、用户 ID)后,转换为普适的“服务器忙,请稍后再试”异常信息,同样以 API 包装体返回给调用方。
错误2:捕获了异常后直接生吞
- 直接try-catch,然后就不管了.....占用内存大,日志可读性差。
php复制代码try{
//可能存在异常的代码
...
}catch (Exception e){
e.printStackTrace();
}
比较好的方式
- 通常情况下,生吞异常的原因,可能是不希望自己的方法抛出受检异常,只是为了把异常“处理掉”
- 也可能是想当然地认为异常并不重要或不可能产生。
- 但不管是什么原因,不管是你认为多么不重要的异常,都不应该生吞,哪怕加一个日志也好。
- 直接丢弃异常不记录、不抛出。这样的处理方式还不如不捕获异常,因为被生吞掉的异常一旦导致 Bug,就很难在程序中找到蛛丝马迹。
错误3:丢弃异常的原始信息
- 自认为是自己知道的异常,只记录自己组装的异常信息。
typescript复制代码@GetMapping("wrong")
public void wrong(){
try {
//读文件操作
readFile();
} catch (IOException e) {
//原始异常信息丢失
throw new RuntimeException("读取文件发生IO异常了!");
}
}
- 或者只记录了异常消息,却丢失了异常的类型、栈等重要信息。
typescript复制代码@GetMapping("wrong")
public void wrong(){
try {
//读文件操作
readFile();
} catch (IOException e) {
//只保留了异常消息,栈没有记录
log.error("文件读取错误, {}", e.getMessage());
throw new RuntimeException("读取文件发生异常了!");
}
}
比较好的方式
- 如上异常只知道文件读取错误的Message,至于为什么读取错误、是不是文件不存在,还是没权限,完全不知道。需要打印完整的异常信息。
typescript复制代码@GetMapping("right1")
public void right1(){
try {
//读文件操作
readFile();
} catch (IOException e) {
log.error("文件读取错误", e);
throw new RuntimeException("读取文件发生IO异常了!");
}
}
或者
代码语言:javascript复制typescript复制代码@GetMapping("right2")
public void right2(){
try {
//读文件操作
readFile();
} catch (IOException e) {
throw new RuntimeException("读取文件发生IO异常了", e);
}
}
错误4:抛出异常时不指定任何消息
代码语言:javascript复制typescript复制代码@GetMapping("wrong")
public void wrong(){
try {
//读文件操作
readFile();
} catch (IOException e) {
//没有指定任何异常信息
throw new RuntimeException();
}
}
- 这么写可能觉得永远不会走到这个逻辑,永远不会出现这样的异常。但,这样的异常一旦出现就很难定位。
错误5:打印日志的写法问题
- 异常信息直接使用 e的写法。反正我之前这样写过。
ini复制代码int i=0;
int y=2;
try {
int z=y/i;
} catch (Exception e) {
log.info( "异常信息" ,e);
log.info( "异常信息:" e);
}
错误6:小心 finally 中的异常
- 有些时候,我们希望不管是否遇到异常,逻辑完成后都要释放资源,这时可以使用 finally 代码块而跳过使用 catch 代码块。
- 但要千万小心 finally 代码块中的异常,因为资源释放处理等收尾操作同样也可能出现异常。比如:
typescript复制代码 @GetMapping( "wrong" )
public void wrong() {
try {
log.info( "try" );
//finally异常会覆盖掉try里面的异常
throw new RuntimeException( "try" );
} finally {
log.info( "finally" );
throw new RuntimeException( "finally" );
}
}
结果
代码语言:javascript复制ini复制代码[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 代码块自己负责异常捕获和处理。
代码语言:javascript复制php复制代码@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 中的异常附加到主异常上:
代码语言:javascript复制php复制代码@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;
}
结果:
代码语言:javascript复制php复制代码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:不要把异常定义为静态变量
代码语言:javascript复制java复制代码public class Exceptions {
public static BusinessException ORDEREXISTS = new BusinessException("兑换失败异常!", 3001);
...
}
然后两个地方异常抛出
代码语言:javascript复制typescript复制代码@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一个新的。
代码语言:javascript复制csharp复制代码public class Exceptions {
public static BusinessException exceptionExists(){
return new BusinessException("兑换失败异常!", 3001);
}
}
备注:参照极客时间 朱晔《Java 业务开发常见错误100例》