异常处理实践

889 阅读5分钟

基础服务中心异常处理实践

如何定义异常

在一个优秀的JAVA应用程序中, 异常处理扮演着重要的角色. 它不是程序的主流程, 但我们要十分小心且优雅的处理. 在我们讨论异常处理之前, 我们首先需要明确什么是异常. 在《Think in Java》一书中对异常定义如下:

异常情形是指阻止当前方法或域继续执行的问题。异常情形和普通问题的区别就是普通问题在当前环境下能够获得足够的信息去处理错误,而异常情形在当前环境下无法获得必要的信息来解决问题,所以不能继续下去。所能做的只是从当前环境跳出,并把问题交给上一级环境,这就是抛出异常时发生的事情

简单来说任何影响代码主流程执行的问题都属于异常, 而当前代码本身没有处理能力, 需要交给其调用者来处理.

异常的分类

Java中为我们定义了三种类型的异常

  • error: 无法处理的异常, 会终止JVM. 编码的无需特别关注. eg, OutOfMemoryError
  • checked exception: 受检查异常, 非代码问题导致. 此类异常必须由代码捕获并处理. eg, IOException
  • unchecked exception: 非受检查异常. 常由代码Bug, 数据错误, 流程错误等导致, jdk不强制要求代码捕获或者申明. eg, NullPointerException

其关系如下

9a64300018fd99e6234576b146b5b968.png

异常的处理

在java中定义了以下关键字用于处理异常

  • throw: 抛出异常
  • throws: 用于方法申明可能会抛出的checked exception. note: unchecked exception与error不需要在方法上显示声明
  • try-catch: 用于捕获代码块内部可能抛出的异常
  • finally: 用于代码块抛出异常后, 处理后续清理资源工作
  • try-with-resource: jdk7以后推荐使用该写法来清理资源

基础业务中心关于异常处理的一些实践

不要在try-catch中抛出异常

在try-catch代码块中抛出异常, 会导致异常传播与预期可能不一致. 正确的做法是减少try-catch范围. 在业务流程中抛出异常. 反例:

try{
   throw new RuntimeException();
}catch(Exception e){
   //do something
}

将checked exception转化为unchecked exception

在编码过程中必然会接触到一些checked exception, 如IOException, SqlException等. 该类异常大部分情况下代码无法处理. 为了避免不必要的异常声明. 建议转化为unchecked exception. 但如果需要处理该类异常, 如io异常后需要重试. 则不建议转化为unchecked exception. 反例

 public Session login(LoginParameter parameter)
            throws UserNotExistException,             
            TooManyLoginFailuresException,
            UnauthorizedDeviceException, 
            LowVersionException, 
            AccessDeniedException {

}

保留日志的上下文

程序发生异常后, 我们经常会打印日志的栈. 但是忽略了日志的上下文. 如果是由于数据或参数导致的异常. 栈信息根本无法为我们提供足够的信息来排查问题. 更好的实践是将异常上下文一起打印 正例

public Session login(LoginParameter parameter)
try{
}catch(Exception e){
   log.error("login error, parameter = {}", parameter.toJson(), e);
}

不要将异常作为流程的分支

异常更多的是因为程序出现不可预期的情况. 而出现这些情况更常见的方法是通知调用者. 将异常交给上层处理. 如果将异常作为流程的分支会出现一些不可预期的情况. 反例

int a;
try{
   //do something
   ++a;
}catch(Exception e){
   a+=10;
}

try-catch代码块范围尽可能地小

  • try catch里面的代码是不会被编译器优化重排的. 因此较大的try-catch对性能有一定影响
  • 更小的try-catch代码块, 更有利于排查问题. 程序可读性更高

使用guava包来优雅的处理异常

guava包中的Throwables提供了大量的方法便于我们优雅的处理异常.

  • getStackTraceAsString: 获取异常栈
  • propagateIfPossible: 传播异常到调用者
  • propagate: 将任何异常类型转转为RuntimeException并抛出

使用spring mvc提供的全局异常处理器

实现spring mvc的HandlerExceptionResolver接口让程序可以对异常进行全局处理, 避免代码分散. 保持代码整洁. 且可以针对不同的异常做个性化的处理 正例

@Override
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    Result result;
    if (BusinessRuntimeException.class.isAssignableFrom(ex.getClass())) {
        //build result & log
    } else {
        //build result & log & do alert
    }
    return new ModelAndView("", "result", result);
}
private String dumpRequestInfo(HttpServletRequest request){
   // return the request info   
}

异常的快速感知

异常的快速感知能力是程序所必须的能力之一, 有助于开发&运维人员快速定位,处理线上异常. 对此基础业务中心使用了两套机制来快速感知异常

  • 异常邮件告警: 程序出现非正常的业务异常时快速邮件告警. 并附带异常栈信息, 上下文等.
alertService.post("sns.mq.error", "convert error, message = {}", body, e);
  • 异常日志业务指标告警: 使用galileo提供的业务指标能力. 实时投递业务指标. 如果业务指标达到指定阈值. 通过短信, 企业邮箱通知开发人员
 this.logger.count((new Builder("alert.posted")).tag("alertKey", alert.getKey()).build(), 1L);

在junit中测试异常流程

异常流程的单元测试我们的实践中的重要一环. 所幸junit提供了优秀的异常验证方式供开发人员使用, 使用注解中的expected来验证是否正确的抛出了异常

 @Test(expected = RuntimeException.class)
 public void updateRichStatusClosed() throws Exception {

总结

异常处理是Java程序中重要且不可分割的一部分, 了解并正确使用异常是每个开发人员必须掌握的技能. 优秀的异常处理机制可以让开发人员更聚焦于你要解决的问题. 代码可读性, 健壮性, 可维护性更高.