springboot 后端统一返回数据格式,异常统一处理
模板
场景:
- 后端给前端的数据类型可能会是基本数据类型、String字符串、对象、数组、或者异常提示等。前端拿到你返回的数据去展示或者给出错误提示,但他不可能说每个接口都把这些异常提示处理一遍,比如说返回没有登录、或者一些业务异常等。
分析:
- 基于上面场景,那么我们要做的就是在后端返回结果前做一层统一处理。返回一个统一的对象,如ResponseVO,有code、msg、data;前端根据返回的code做统一处理
- code=0,返回成功,返回数据在data上
- code=1或其他,后端异常返回,可能是业务异常,也可能是程序异常,错误信息放在msg上
- 如果未登入,后端返回403,这时候前端在调用后端接口返回那里根据错误码去做统一的处理,统一提示或其他。成功的话就把返回的数据data给对应调用方法那里。
实现:
初级版,我们返回一个map,然后通过map把code、msg、data 放进去
@RequestMapping("/test")
public Map<String,Object> test(){
Map<String,Object> map = new HashMap<>();
map.put("code","0");
map.put("msg","成功");
map.put("data","测试");
return map;
}
- 返回结果:
- 上面图片我们可以看到,满足了我们的需求,返回了code、msg、还有我们的数据data。
- 但是问题来了,我们每个方法都要写一遍map,把这些数据放进去是不是很麻烦呢,在上面花这么多时间去写这个还怎么摸鱼呢,因此我们小小的优化一下就有了我们的进阶版
进阶版,统一封装:定义一个统一的返回对象ResponseVO ,在ResponseVO 里写成功和失败的方法
@Data
public class ResponseVO implements Serializable {
/**
* 响应状态码,0-成功,非0-失败
*/
private Integer code = 0;
/**
* 返回结果说明
*/
private String msg = "成功";
/**
* JSON格式响应数据:实体类数据
*/
private Object data;
/**
* 返回成功
* @param data
* @return
*/
public static ResponseVO success(Object data){
ResponseVO response = new ResponseVO();
response.setCode(0);
response.setMsg("成功");
response.setData(data);
return response;
}
}
- 这时候在controller调用就变成了下面这样,是不是简洁多了呢
@RequestMapping("/test1")
public ResponseVO test1(){
return ResponseVO.success("测试1");
}
- 现在虽然简洁多了,但是还是在每个方法上都要写ResponseVO.success()或者ResponseVO.fail(),而且每个方法的返回值都变成了ResponseVO,我们都不知道他们的意义了,那有没有统一处理的呢,就是我该返回啥就返回啥,controller层不用关心这些?答案当然是有的,因此就有了下面的最终版方案。
最终版,ResponseBodyAdvice
- 接下来就要用到ResponseBodyAdvice,从字面意思理解它的意思就是返回体切面,就是对Controller返回的数据进行统一处理,因此我们只要实现这个接口,在上面做统一处理即可,他有两个接口,我们只需在beforeBodyWrite方法处理就可以了,唯一要注意的就是当返回String类型时要特殊处理,不然会报转换错误。统一封装后就不用去关心返回类型了。
@RestControllerAdvice
public class ResponseHandler implements ResponseBodyAdvice<Object> {
private Log log = LogFactory.getLog(ResponseHandler.class);
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
ResponseVO respVo = null;
if (body instanceof ResponseVO) {
respVo = (ResponseVO) body;
}else {
respVo = new ResponseVO();
respVo.setData(body);
}
//如果返回的字符串类型,会先判断HttpMessageConverter能否支持对应的返回类型再使用ResponseBodyAdvice进行封装
//那么此时在进来就不是String类型,所以会报无法转换成ResponseVO对象,那么这里有两种方法,一种是直接返回json字符串,另一种是
//一种是自己的WebConfig进行额外的配置
if (body instanceof String){
return JSONUtil.toJsonStr(respVo);
}
return respVo;
}
}
问题1、假如有个接口特殊,不需要这个返回这个格式怎么办呢?
- 我们可以用到ResponseBodyAdvice接口的另一个方法,让你的方法返回值不走这个统一返回格式处理,最好的方式就是定一个注解,在需要忽略的方法上加上这个注解,实现方式如下
- 定义注解IgnoreResponseHandler
@Documented
@Inherited
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreResponseHandler {
}
- 在ResponseBodyAdvice的supports方法忽略
@RestControllerAdvice
public class ResponseHandler implements ResponseBodyAdvice<Object> {
private Log log = LogFactory.getLog(ResponseHandler.class);
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return !returnType.hasMethodAnnotation(IgnoreResponseHandler.class);
}
}
- 使用,返回结果,这样就忽略掉了
@RequestMapping("/test5")
@IgnoreResponseHandler
public String test5(){
return "测试1";
}
全局异常处理器:统一处理返回的业务异常
- Spring3.2帮我们写了一个通知新注解,其原理是Aop,注解为@ControllerAdvice,它能够默认拦截所有带@Controller注解的类,拦截之后可以写增强功能,可对controller中被 @RequestMapping注解的方法加一些逻辑处理。
- @RestControllerAdvice 与 @ControllerAdvice的区别就和@RestController与@Controller的区别类似,@RestControllerAdvice注解包含了@ControllerAdvice注解和@ResponseBody注解。
- 当自定义类加@ControllerAdvice注解时,方法需要返回json数据时,每个方法还需要添加@ResponseBody注解:
- 当自定义类加@RestControllerAdvice注解时,方法自动返回json数据,每个方法无需再添加@ResponseBody注解:
- 最常用的就是异常处理:在类的方法上加上注解@ExceptionHandler(XxxException.class),就能捕获所有指定的异常。
- 需要配合@ExceptionHandler使用。当将异常抛到controller时,可以对异常进行统一处理,规定返回的json格式或是跳转到一个错误页面
- 对于全部的异常我们想统一处理,就要用到@ExceptionHandler(value = Exception.class)这个注解了,加上这个注解,当抛出异常时都会进这个方法。
//全局异常捕捉处理
@ControllerAdvice
public class CustomExceptionHandler {
@ResponseBody
@ExceptionHandler(value = Exception.class)
public Map errorHandler(Exception ex) {
Map map = new HashMap();
map.put("code", 400);
//判断异常的类型,返回不一样的返回值
if(ex instanceof MissingServletRequestParameterException){
map.put("msg","缺少必需参数:"+((MissingServletRequestParameterException) ex).getParameterName());
}
else if(ex instanceof MyException){
map.put("msg","这是自定义异常");
}
return map;
}
}
- 自定义异常类
//自定义异常类
@Data
public class MyException extends RuntimeException {
private long code;
private String msg;
public MyException(Long code, String msg){
super(msg);
this.code = code;
this.msg = msg;
}
public MyException(String msg){
super(msg);
this.msg = msg;
}
}
@RestController
public class TestController {
@RequestMapping("testException")
public String testException() throws Exception{
throw new MissingServletRequestParameterException("name","String");
}
@RequestMapping("testMyException")
public String testMyException() throws MyException{
throw new MyException("i am a myException");
}
}
- 分别访问testException和testMyException接口,可得到以下结果
- 如果不需要返回json数据,而要渲染某个页面模板返回给浏览器,那么可以这么实现:
@ExceptionHandler(value = MyException.class)
public ModelAndView myErrorHandler(MyException ex) {
ModelAndView modelAndView = new ModelAndView();
//指定错误页面的模板页
modelAndView.setViewName("error");
modelAndView.addObject("code", ex.getCode());
modelAndView.addObject("msg", ex.getMsg());
return modelAndView;
}