> 文章列表 > springboot项目——统一系统响应对象+「全局异常捕获」,保证系统响应数据结构一致性

springboot项目——统一系统响应对象+「全局异常捕获」,保证系统响应数据结构一致性

springboot项目——统一系统响应对象+「全局异常捕获」,保证系统响应数据结构一致性

分享下系统中请求接口时响应参数如何做到一致性。
大多数情况下,系统请求的响应是这样的:

{"code": 200,"msg": "操作成功","data": {},"success": true
}

失败时是这样的:

{"code": 500,"msg": "操作失败,请检查","data": {},"success": false
}

那这样是怎么来设计和处理的,今天讲解一下:

1. 首先定义响应格式

无论是成功还是失败,需要先定好一个json的格式。@RestController是常用在controller类上的注解,用来保证接口的响应方法的返回值直接以指定的格式写入Http response body,也就是@Controller和@ResponseBody的复合注解。
清楚了这个之后,我们需要的是将每个接口的响应对象都设置为同一个,这样就可以保证了成功请求后的响应参数一致。不过问题来了:接口类型很多种,有响应数组的、Integer类型、String类型、Boolean类型,如何做到一致性呢?
相应格式设计思路:

  • 必要字段code:业务状态码,首先是需要一个code字段来声明接口的状态码,200表示成功,异常展示对应的错误code。注意这里的code并不是http请求的响应状态码,是系统定义在response body体中的一个code字段。
  • 必要字段data:数据字段,无论接口需要返回任何一种种数据,统一认定是接口数据,包装在data中。
  • 必要字段msg:响应信息,用来对code字段的解读,请求成功或者失败后对请求结果的一种描述,异常信息的展示通常是使用这个字段。
  • 非必要字段timestamp:时间戳/时间字段,该字段是用来记录请求的完成时间,大多数系统会忽略该字段。不过它的存在还用来可以分析系统报错时的问题。
  • 非必要字段success:是否成功,再次描述请求的成功与否

思路设计好,现在来设计一个结构,把上述字段都用上:

{"code": 200,"msg": "操作成功","data": {},"success": true,"timestamp": 1679967317917
}

2. 设计Java统一响应对象

接着根据这个JSON设计一个基础对象:

@ApiModel("统一响应类")
public class Rest<T extends BaseResponse> implements Serializable {private static final long serialVersionUID = 1L;private Boolean success = Boolean.TRUE;private String code = "200";private String msg = "操作成功";private T data;}
@ApiModel("公共响应类")
public abstract class BaseResponse implements Serializable {}
@ApiModel("公共请求类")
public abstract class BaseRequest implements Serializable {}

讲一下为什么会有BaseResponse这个基础响应类,还会有BaseRequest、PageResponse、PageRequest,主要目的还是“统一”思想,让每个请求都有个父类,每个响应对象都有个父类。便于工程化管理,也可以在基础类上加一些通用参数。

3. 设计接口使用

看下没有统一接口的Controller处理器层,会感觉到混乱,开发人员想怎样返回就怎样返回,而通过统一对象Rest之后,是不是看起来会好很多。

/* 如果没有设计统一响应的话,这是一个示例接口* ①代表任何一种响应类型,也可以是void* ②代表某个请求的请求参数对象* ③代表响应对象*/@PostMapping("/test")@ApiOperation(value = "实例接口", produces = MediaType.APPLICATION_JSON_VALUE)public*** test(@RequestBody***Request request) {return***;}/* 统一响应成功示例接口* ①代表某个请求的请求参数对象*/@PostMapping("/test")@ApiOperation(value = "实例接口", produces = MediaType.APPLICATION_JSON_VALUE)public Rest<BaseResponse> test(@RequestBody***Request request) {return Rest.success();}
/* 统一响应失败示例接口* ①代表某个请求的请求参数对象*/@PostMapping("/test")@ApiOperation(value = "实例接口", produces = MediaType.APPLICATION_JSON_VALUE)public Rest<BaseResponse> test(@RequestBody***Request request) {return Rest.error();}

这样看起来就是比较统一清楚,接下来还要完善一下Rest中的success()方法和error()方法。主要是有参和无参,对应需要响应参数和无响应参数的情况。
响应错误时,一般会由开发人员指定错误的枚举类型,枚举中包含错误模板信息模板,和错误code。示例信息模板:“请求失败,请检查数据id:{}是否存在,时间:{}”,这里会有占位符,通过传入的可变长参数进行替换。

    /* 成功*/public static Rest<BaseResponse> success() {return new Rest<>().setData(new BaseResponse());}/* 成功 @param data 响应数据* @param <T>*/public static <T extends BaseResponse> Rest<T> success(T data) {return new Rest<T>().setData(data);}/* 错误 @param errorCode 错误code* @param params    信息模板参数列表* @return*/public static Rest<BaseResponse> error(ErrorCodeEnum errorCode, Object... params) {return new Rest<>().setData(new BaseResponse()).setCode(errorCode.getCode()).setMsg(StrUtil.format(errorCode,  params).getMessage()).setSuccess(false);}/* 错误 @param e 异常* @return*/public static Rest<BaseResponse> error(ServiceException e) {return new Rest<>().setData(new BaseResponse()).setCode(e.getCode()).setMsg(e.getMessage()).setSuccess(false);}

到这里,Controller处理器层的统一封装响应就完成了,我们的统一化已经完成了一半。接下来是对业务代码异常的捕获和统一处理。

4. 业务代码异常的捕获和统一处理

上述是对已知的成功或者错误请求统一处理,还有很多情况下的问题,开发人员是不可预知的。就好像error和exception,error是不可操作的,exception是可以捕获的。
我们的系统报错是一种error,且spring会抛出一种异常,虽然不可以对error进行处理,换种角度思考,我们对exception进行捕获后,最后响应我们统一的response对象,是不是从另一种维度来对error做了处理。
接下里就是对整个系统中的所有exception进行捕获处理:
重点注解@RestControllerAdvice、@ResponseStatus、@ExceptionHandler

  • @RestControllerAdvice等同于@ControllerAdvice + @ResponseBody,对系统中的Controller处理器层做增强处理。
  • @ResponseStatus,使用在方法上,指定http的状态码
  • @ExceptionHandler,使用在方法上,指定捕获的异常类,也可以在注解修饰的方法上指定异常入参值的方式指定需要捕获的异常。

全局异常处理类:
HttpStatus是一个常量枚举,包含了http的所有状态码。不同的异常使用方法的重载方式handler(Throwable e) 对不同的异常进行捕获,如果没有进行匹配最终会到Throwable的异常里面做处理,保证任何异常均是统一响应格式。

@RestControllerAdvice
public class ExceptionHandling {private static final ObjectMapper OBJECT_MAPPER = JsonUtil.defaultObjectMapper();/* 捕获基类Throwable* HttpStatus.INTERNAL_SERVER_ERROR 对应 code=500* @param e* @return*/@ExceptionHandler@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)public Rest<BaseResponse> handler(Throwable e) {//处理异常log.error(e.getMessage(), e);return Rest.error(SystemErrorCodeEnum.BUSY);}/* 捕获系统中的自定义异常* BAD_REQUEST 对应 code=400* ServiceException是系统中自定义的异常类* @param e* @return*/@ExceptionHandler@ResponseStatus(HttpStatus.BAD_REQUEST)public Rest<BaseResponse> handler(ServiceException e) {//处理异常log.warn(e.getMessage(), e);return Rest.error(e);}/* 参数格式错误 @RequestParam 上 validate 失败抛出的异常* 对应的就是校验RequestParam参数和校验PathVariable参数,这两种校验不通过,系统会抛出此异常* ParameterErrorCodeEnum.PARAMETER_NOT_VALID模板信息为:"参数异常:{},请检查后重试"* @param e* @return*/@ExceptionHandler@ResponseStatus(HttpStatus.BAD_REQUEST)public Rest<BaseResponse> handler(ConstraintViolationException e) {//处理异常ArrayNode arrayNode = OBJECT_MAPPER.createArrayNode();e.getConstraintViolations().forEach(constraintViolation -> {for (Path.Node next : constraintViolation.getPropertyPath()) {if (ElementKind.PARAMETER.equals(next.getKind())) {String receivedValue = ObjectUtil.defaultIfNull(constraintViolation.getInvalidValue(), "null").toString();ObjectNode objectNode = OBJECT_MAPPER.createObjectNode().put("参数", next.getName()).put("接收到的值", receivedValue).put("错误信息", constraintViolation.getMessage());arrayNode.add(objectNode);}}});String msg;if (arrayNode.size() > 0) {msg = arrayNode.toString();} else {msg = e.getMessage();}return Rest.error(ParameterErrorCodeEnum.PARAMETER_NOT_VALID, msg);}/* 参数格式错误 @RequestBody 上 validate 失败抛出的异常* 对应的是校验RequestBody入参,校验不通过,系统会抛出此异常 @param e* @return*/@ExceptionHandler@ResponseStatus(HttpStatus.BAD_REQUEST)public Rest<BaseResponse> handler(MethodArgumentNotValidException e) {//处理异常List<ObjectError> allErrors = e.getBindingResult().getAllErrors();ArrayNode arrayNode = OBJECT_MAPPER.createArrayNode();String msg = null;for (ObjectError error : allErrors) {if (error instanceof FieldError) {String field = ((FieldError) error).getField();String defaultMessage = error.getDefaultMessage();Object rejectedValue = ((FieldError) error).getRejectedValue();String receivedValue = ObjectUtil.defaultIfNull(rejectedValue, "null").toString();ObjectNode objectNode = OBJECT_MAPPER.createObjectNode().put("参数", field).put("接收到的值", receivedValue).put("错误信息", defaultMessage);arrayNode.add(objectNode);msg = arrayNode.toString();} else {msg = error.getDefaultMessage();}}return Rest.error(ParameterErrorCodeEnum.PARAMETER_NOT_VALID, msg);}/* RequestParam 注解中 required = true 的情况拦截* springServlet层次的异常* MISSING_REQUEST_PARAMETER的模板是:"缺少参数:[{}],类型为[{}]"* @param e* @return*/@ExceptionHandler@ResponseStatus(HttpStatus.BAD_REQUEST)public Rest<BaseResponse> handler(MissingServletRequestParameterException e) {//处理异常String parameterName = e.getParameterName();String parameterType = e.getParameterType();return Rest.error(ParameterErrorCodeEnum.MISSING_REQUEST_PARAMETER, parameterName, parameterType);}/* 请求方法不支持 例如:GET/POST不对应* METHOD_NOT_SUPPORT的模板为"请求方法[{}]不支持,支持的方法为{}"* @param e* @return*/@ExceptionHandler@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)public Rest<BaseResponse> handler(HttpRequestMethodNotSupportedException e) {//处理异常List<String> supportedMethods = Arrays.asList(e.getSupportedMethods());
Rest.error(ParameterErrorCodeEnum.METHOD_NOT_SUPPORT, e.getMethod(), supportedMethods);}/* 接口不存在* NO_HANDLER_FOUND的模板是:"接口[{}]不存在"* @param e* @return*/@ExceptionHandler@ResponseStatus(HttpStatus.NOT_FOUND)public Rest<BaseResponse> handler(NoHandlerFoundException e) {//处理异常String requestURL = e.getRequestURL();return Rest.error(ParameterErrorCodeEnum.NO_HANDLER_FOUND, requestURL);}