当前位置: > > > SpringBoot - 实用工具类库common-util使用详解12(数据通用返回格式、全局异常处理)

SpringBoot - 实用工具类库common-util使用详解12(数据通用返回格式、全局异常处理)

十一、数据通用返回格式

1,什么是统一数据返回格式?

(1)前后端分离是当今服务形式的主流,为了让前端有更好的逻辑展示与页面交互处理,统一的数据返回格式必不可少。通常每次一次 RESTful 请求的返回数据都应该包含类似如下几个信息:
  • success:标识请求成功与否,如:true(成功)、false(失败)
  • code:错误码,如果异常的话则为明确错误码,从而更好的对应业务异常。如果请求成功该值可为空或者“0000
  • message:错误消息,与错误码相对应,更具体的描述异常信息。
  • data:返回结果,通常是 Bean 对象对应的 JSON 数据,通常为了应对不同返回值类型,将其声明为泛型类型
  • timestamp:执行时间戳

(2)而 common-util 这个工具库也为我们提供了一个现成的通用返回数据封装类 CommonResult,所属的包为 com.power.common.model,我们直接使用即可。
工具类库 common-util 的安装配置,可以参考我之前写的文章:SpringBoot - 实用工具类库common-util使用详解1(安装配置)

2,执行成功响应

(1)无返回结果:
@RestController
public class HelloController {
    @RequestMapping("/test")
    public CommonResult test() {
        return CommonResult.ok();
    }
}

(2)返回一个对象:
@RestController
public class HelloController {
    @RequestMapping("/test")
    public CommonResult<Book> test() {
        Book book = new Book(1, "东野圭吾", "沉默的巡游", 32f);
        return CommonResult.ok().setResult(book);
    }
}

(3)返回一个集合:
@RestController
public class HelloController {
    @RequestMapping("/test")
    public CommonResult<List> test() {
        List<Book> books= new ArrayList<>();
        books.add(new Book(1, "东野圭吾", "沉默的巡游", 32f));
        books.add(new Book(2, "鲁迅", "彷徨", 2.99f));
        return CommonResult.ok().setResult(books);
    }
}

3,执行失败响应

(1)不指定错误信息,以及错误响应码:
@RestController
public class HelloController {
    @RequestMapping("/test")
    public CommonResult test() {
        return CommonResult.fail();
    }
}

(2)指定错误信息,以及错误响应码:
@RestController
public class HelloController {
    @RequestMapping("/test")
    public CommonResult test() {
        return CommonResult.fail("1002", "参数格式错误");
    }
}

(3)当然实际开发中为了维护方便,我们首先会定义了一个 ErrorCode 枚举:
提示IMessage 接口也是 common-util 工具类库中提供的。
public enum ErrorCodeEnum implements IMessage {

    SUCCESS("0000", "succeed"),
    PARAM_EMPTY("1001", "必选参数为空"),
    PARAM_ERROR("1002", "参数格式错误"),
    UNKNOWN_ERROR("9999", "系统繁忙,请稍后再试....");

    private String code;
    private String message;

    ErrorCodeEnum(String errCode, String errMsg) {
        this.code = errCode;
        this.message = errMsg;
    }

    @Override
    public String getCode() {
        return this.code;
    }

    @Override
    public String getMessage() {
        return this.message;
    }
}

(4)然后使用这个错误信息枚举即可:
@RestController
public class HelloController {
    @RequestMapping("/test")
    public CommonResult test() {
        return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR);
    }
}

附:实现统一数据返回格式的自动封装

    上面的样例我们都是手动将返回值封装成 CommonResult 对象并返回,但如果每个 API 都需要做这些重复的工作会显的不够优雅。下面演示如何结合 @RestControllerAdvice 注解实现返回数据的自动封装。

1,实现返回数据的自动转换

(1)首先我们创建如下自定义的 Handler,其作用是对于 com.hangge.controller 包下的所有 controller
  • 如果返回值是 CommonResult 对象的话则不做处理
  • 如果不是的话会自动封装成 CommonResult 对象
// 全局的返回数据自动转换
@RestControllerAdvice("com.hangge.controller")
class RestResponseHandler implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter,
                            Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter,
                                  MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> aClass,
                                  ServerHttpRequest serverHttpRequest,
                                  ServerHttpResponse serverHttpResponse) {
        // 如果返回值已经是 CommonResult,则不做处理直接返回
        if (body instanceof CommonResult){
            return body;
        }
        // 否则的话封装成 CommonResult 再返回
        CommonResult commonResult = CommonResult.ok().setResult(body);
        // 如果controller层中返回的类型是String,我们还需要特殊处理下(将CommonResult对象转回String)
        if (body instanceof String) {
            // 这里我使用 FastJSON 进行转换
            return JSON.toJSONString(commonResult);
        }
        return commonResult;
    }
}

(2)测试一下,虽然我们 controller 里面返回的是一个 List,但从请求结果可以发现,最终得到的是封装后的 CommonResult 对象:
@RestController
public class HelloController {
    @RequestMapping("/test")
    public List test() {
        List<Book> books= new ArrayList<>();
        books.add(new Book(1, "东野圭吾", "沉默的巡游", 32f));
        books.add(new Book(2, "鲁迅", "彷徨", 2.99f));
        return books;
    }
}

(3)如果 controller 没有返回值,也是会得到 CommonResult 对象的:
@RestController
public class HelloController {
    @RequestMapping("/test")
    public void test() {
    }
}

2,全局异常处理

(1)上面的配置只是实现了对正常数据的自动封装,当程序发生异常时,我们希望也能返回统一的数据格式到前台。这个只添加如下全局异常处理 Handler 即可。无论是 controller 层里抛出的异常,还是请求没有进入 controller 层(比如发生 401403 等请求错误),都是可以返回通过格式。
// 全局的Rest异常处理
@RestControllerAdvice
public class RestExceptionHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(RestExceptionHandler.class);

    // 处理参数验证异常
    @ExceptionHandler(value = {MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public CommonResult illegalParamsExceptionHandler(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        FieldError fieldError = bindingResult.getFieldError();
        LOGGER.error("request params invalid: {}", fieldError.getDefaultMessage());
        return processBindingError(fieldError);
    }

    // 处理参数转换失败异常
    @ExceptionHandler(value = {MethodArgumentTypeMismatchException.class})
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public CommonResult methodArgumentTypeMismatchException(MethodArgumentTypeMismatchException ex)
    {
        String error = String.format("The parameter '%s' should be of type '%s'", ex.getName(),
                ex.getRequiredType().getSimpleName());
        return CommonResult.fail("400", error);
    }

    // 处理资源找不到异常(404)
    @ExceptionHandler(value = {NoHandlerFoundException.class})
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public CommonResult noHandlerFoundException(Exception ex) {
        return CommonResult.fail("404", "Resource Not Found");
    }

    // 处理不支持当前媒体类型异常(415)
    @ExceptionHandler(value = {HttpMediaTypeNotSupportedException.class})
    @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
    public CommonResult handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex) {
        StringBuilder builder = new StringBuilder();
        builder.append(ex.getContentType());
        builder.append(" media type is not supported. Supported media types are ");
        ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(","));
        return CommonResult.fail("415", builder.toString());
    }

    // 处理方法不被允许异常(405)
    @ExceptionHandler(value = {HttpRequestMethodNotSupportedException.class})
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    public CommonResult methodNotSupportedException(HttpRequestMethodNotSupportedException ex) {
        LOGGER.error("Error code 405: {}", ex.getMessage());
        return CommonResult.fail("405", ex.getMessage());
    }

    // 处理其他异常(错误码统一为500)
    @ExceptionHandler(value = {Exception.class})
    @ResponseStatus(HttpStatus.OK)
    public CommonResult unknownException(Exception ex) {
        LOGGER.error("Error code 500:{}", ex);
        return new CommonResult("500", ex.getMessage());
    }

    // 处理参数验证异常(转换成对应的CommonResult)
    private CommonResult processBindingError(FieldError fieldError) {
        String code = fieldError.getCode();
        LOGGER.debug("validator error code: {}", code);
        switch (code) {
            case "NotEmpty":
                return CommonResult.fail(ErrorCodeEnum.PARAM_EMPTY.getCode(),
                        fieldError.getDefaultMessage());
            case "NotBlank":
                return CommonResult.fail(ErrorCodeEnum.PARAM_EMPTY.getCode(),
                        fieldError.getDefaultMessage());
            case "NotNull":
                return CommonResult.fail(ErrorCodeEnum.PARAM_EMPTY.getCode(),
                        fieldError.getDefaultMessage());
            case "Pattern":
                return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR.getCode(),
                        fieldError.getDefaultMessage());
            case "Min":
                return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR.getCode(),
                        fieldError.getDefaultMessage());
            case "Max":
                return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR.getCode(),
                        fieldError.getDefaultMessage());
            case "Length":
                return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR.getCode(),
                        fieldError.getDefaultMessage());
            case "Range":
                return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR.getCode(),
                        fieldError.getDefaultMessage());
            case "Email":
                return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR.getCode(),
                        fieldError.getDefaultMessage());
            case "DecimalMin":
                return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR.getCode(),
                        fieldError.getDefaultMessage());
            case "DecimalMax":
                return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR.getCode(),
                        fieldError.getDefaultMessage());
            case "Size":
                return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR.getCode(),
                        fieldError.getDefaultMessage());
            case "Digits":
                return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR.getCode(),
                        fieldError.getDefaultMessage());
            case "Past":
                return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR.getCode(),
                        fieldError.getDefaultMessage());
            case "Future":
                return CommonResult.fail(ErrorCodeEnum.PARAM_ERROR.getCode(),
                        fieldError.getDefaultMessage());
            default:
                return CommonResult.fail(ErrorCodeEnum.UNKNOWN_ERROR);
        }
    }
}

(2)假设我们在 Controller 中随便抛出一个异常,可以看到请求响应显示的是通用返回格式:
@RestController
public class HelloController {
    @RequestMapping("/test")
    public void test() {
        throw new MaxUploadSizeExceededException(1000);
    }
}

(3)但对于 404 错误(比如我们访问一个不存在的地址),会发现并没有返回统一格式。这是因为 Spring Boot 默认不会抛出 404 异常(NoHandlerFoundException),所以在 ControllerAdvice 中捕获不到该异常,导致 404 总是跳过 ContollerAdvice,直接显示 ErrorController 的错误页。
springboot WebMvcAutoConfiguration 会默认配置如下资源映射:
  • / 映射到 /static(或 /public/resources/META-INF/resources
  • /webjars/ 映射到 classpath:/META-INF/resources/webjars/
  • /**/favicon.ico 映射 favicon.ico 文件.

(4)要解决这个问题,我们在 application.properties 中添加如下两行配置即可,这样 NoHandlerFoundException 异常就能被 @ControllerAdvice 捕获了:
注意:
  • 配置修改后,我们如果需要访问静态文件前面就需要加上 /static。比如:http://localhost:8080/static/java.png
  • 当然如果我们是一个纯后台应用,没有静态文件的话,可以直接将第二个配置改成 spring.resources.add-mappings=false,不要为我们工程中的资源文件建立映射。
spring.mvc.throw-exception-if-no-handler-found=true
spring.mvc.static-path-pattern=/static/**
评论0