在使用spring或springboot开发后端项目,特别是前后端分离的后端项目时,通常会涉及到统一的返回数据格式,这样的设计方式有利于前后端松耦合和项目工程化。在系统中也往往会发生系统性或业务性的异常,这时会涉及到统一的错误类型与异常处理。
关于统一的返回数据格式、错误类型、异常处理的设计有很多,这里是我经常使用的一种实现方式,会涉及到统一的返回类型类,错误类型枚举,错误类型接口,业务异常类,异常处理方法。当然如果嫌麻烦也可以使用springmvc提供的ResponseEntity<>类,不过个人感觉springmvc提供的原生方式不够灵活。下面,来看看我的吧(手动挡脸)。
1.统一的返回类型类
如果仅仅是做统一的返回数据格式还是比较简单的,只需提供一个统一的返回类型pojo类即可。这个类的具体对象就是我们最终会返回给前端的统一数据格式,当然前提是它会被序列化为JSON。可参考如下代码
public class CommonReturnType {
private String status;
private Object content;
public static CommonReturnType success(Object content){
return new CommonReturnType("success",content);
}
public static CommonReturnType fail(Object content){
return new CommonReturnType("fail",content);
}
/**** constructor,setter,gettter ****/
}
在这个pojo类中只提供两个成员变量,其中status的值只能是success或fail,用于告知前端是否成功请求到了所需的数据;content用于存放实际返回的数据,当然如果没有成功请求到所需的数据content中就是相关的错误信息。同时提供两个静态的自创建方法,减少new对象的麻烦。另外提供的有参/无参构造方法,get/set方法都是默认实现即可就不展示在这里了。
2.统一的错误类型
值得注意的是,如果只是想提供统一的错误类型枚举,是不用提供错误类型接口的,但是错误类型往往与异常处理相联系,为了下一步包装一个业务异常类,所以这里先提供错误类型接口,再提供错误类型枚举,如果看到这个接口有什么不明白的可以等到看完第三步再回来看一下,或者了解一下包装器模式。
错误类型接口如下
public interface CommonError {
public int getErrCode();
public String getErrMsg();
public Map<String,Object> getErrData();
public CommonError setErrMsg(String errMsg);
}
有了错误类型接口就可以定义统一的错误类型枚举,如下
//统一的错误类型枚举
public enum CommonErrorEnum implements CommonError{
//10000通用错误
UNKNOWN_ERROR(10001,"未知错误"),
PARAMETER_VALIDATION_ERROR(10002,"参数不合法"),
//20000用户错误
SECURITY_CONTROL(20001,"安全控制"),
USER_NOT_EXIST(20002,"用户不存在"),
USER_LOGIN_FAIL(20003,"用户名或密码错误"),
USER_NOT_LOGIN(20004,"用户还未登录,请先登录"),
NO_PERMISSION(20005,"权限不足,请联系管理员"),
;
private int errCode;
private String errMsg;
CommonErrorEnum(int errCode, String errMsg) {
this.errCode = errCode;
this.errMsg = errMsg;
}
@Override
public int getErrCode() {
return this.errCode;
}
@Override
public String getErrMsg() {
return this.errMsg;
}
@Override
public Map<String, Object> getErrData() {
Map<String,Object> errData=new HashMap<String,Object>();
errData.put("errCode",this.errCode);
errData.put("errMsg",this.errMsg);
return errData;
}
@Override
public CommonError setErrMsg(String errMsg) {
this.errMsg=errMsg;
return this;
}
}
可以看到,这个枚举类有两个属性,分别是错误码errCode和错误消息errMsg,这就是后端发生错误时会返回给前端的错误信息;为了将错误信息方便放入我们第一步所说的CommonReturnType的content属性中,还提供了getErrDta方法;为了方便实际情况可能对errMsg有修改所以还提供了setErrMsg方法。之后回来看一下枚举的每一个项,不同分类的错误的错误码区间是不同的,方便前后端统一协商,当然这里只是几个用于参考的枚举项,你也可以新建自己所需的枚举项。
3.统一的业务异常类
代码参考如下
//统一的业务异常类 也是统一的错误类型的包装器
public class BusinessException extends Exception implements CommonError {
private CommonError commonError;
public BusinessException(CommonError commonError){
super();
this.commonError=commonError;
}
public BusinessException(CommonError commonError,String errMsg){
super();
this.commonError=commonError;
this.commonError.setErrMsg(errMsg);
}
@Override
public int getErrCode() {
return this.commonError.getErrCode();
}
@Override
public String getErrMsg() {
return this.commonError.getErrMsg();
}
@Override
public Map<String, Object> getErrData() {
Map<String,Object> errData=new HashMap<String,Object>();
errData.put("errCode",this.commonError.getErrCode());
errData.put("errMsg",this.commonError.getErrMsg());
return errData;
}
@Override
public CommonError setErrMsg(String errMsg) {
this.commonError.setErrMsg(errMsg);
return this;
}
}
这个类继承了exception类,实现了我们第二步所说的错误类型接口,同时有一个错误类型接口的成员变量,这是一个典型包装器模式的实现。其各个方法基本就是对错误类型接口的对象(或者说错误类型枚举的某一项)的操作(不知道这么说或者这么理解是否准确,反正大概是这么个意思,手动捂脸|ू・ω・` )|ू・ω・` ))。如果看到这一步还是迷糊的话,那就继续看第4、5步的实际使用吧。
4.全局异常处理方法
在具体的业务代码中抛出业务异常对象,或者系统抛出系统异常对象后,我们须要进行异常处理,由于系统抛出异常的最顶端是到达控制器,所以我将这个全局异常处理方法放在一个控制器基类中,之后具体的控制器继承这个基类即可。当然,这个全局异常处理方法也可以写在一个控制器通知类或处理器拦截器里面,这里是因为项目中还有其他内容要放在控制器基类里面,所以采用这种方式。
public class BaseController {
// 全局异常处理方法
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.OK)
public CommonReturnType handleException(HttpServletRequest request,Exception ex){
Map<String,Object> data=null;
if(ex instanceof BusinessException){
data=((BusinessException) ex).getErrData();
}else{
data=CommonErrorEnum.UNKNOWN_ERROR.getErrData();
ex.printStackTrace();
}
return CommonReturnType.fail(data);
}
}
可以看到这个全局异常处理方法加了异常处理通知注解和响应状态码注解,分别负责捕获异常和在异常后的响应中发送http200状态码。这个方法的返回类型仍然是第一步所创建的CommonReturnType,不过其调用的自创建方法是fail,这样其返回的status属性就可以告诉前端没有成功请求到所需数据。其次,在方法的主体中我们判断抛出的异常时业务异常还是系统异常,如果为业务异常则直接将其错误信息交给CommonReturnType,如为系统异常,则均返回为未知错误,然后将错误信息交给CommonReturnType,当然在后端这肯定不是一个未知错误,为了方便日后调试,需要打印系统异常的错误信息到控制台或日志。
经过前4步,使得前端不管是否成功过请求到了所需数据,后端均会返回统一返回类型,只是不同情况返回的具体属性值与数据是不一样的。通过这样的设计方式有利于前后端松耦合和项目工程化。接下来让我们看看具体的使用吧。
5.代码中直接返回统一的返回类型或者抛出统一的业务异常
为了方便测试这种统一的返回数据格式、错误类型、异常处理,我写了一个简单的通过用户id获取用户数据的接口,当然实际的接口肯定更严谨,如下
@RestController
@RequestMapping("/user")
public class UserController extends BaseController {
@Autowired
UserService userService;
// 通过id获取用户数据的接口
@RequestMapping("/getuser")
public CommonReturnType getUser(@RequestParam(name = "id") Integer id) throws BusinessException {
UserModel userModel = userService.getUserById(id);
if (userModel == null) {
throw new BusinessException(CommonErrorEnum.USER_NUT_EXIST);
}
return CommonReturnType.success(userModel);
}
}
在这个controller中,我调用service的方法通过用户id获取用户数据。如果返回的userModel为空,则没有这个id对应的记录,此时不做其他处理,直接抛出一个业务异常,给业务异常类的构造方法传的参数是,统一的错误类型枚举中的用户名未找到这一项,之后这个业务异常会被第三步所写的全局异常处理方法捕获,最终返回一个包含错误信息的CommonReturnType对象。反之如果userModel不为空,则成功请求到了所需数据,此时将userModel交给统一的返回类型的success方法,最终会返回一个包含了用户数据userModel的CommonReturnType对象。之后CommonReturnType对象会被序列化,前端得到相应统一的返回数据格式json进行处理。
下面是两种情况的接口测试结果
未成功请求到用户数据时的测试结果:
成功请求到用户数据时的测试结果:
好了,看到返回结果,就可以知道之前所做的统一的返回数据格式、错误类型、异常处理这一切还是很有意义的。当然统一的返回数据格式、错误类型、异常处理的具体实现方式还是比较多的,也可以在自己的编码实践中做自己的总结。当然了,如果是在项目中,团队成员就一定要一起协商好,所有代码使用一套的统一的返回数据格式、错误类型、异常处理。