十一、数据通用返回格式
1,什么是统一数据返回格式?
(1)前后端分离是当今服务形式的主流,为了让前端有更好的逻辑展示与页面交互处理,统一的数据返回格式必不可少。通常每次一次 RESTful请求的返回数据都应该包含类似如下几个信息:
success:标识请求成功与否,如:true(成功)、false(失败)
code:错误码,如果异常的话则为明确错误码,从而更好的对应业务异常。如果请求成功该值可为空或者“0000”
message:错误消息,与错误码相对应,更具体的描述异常信息。
data:返回结果,通常是 Bean对象对应的 JSON数据,通常为了应对不同返回值类型,将其声明为泛型类型
timestamp:执行时间戳
(2)而common-util 这个工具库也为我们提供了一个现成的通用返回数据封装类 CommonResult,所属的包为com.power.common.model,我们直接使用即可。
2,执行成功响应
(1)无返回结果:
@RestController
public class HelloController {
@RequestMapping("/test")
public CommonResult test() {
return CommonResult.ok();
}
}
(2)返回一个对象:
@RestController
public class HelloController {
@RequestMapping("/test")
public CommonResult test() {
Book book = new Book(1, "东野圭吾", "沉默的巡游", 32f);
return CommonResult.ok().setResult(book);
}
}
(3)返回一个集合:
@RestController
public class HelloController {
@RequestMapping("/test")
public CommonResult test() {
List 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 {
@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 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/**