异常处理好,下班回家早_第1张图片

古时的风筝原创系列

API (Application Programming Interface)对我们来说那简直太熟悉了,整个编程过程充满了各种 API 的调用,无论是系统 API、JDK API、同一项目下 service 层 API,还是第三方 API,例如 RESTful 接口。

我们这里姑且分为内部用 API 和外部用 API。内部用可以理解为本团队使用,可以将完整异常信息暴露出去的 API。外部用可以理解为供本团队外的开发者使用,例如 RESTful 接口、dubbo 接口,不希望暴露完整的异常出去的 API。

不论是哪种情况,正确的处理 API 产生的异常都是非常重要的,更好的处理异常,可能帮我们更快速的定位问题以及指引使用者正确的使用 API。

相信有不少同学碰到过异常处理不到位产生的问题,比如我就碰到过一个让人哭笑不得的异常处理。

很早之前,我刚接手一个Spring MVC 项目,也没什么说明文档,说实话,项目也不是很复杂,就是服务多一点而已。我在本机启动,先启动了几个 dubbo 服务,然后启动 UI 服务(没有前后端分离,用的是 freemarker),启动也正常,然后开心的访问登录页,结果来到了一个令人难过的页面 404 页面。好吧,有问题就解决,开发过程中,碰到问题才正常,碰不到问题反而有点奇怪,于是乎,开始寻找问题,404 一般来说大概率是配置有问题,于是开始查配置文件,看是不是有的地方配置不通,但是,经过一番查找还是没发现问题,这就有点不对劲了,那肯定是有什么特殊的配置,翻了一遍文档,但是这文档写的,有和没有并没有多大的差别。咨询了一下之前的开发同事,回复是好像没有什么特殊的配置。

那好吧,debug 一下,后台 Controller 接口还没执行到就报错了,那肯定是有全局异常捕获或者有拦截器之类的,没错,所有 Controller 都继承了 BaseController ,而 BaseController 里有全局异常捕获的方法,最奇葩的地方来了,捕到异常直接返回 404,这也可以,这是没开发完还是故意迷惑对手。我对这波操作表示由衷的佩服。

异常处理好,下班回家早_第2张图片

而为什么会报异常,那又是另一个故事了,简单来说就是,用户认证服务用了一个第三方的组件,这个组件可以配置域名白名单,必须在白名单里的域名才能正常请求,所以本机调试必须要配置上 host 对应上白名单里的域名才可以。话说这么重要的信息不应该写作文档里吗。

内部 API

那 MVC 分层架构来说,Controller、Service、Dao 层可能由不同的人开发,那不同层之间的调用,或者同一项目中不同模块之间的调用,或者一些提供公共方法的包等,都可以理解为内部 API 调用。

内部用的 API 处理起来比较灵活,每个团队可能都有不同的标准。共同的一点是,异常都可以完全的暴露出去给调用方,当然,也可以选择在 API 方法中自行处理。

内部用 API 异常处理应该遵循以下准则:

1、所有准则中,有一条是必须要遵守的,那就是异常捕获了就必须处理,不能抓了异常后默默把异常吃掉。不知道你有没有碰到过这种情况,异常抓了连日志都不打一下。

2、如果捕获了异常不想自己处理,那应该抛出去,让调用方自行处理。

3、避免把大段代码都放到 try... catch 中,应该尽可能的只包括住可能发生异常的代码。

4、 try...catch 中如果包含异常后要回滚的操作,一定要手动回滚,不要给调用方带来不必要的麻烦。

5、尽量不要直接抛 Excepiton 或 Throwable,应该使用业务相关的自定义异常类。例如定义好 DaoException、ServiceException。

例如下面这个 ServiceExcepiton 类的定义,有错误信息和异常码。

public class ServiceException extends RuntimeException {

   public ServiceException(String message){
       super(message);
  }

   public ServiceException(String message,Throwable cause){
       super(message,cause);
  }

   public ServiceException(String message, String errorCode){
       this(message);
       this.errorCode = errorCode;
  }

   public ServiceException(String message, String errorCode, Throwable cause){
       this(message,cause);
       this.errorCode = errorCode;
  }

   private String errorCode;

   public String getErrorCode() {
       return errorCode;
  }

   public void setErrorCode(String errorCode) {
       this.errorCode = errorCode;
  }
}

另外,内部不同模块,不同系统之间的调用有可能要通过 dubbo 或者其他 HTTP 或者非 HTTP 的 RPC 调用,可以参考下面的外部 API 异常处理方式。

外部 API

对于外部用的 API ,一般采用 HTTP 方式,直接返回完整异常信息肯定是不可取的。对调用方来说没什么用处。二来也不是很安全,完整的堆栈信息容易暴露过多的服务器信息。另外,堆栈信息一般都很大,会造成网络传输等方面的性能损耗。

如果你做过公众号开发,那肯定知道微信开发文档里定义的全局返回码。

异常处理好,下班回家早_第3张图片

提供给外部使用的 API 一般要遵循如下准则:

1、定义统一的返回格式,不能同一套系统中开放出去的接口返回格式不一致,这就很不专业了,一般由一个状态码表示返回成功或异常。

内部可以定义一个 Result 泛型类来实现统一的格式输出。

public class Result implements Serializable {
   private static final long serialVersionUID = 1L;
   private T result;

   private String code;

   private String message;

   private boolean success;

   private Result() {
  }

   private Result(T result, String code, String message, boolean success) {
       this.result = result;
       this.code = code;
       this.message = message;
       this.success = success;
  }

   public static  Result ok(T result) {
       return new Result(result, null, null, true);
  }

   public static  Result fail(String code, String message) {
       return new Result((Object)null, code, message, false);
  }
}

2、定义与状态码对应的异常信息提示,一定要能准确的表示异常发生的原因,帮助使用者定位问题。

异常状态码和描述信息可以通过一个枚举类来实现

public enum ErrorCodeEnum {

   USER_NOT_EXIST("用户不存在", 400001),

   PARAMS_PART_ID_ERROR("参数 partId 错误", 400002);

   private String message;
   private int code;

   private ErrorCodeEnum(String message, int code) {
       this.message = message;
       this.code = code;
  }

   public String getMessage() {
       return this.message;
  }

   public int getCode(){
       return this.code;
  }
}

3、提供完备的接口文档,其中对应异常部分就是状态码对应的信息描述说明,例如上图微信开发者文档。

总结

以上的异常处理实际上就是团队中的代码规范,每个公司、每个团队可能会有不同,但基本原则都类似。异常处理只是项目中很细节的问题,但往往很多问题都出在细节上,看似简单,但却要提早规范,引起重视。

有一些准则直接参考了 《阿里巴巴Java开发手册(终极版)》,没读过的同学可以到网上搜索一份,也可以在本公众号内回复 「阿里巴巴」获取下载链接。