统一异常处理+错误编码设计方案
一、背景
1、系统在运行的时候可能会有下面这些种类的错误/失败发生:
(1) 依赖组件挂了,可能是 db,可能是 mq,可能是 cache。
(2)依赖服务挂了,可能是别人给你提供的 http/rpc 服务挂了。
(3)可能是你的依赖方超时了。
(4)可能是调用方的参数有问题。
(5)可能是调用方的参数无法正确地通过校验。
(6)可能是用户的某种操作在业务逻辑上不合理性,不能够接着让他执行下去
(7)还可能是程序自身出错了,比如数组越界,把 null 当成了某种合法的数据结构等等。
上面这些情况都是有很大概率发生的,当这种情况发生的时候,如果用户向你反馈了问题,你要怎么进行跟踪呢?
2、互联网公司在系统内部出了点什么问题的时候,展现给用户的是什么?白屏、无任何响应、nginx 504 Gateway Timeout。用户都不知道这些是什么。
二、目标
1、用户角度
对于产品的用户来讲,希望的是无论任何情况下都要有一个明确的反馈,正常情况下自不用说。而特殊情况下,也应该看得到系统到底出了什么问题,用户网络不行了就告诉用户是网络问题,不要出现一堆莫名其妙的英文。在各种异常情况下,要保证用户能够恢复到正常使用中去。不要显示用户看不懂的任何信息。不要什么都不显示(白屏)。用户的想法会要求在服务端就有完善的错误兜底。而不是写完正常的业务逻辑就完事了。
2、研发角度
实际上就是要有调用链的错误存储逻辑,比如errors 应该是能够一路把上游的错误串下来,而不是直接只存储当前这一级出了什么问题。
错误码最大的好处大概就是能够按照错误码建立自己的业务错误字典,这个字典你甚至可以在客户端进行存储,当用户使用报错的时候可以直接弹出错误原因自查选项以及恢复建议。错误码对于用户和客服,客服和技术人员之间沟通也有很大的好处,至少在软件使用和技术方面上的沟通成本会下降很多。
当看到错误码或者错误信息的时候,能马上找到代码的位置就事半功倍。
三、如何存储
1、所有错误消息都已经确定,可以写在程序里,用枚举比用全局常量更容易。
2、错误消息不能固定,可能在应用过程中添加,保存在数据库中。专门写一套管理类来处理,用 HashTable 或者 HashMap 之类的方式来实现。
3、把错误的key放在java的枚举里面,然后把错误的key和错误的信息的映射放在外部文件中,比如properties文件里。在运行时,根据错误枚举的key来实时从文件中取出错误文本就可以了,因为错误不是经常发生的,实时读取错误信息应该没有问题,当然也可以在程序启动时候把所有的错误信息读进来然后放到缓存里以提高性能。
四、设计错误编码的具体实现
1、统一设计目标
统一展示用户提示信息:编号“-网络异常,请联系相关人员处理!”
例如:x1010001-网络异常,请联系相关人员处理!
2、如何存储
在程序中使用枚举对错误编码进行存储。
3、如何获取
(1)系统bug级别的:直接在枚举中查看错误信息
(2)非系统bug级别的:设计单独的接口获取错误信息
综上所述:为了很好的扩展功能,建议设计单独的接口获取错误信息
4、具体编码设计
第1位(固定,用x标识,没有特殊设计含义,只是为了方便存储)
第2位(错误级别,1为非系统bug,2为系统bug需要改代码)
第3-4位(功能模块)
第5-8位(错误编码,从0001开始,依次顺延)
实例:x1010001
五、开发规范(在代码中使用“抛异常”还是“返回错误码”)
1、对于公司外的 http/api 开放接口必须使用“错误码”;而应用内部推荐异常抛出。
2、跨应用间 RPC 调用,优先考虑使用 Result 方式,封装 isSuccess()方法、“错误码”、“错误简短信息”。
(1)关于 RPC 方法返回方式使用 Result 方式的理由
使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多。
如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
(2)具体实现
统一返回结果展示,定义Result,封装 isSuccess()方法、“错误码”、“错误信息”。并提供ResultUtil工具类方便使用。
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
private boolean success = false;//是否成功
private String code;//状态码
private String message;//信息
private T data;//装载数据
public Result() {}
/**
* @param success 是否成功
*/
public Result(boolean success) {
this.success = success;
}
/**
* @param success 是否成功
* @param message 消息
*/
public Result(boolean success, String message) {
this.success = success;
this.message = message;
}
/**
* @param success 是否成功
* @param message 消息
* @param data 数据
*/
public Result(boolean success, String message, T data) {
this.success = success;
this.message = message;
this.data = data;
}
/**
* @param code 错误码
* @param message 错误信息
*/
public Result(String code, String message) {
this.code = code;
this.message = message;
}
public boolean isSuccess() {
return success;
}
//省略get和set
}
public class ResultUtil {
/**
* return success
* @param data
* @return
*/
public static <T> Result<T> success(T data) {
Result<T> result = new Result<T>();
result.setCode("0");
result.setData(data);
result.setSuccess(true);
result.setMessage("success");
return result;
}
/**
* return success
* @return
*/
public static Result success() {
return success(null);
}
/**
* return error
*
* @param code 错误码
* @param msg 错误信息
* @return
*/
public static Result error(String code, String msg) {
Result result = new Result();
result.setCode(code);
result.setMessage(msg);
return result;
}
/**
* 枚举
*
* @param status
* @return
*/
public static Result error(ErrorCodeEnum status) {
return error(status.getCode(), status.getMsg());
}
}
3、统一异常处理
(1)自定义异常类(运行时异常),并使用全局异常处理类GlobalErrorHandler统一处理所有异常。
若为json,直接返回Result,若为html,返回error页面,并显示错误信息。
(2)具体实现
public class CommonException extends RuntimeException{
private static final long serialVersionUID = 1L;
private String code;
private String msg;
public CommonException(String code,String msg){
this.code = code;
this.msg = msg;
}
public CommonException(ErrorCodeEnum resultEnum){
this.code = resultEnum.getCode();
this.msg = resultEnum.getMsg();
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
@ControllerAdvice
public class GlobalErrorHandler {
private final static String DEFAULT_ERROR_VIEW = "error";//错误信息页
@ExceptionHandler(value = Exception.class)
@ResponseBody
public Object handleException(Exception e,HttpServletRequest request) {
Result obj = new Result();
if (e instanceof CommonException) {
CommonException commone = (CommonException)e;
obj = ResultUtil.error(commone.getCode(), commone.getMsg());
}else{
obj = ResultUtil.error(ErrorCodeEnum.x9999999);
}
//使用HttpServletRequest中的header检测请求是否为ajax, 如果是ajax则返回json, 如果为非ajax则返回view(即ModelAndView)
String contentTypeHeader = request.getHeader("Content-Type");
String acceptHeader = request.getHeader("Accept");
String xRequestedWith = request.getHeader("X-Requested-With");
if ((contentTypeHeader != null && contentTypeHeader.contains("application/json"))
|| (acceptHeader != null && acceptHeader.contains("application/json"))
|| "XMLHttpRequest".equalsIgnoreCase(xRequestedWith)) {
return obj;
} else {
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("code", obj.getCode());
modelAndView.addObject("msg", obj.getMessage());
modelAndView.addObject("url", request.getRequestURL());
modelAndView.setViewName(DEFAULT_ERROR_VIEW);
return modelAndView;
}
}
}