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,我们直接使用即可。
(2)返回一个对象:
工具类库 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 枚举:
(4)然后使用这个错误信息枚举即可:
(2)测试一下,虽然我们 controller 里面返回的是一个 List,但从请求结果可以发现,最终得到的是封装后的 CommonResult 对象:
(3)如果 controller 没有返回值,也是会得到 CommonResult 对象的:
提示: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; } }
@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 层(比如发生 401、403 等请求错误),都是可以返回通过格式。
// 全局的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/**
