springboot封装统一返回数据格式和异常处理

github源码 https://github.com/zehuawong/SpringBoot-JPA-Thymeleaf-Demo
慕课网课程廖师兄Web进阶:https://www.imooc.com/video/14342

1、统一响应数据格式的json

统一json格式的response,如添加一条数据失败的时候的响应码是1而不是500,

{
    "code": 1,
    "msg": "年龄需要大于6岁",
    "data": null
}

{
    "code": 0,
    "msg": "成功",
    "data": {
        "id": 10,
        "name": "李",
        "age": 7
    }
}

2、定义http请求返回的最外层对象,封装返回数据的统一格式

/**
 * http请求返回的最外层对象
 * Created by wzh-zhua on 2018/10/1.
 */
public class Result<T> {
    /** 错误码. */
    private Integer code;

    /** 提示信息. */
    private String msg;

    /** 具体的内容. */
    private T data;

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

}

3、代码复用,为了防止多次出现new Result()的代码造成冗余,增加一个工具类ResultUtil

springboot封装统一返回数据格式和异常处理
为了避免上面出现的代码冗余情况,应该增加工具类,封装请求失败和成功时候的方法,这里可使用静态方法

public class ResultUtil {

    public static Result success(Object object) {
        Result result = new Result();
        result.setCode(0);
        result.setMsg("成功");
        result.setData(object);
        return result;
    }

    public static Result success() {
        return success(null);
    }

    public static Result error(Integer code, String msg) {
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        return result;
    }
}

4、Controller中冗余代码修改后变为

 /**
     * 添加一个女生
     * @return
     */
    @PostMapping(value = "/addgirl")
    public Result<Girl> girlAdd(@Valid Girl girl, BindingResult bindingResult) {
        //假如表单参数很多,用@RequestParam方式就不合适了
         //这里需要做表单验证
        if (bindingResult.hasErrors()) {
            return ResultUtil.error(1, bindingResult.getFieldError().getDefaultMessage());
        }
        return ResultUtil.success(girlRepository.save(girl));

    }

5、统一异常处理和全局异常捕获:@ControllerAdvice + @ExceptionHandler

可以参考
@ControllerAdvice + @ExceptionHandler 全局处理 Controller 层异常
https://blog.csdn.net/kinginblue/article/details/70186586
Spring Boot @ControllerAdvice 处理全局异常,返回固定格式Json
https://blog.csdn.net/u014044812/article/details/78219692

场景一
springboot封装统一返回数据格式和异常处理
上面的这个异常的status的响应码的格式是500,而且json的字段不是我们自定义的字段
为了得到异常情况统一的返回数据,我们可以对异常捕获,取到我们想要的内容,再给它封装,最后返回给浏览器。

场景二
还有一种情况是,对于与数据库相关的 Spring MVC 项目,我们通常会把 事务 配置在 Service层,当数据库操作失败时让 Service 层抛出运行时异常,Spring 事物管理器就会进行回滚。

如此一来,我们的 Controller 层就不得不进行 try-catch Service 层的异常,否则会返回一些不友好的错误信息到客户端。但是,Controller 层每个方法体都写一些模板化的 try-catch 的代码,很难看也难维护,特别是还需要对 Service 层的不同异常进行不同处理的时候。

新建一个handle包,新建ExceptionHandle.java类

/**
 * 捕获异常的类,返回信息给浏览器,可以自定义返回的code,msg等信息
 */
@ControllerAdvice
public class ExceptionHandle {

    private final static Logger logger = LoggerFactory.getLogger(ExceptionHandle.class);

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Result handle(Exception e) {
        if (e instanceof GirlException) {   //判断异常是否是我们定义的异常
            GirlException girlException = (GirlException) e;
            return ResultUtil.error(girlException.getCode(), girlException.getMessage());
        }else {
            logger.error("【系统异常】{}", e);
            return ResultUtil.error(-1, "未知错误");
        }
    }
}

需要注意

  • @ControllerAdvice:在spring 3.2中,新增了@ControllerAdvice 注解,用于拦截全局的Controller的异常,注意:ControllerAdvice注解只拦截Controller不会拦截Interceptor的异常。
  • 如果有需要,可以用Logger记录异常信息

优点和缺点

  • 优点:将 Controller 层的异常和数据校验的异常进行统一处理,减少模板代码,减少编码量,提升扩展性和可维护性。
  • 缺点:只能处理 Controller 层未捕获(往外抛)的异常,对于 Interceptor(拦截器)层的异常,Spring 框架层的异常,就无能为力了。

注意完善ExceptionHandle类
可以参考文章 https://blog.csdn.net/kinginblue/article/details/70186586

  1. @ExceptionHandler(BusinessException.class) 声明了对 BusinessException 业务异常的处理,并获取该业务异常中的错误提示,构造后返回给客户端。

  2. @ExceptionHandler(Exception.class) 声明了对 Exception 异常的处理,起到兜底作用,不管 Controller 层执行的代码出现了什么未能考虑到的异常,都返回统一的错误提示给客户端。

  3. @ExceptionHandler(MethodArgumentNotValidException.class) 处理所有接口数据验证异常

6、自定义异常类

注意:spring中,只有继承RuntimeException才会进行事务回滚,Exception不会进行事务回滚

public class GirlException extends RuntimeException{    //注意:spring中,只有RuntimeException才会进行事务回滚,Exception不会进行事务回滚

    private Integer code;

    public GirlException(ResultEnum resultEnum) {
        super(resultEnum.getMsg());
        this.code = resultEnum.getCode();
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }
}

7、Service层中异常部分的代码就可以修改为

springboot封装统一返回数据格式和异常处理

public void getAge(Long id) throws Exception{   //逻辑判断
        Girl girl = findOne(id);
        Integer age = girl.getAge();
        if (age < 10) {
            //返回"你还在上小学吧" code=100
            throw new GirlException(ResultEnum.PRIMARY_SCHOOL);
        }else if (age > 10 && age < 16) {
            //返回"你可能在上初中" code=101
            throw new GirlException(ResultEnum.MIDDLE_SCHOOL);
        }

        //如果>16岁,加钱
        //...
    }

修改:上图中的code=100不应该到处出现,不利于代码维护

8、为了统一管理返回数据结果code和message,新建一个枚举类ResultEnum

public enum ResultEnum {
    UNKONW_ERROR(-1, "未知错误"),
    SUCCESS(0, "成功"),
    PRIMARY_SCHOOL(100, "我猜你可能还在上小学"),
    MIDDLE_SCHOOL(101, "你可能在上初中"),

    ;

    private Integer code;

    private String msg;

    ResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}