回顾
回顾下第一次做学校实训时做前后端分离开发,那时候在网上找了很多关于怎么做 统一接口返回 和 全局异常处理 的方法,做到后面发现,这两被我写的太臃肿了。
返回对象 HttpResult
@Data
public class HttpResult<T> {
private Boolean success;
private Integer code;
private T data;
private String msg;
private HttpResult() {
this.code = 200;
this.success = true;
}
private HttpResult(T obj) {
this.code = 200;
this.data = obj;
this.success = true;
}
private HttpResult(String msg) {
this.success = false;
this.code = 400;
this.msg = msg;
}
private HttpResult(Integer code, String msg) {
this.success = false;
this.code = code;
this.msg = msg;
}
private HttpResult(ResultCodeEnum resultCode) {
this.success = false;
this.code = resultCode.getCode();
this.msg = resultCode.getMsg();
}
public static<T> HttpResult<T> success(){
return new HttpResult();
}
public static<T> HttpResult<T> success(T data){
return new HttpResult<T>(data);
}
public static<T> HttpResult<T> failure(String msg){
return new HttpResult<T>(msg);
}
public static<T> HttpResult<T> failure(Integer code, String msg){
return new HttpResult<T>(code, msg);
}
public static<T> HttpResult<T> failure(ResultCodeEnum resultCode){
return new HttpResult<T>(resultCode);
}
}
接着就是业务状态码枚举类 ResultCodeEnum 的定义,感觉这个可真是杂乱无章。
@Getter
public enum ResultCodeEnum {
SUCCESS(200, "SUCCESS"),
SERVER_ERROR(500, "server error"),
USER_ALREADY_EXIST(1209,"用户已存在"),
LOGIN_NOT_VALUE(1209,"空值"),
TOKEN_ERROR(1201,"token无效"),
EMAIL_OR_PASSWORD_ERROR(1209,"邮箱或密码错误"),
NOT_LOGIN(1201,"未登入"),
SURVEY_VALUE_NULL(1204,"问卷值为空"),
SURVEY_ALREADY_DELETE(1208,"问卷已删除");
private Integer code;
private String msg;
ResultCodeEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
还有自定义的业务异常处理类,太多了
问题
- 返回对象的静态方法太多,因为感觉每种都有需要。
- 业务状态码太繁杂,且都没有规律。
- 业务异常类太多,和第2点差不多。
- 且每回都要调返回对象的方法,太麻烦。
解决
返回对象
- code:业务状态码。
- data:数据。
- msg:返回错误信息,成功没必要返回信息。
我取消了 success 属性,因为感觉这个属性没什么用, success 代表请求有没有成功,直接通过 code 判断就好了,规定好500就是失败,200就是成功,其他就是业务异常。
@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class ResponseModel {
private int code;
private Object data;
private String msg;
public static ResponseModel success(Object data) {
ResponseModel responseModel = new ResponseModel();
responseModel.setCode(200);
responseModel.setData(data);
return responseModel;
}
public static ResponseModel failure(ResponseMsgEnum responseMsgEnum) {
ResponseModel responseModel = new ResponseModel();
responseModel.setCode(responseMsgEnum.getCode());
responseModel.setMsg(responseMsgEnum.getMsg());
return responseModel;
}
}
只写了两个静态方法, success 是成功,成功返回200,加上数据,不需要返回信息。 failure 是失败,返回业务异常状态码,和错误信息,这些都定义在枚举类 ResponseMsgEnum 中,业务失败也就不需要返回数据了。
枚举类
枚举类之前就是各种细节都定义一个,实在是太繁杂了。比如上次做的是问卷系统,那么找不到某个问卷id、找不到某一个用户id、这样就要定义两个,但其实只要定义成一个404,找不到该资源就好了。前端哪里得到404,根据不同的场景做不同的处理就好了,没必要每个都是不一样的。
@NoArgsConstructor
@AllArgsConstructor
@Getter
public enum ResponseMsgEnum {
SUCCESS(200, ""),
CLIENT_ERROR(400, "客户端参数错误"),
AUTH_FAILED(401,"身份验证失败"),
NO_FOUND(404, "资源不存在"),
SERVER_ERROR(500, "服务器错误");
private int code;
private String msg;
}
统一接口返回
之前是每个接口都调一次返回对象的静态方法,将数据封装返回。这些都是属于重复性地工作,可以用 @RestControllerAdvice 注解,拦截下后端返回的数据,实现 ResponseBodyAdvice 接口对数据做一层包装再返回给前端。
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@SneakyThrows
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
if(body instanceof ResponseModel){ // 返回类型是否已经封装
return body;
} else if (body instanceof String){
return objectMapper.writeValueAsString(ResponseModel.success(body));
} else {
return ResponseModel.success(body);
}
}
}
返回值经过这里会先走 supports 方法判断是否要执行下面地 beforeBodyWrite 方法。这里还可以做一些其他更高级的操作,但是我暂时不需要,也没去研究,所以直接放行。
然后controller这边就可以直接返回,不需要调返回对象的方法。
@PostMapping("login")
public Map<String, String> login(@RequestBody LoginDTO loginDTO) {
if (loginDTO.getMethod() == LoginDTO.METHOD_PSD) { // 通过密码登录
return loginService.loginByPsd(loginDTO);
} else { // 通过验证码登录
return loginService.loginByOtp(loginDTO);
}
}
全局异常处理
把状态码合并其实就是减少异常种类,这里的代码其实与之前没什么不同,唯一要说的就是,这里捕获异常直接返回给前端是已经包装好的,但是也会经过上面的统一接口返回处理,所以上面就加了一个类型判断是否要包装。
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler({AuthFailedException.class})
public ResponseModel handleAuthFailedException(AuthFailedException e) {
return ResponseModel.failure(e.getResponseMsgEnum());
}
@ExceptionHandler({NoFoundException.class})
public ResponseModel handleNoFoundException(NoFoundException e) {
return ResponseModel.failure(e.getResponseMsgEnum());
}
// 参数校验
@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseModel handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
return ResponseModel.failure(ResponseMsgEnum.CLIENT_ERROR);
}
@ExceptionHandler(Exception.class)
public ResponseModel handleException(Exception e) {
e.printStackTrace();
return ResponseModel.failure(ResponseMsgEnum.SERVER_ERROR);
}
}
业务异常类
上面的代码块前两个都是我自己定义的业务异常。
先弄个异常基类,本来两个属性就够了,但是为了方便点,加上了 ResponseMsgEnum ,可以直接传一个枚举值。而且之前捕获异常是在异常处理器那里写死的,捕获什么异常就传返回特定的值(在枚举类中定义好的)回去,现在是抛出异常传一个枚举类型回去。
@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class BusinessException extends RuntimeException {
private int code;
private String msg;
private ResponseMsgEnum responseMsgEnum;
public BusinessException(ResponseMsgEnum responseMsgEnum) {
this.responseMsgEnum = responseMsgEnum;
}
}
然后就是业务异常类,继承上面的基类。
public class AuthFailedException extends BusinessException {
public AuthFailedException(ResponseMsgEnum responseMsgEnum) {
super(responseMsgEnum);
}
}
之后抛出异常,可以直接传一个枚举值。
if (userPO == null) {
throw new NoFoundException(ResponseMsgEnum.NO_FOUND);
}
if (!StringUtils.equals(loginDTO.getPassword(),userPO.getPassword())) {
throw new AuthFailedException(ResponseMsgEnum.AUTH_FAILED);
}
后记
还有好多如 ResponseBodyAdvice 还没深入地看看。
若有不足之处请多指教。
这里参考了掘金众多文章