Spring MVC Controller返回值及异常的统一处理方法

作者:donespeak 时间:2023-04-01 00:23:12 

旧的设计方案

开发api的时候,需要先定义好接口的数据响应结果.如下是一个很简单直接的Controller实现方法及响应结果定义.


@RestController
@RequestMapping("/users")
public class UserController {

@Inject
private UserService userService;

@GetRequest("/{userId:\\d+}")
public ResponseBean signin(@PathVariable long userId) {
 try {
  User user = userService.getUserBaseInfo(userId);
  return ResponseBean.success(user);
 } catch (ServiceException e) {
  return new ReponseBean(e.getCode(), e.getMsg());
 } catch (Exception e) {
  return ResponseBean.systemError();
 }
}
}

{
code: "",
data: {}, // 可以是对象或者数组
msg: ""
}

从上面的代码,我们可以看到对于每个 Controller 方法,都会有很多重复的代码出现,我们应该设法去避免重复的代码。将重复的代码移除之后,可以得到如下的代码,简单易懂。


@RestController
@RequestMapping("/users")
public class UserController {

@Inject
private UserService userService;

@GetRequest("/{userId:\\d+}")
public User signin(@PathVariable long userId) {
 return userService.getUserBaseInfo(userId);
}
}

在以上的实现中,还做了一个必要的要求,就是 ServiceException 需要定义为 RuntimeException的子类,而不是 Exception的子类。由于 ServiceException 表示服务异常,一般发生这种异常是应该直接提示前端,而无需进行其他特殊处理的。在定义为 RuntimeException 的子类之后,会减少大量的异常抛出声明,而且不再需要在事务@Transactional 中进行特殊声明。

统一 Controller 返回值格式

在开发的过程中,我发现上面的结构


@ControllerAdvice
public class ControllerResponseHandler implements ResponseBodyAdvice<Object> {

private Logger logger = LogManager.getLogger(getClass());

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
 // 支持所有的返回值类型
 return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
  Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
  ServerHttpResponse response) {
 if(body instanceof ResponseBean) {
  return body;
 } else {
  // 所有没有返回ResponseBean结构的结果均认为是成功的
  return ResponseBean.success(body);
 }
}
}

统一异常处理

如下的代码中,ServiceException ServiceMessageException ValidatorErrorType FieldValidatorError 均为自定义类。


@ControllerAdvice
public class ControllerExceptionHandler {

private Logger logger = LogManager.getLogger(getClass());

private static final String logExceptionFormat = "[EXIGENCE] Some thing wrong with the system: %s";

/**
 * 自定义异常
 */
@ExceptionHandler(ServiceMessageException.class)
public ResponseBean handleServiceMessageException(HttpServletRequest request, ServiceMessageException ex) {
 logger.debug(ex);
 return new ResponseBean(ex.getMsgCode(), ex.getMessage());
}

/**
 * 自定义异常
 */
@ExceptionHandler(ServiceException.class)
public ResponseBean handleServiceException(HttpServletRequest request, ServiceException ex) {
 logger.debug(ex);
 String message = codeToMessage(ex.getMsgCode());
 return new ResponseBean(ex.getMsgCode(), message);
}

/**
 * MethodArgumentNotValidException: 实体类属性校验不通过
 * 如: listUsersValid(@RequestBody @Valid UserFilterOption option)
 */
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseBean handleMethodArgumentNotValid(HttpServletRequest request, MethodArgumentNotValidException ex) {
 logger.debug(ex);
 return validatorErrors(ex.getBindingResult());
}

private ResponseBean validatorErrors(BindingResult result) {
 List<FieldValidatorError> errors = new ArrayList<FieldValidatorError>();
 for (FieldError error : result.getFieldErrors()) {
  errors.add(toFieldValidatorError(error));
 }
 return ResponseBean.validatorError(errors);
}

/**
 * ConstraintViolationException: 直接对方法参数进行校验,校验不通过。
 * 如: pageUsers(@RequestParam @Min(1)int pageIndex, @RequestParam @Max(100)int pageSize)
 */
@ExceptionHandler(ConstraintViolationException.class)
public ResponseBean handleConstraintViolationException(HttpServletRequest request,
  ConstraintViolationException ex) {
 logger.debug(ex);
 //
 List<FieldValidatorError> errors = new ArrayList<FieldValidatorError>();

for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
  errors.add(toFieldValidatorError(violation));
 }
 return ResponseBean.validatorError(errors);
}

private FieldValidatorError toFieldValidatorError(ConstraintViolation<?> violation) {
 Path.Node lastNode = null;
 for (Path.Node node : violation.getPropertyPath()) {
  lastNode = node;
 }

FieldValidatorError fieldNotValidError = new FieldValidatorError();
 // fieldNotValidError.setType(ValidatorTypeMapping.toType(violation.getConstraintDescriptor().getAnnotation().annotationType()));
 fieldNotValidError.setType(ValidatorErrorType.INVALID.value());
 fieldNotValidError.setField(lastNode.getName());
 fieldNotValidError.setMessage(violation.getMessage());
 return fieldNotValidError;
}

private FieldValidatorError toFieldValidatorError(FieldError error) {
 FieldValidatorError fieldNotValidError = new FieldValidatorError();
 fieldNotValidError.setType(ValidatorErrorType.INVALID.value());
 fieldNotValidError.setField(error.getField());
 fieldNotValidError.setMessage(error.getDefaultMessage());
 return fieldNotValidError;
}

/**
 * BindException: 数据绑定异常,效果与MethodArgumentNotValidException类似,为MethodArgumentNotValidException的父类
 */
@ExceptionHandler(BindException.class)
public ResponseBean handleBindException(HttpServletRequest request, BindException ex) {
 logger.debug(ex);
 return validatorErrors(ex.getBindingResult());
}

/**
 * 返回值类型转化错误
 */
@ExceptionHandler(HttpMessageConversionException.class)
public ResponseBean exceptionHandle(HttpServletRequest request,
  HttpMessageConversionException ex) {
 return internalServiceError(ex);
}

/**
 * 对应 Http 请求头的 accept
 * 客户器端希望接受的类型和服务器端返回类型不一致。
 * 这里虽然设置了拦截,但是并没有起到作用。需要通过http请求的流程来进一步确定原因。
 */
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public ResponseBean handleHttpMediaTypeNotAcceptableException(HttpServletRequest request,
  HttpMediaTypeNotAcceptableException ex) {
 logger.debug(ex);
 StringBuilder messageBuilder = new StringBuilder().append("The media type is not acceptable.")
   .append(" Acceptable media types are ");
 ex.getSupportedMediaTypes().forEach(t -> messageBuilder.append(t + ", "));
 String message = messageBuilder.substring(0, messageBuilder.length() - 2);

return new ResponseBean(HttpStatus.NOT_ACCEPTABLE.value(), message);
}

/**
 * 对应请求头的 content-type
 * 客户端发送的数据类型和服务器端希望接收到的数据不一致
 */
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public ResponseBean handleHttpMediaTypeNotSupportedException(HttpServletRequest request,
  HttpMediaTypeNotSupportedException ex) {
  logger.debug(ex);
 StringBuilder messageBuilder = new StringBuilder().append(ex.getContentType())
   .append(" media type is not supported.").append(" Supported media types are ");
 ex.getSupportedMediaTypes().forEach(t -> messageBuilder.append(t + ", "));
 String message = messageBuilder.substring(0, messageBuilder.length() - 2);
 System.out.println(message);
 return new ResponseBean(HttpStatus.UNSUPPORTED_MEDIA_TYPE.value(), message);
}

/**
 * 前端发送过来的数据无法被正常处理
 * 比如后天希望收到的是一个json的数据,但是前端发送过来的却是xml格式的数据或者是一个错误的json格式数据
 */
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseBean handlerHttpMessageNotReadableException(HttpServletRequest request,
  HttpMessageNotReadableException ex) {
 logger.debug(ex);
 String message = "Problems parsing JSON";
 return new ResponseBean(HttpStatus.BAD_REQUEST.value(), message);
}

/**
 * 将返回的结果转化到响应的数据时候导致的问题。
 * 当使用json作为结果格式时,可能导致的原因为序列化错误。
 * 目前知道,如果返回一个没有属性的对象作为结果时,会导致该异常。
 */
@ExceptionHandler(HttpMessageNotWritableException.class)
public ResponseBean handlerHttpMessageNotWritableException(HttpServletRequest request,
  HttpMessageNotWritableException ex) {
 return internalServiceError(ex);
}

/**
 * 请求方法不支持
 */
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseBean exceptionHandle(HttpServletRequest request, HttpRequestMethodNotSupportedException ex) {
 logger.debug(ex);
 StringBuilder messageBuilder = new StringBuilder().append(ex.getMethod())
   .append(" method is not supported for this request.").append(" Supported methods are ");

ex.getSupportedHttpMethods().forEach(m -> messageBuilder.append(m + ","));
 String message = messageBuilder.substring(0, messageBuilder.length() - 2);
 return new ResponseBean(HttpStatus.METHOD_NOT_ALLOWED.value(), message);
}

/**
 * 参数类型不匹配
 */
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ResponseBean methodArgumentTypeMismatchExceptionHandler(HttpServletRequest request,
  MethodArgumentTypeMismatchException ex) {
 logger.debug(ex);
 String message = "The parameter '" + ex.getName() + "' should of type '"
   + ex.getRequiredType().getSimpleName().toLowerCase() + "'";

FieldValidatorError fieldNotValidError = new FieldValidatorError();
 fieldNotValidError.setType(ValidatorErrorType.TYPE_MISMATCH.value());
 fieldNotValidError.setField(ex.getName());
 fieldNotValidError.setMessage(message);

return ResponseBean.validatorError(Arrays.asList(fieldNotValidError));
}

/**
 * 缺少必填字段
 */
@ExceptionHandler(MissingServletRequestParameterException.class)
public ResponseBean exceptionHandle(HttpServletRequest request,
  MissingServletRequestParameterException ex) {
 logger.debug(ex);
 String message = "Required parameter '" + ex.getParameterName() + "' is not present";

FieldValidatorError fieldNotValidError = new FieldValidatorError();
 fieldNotValidError.setType(ValidatorErrorType.MISSING_FIELD.value());
 fieldNotValidError.setField(ex.getParameterName());
 fieldNotValidError.setMessage(message);

return ResponseBean.validatorError(Arrays.asList(fieldNotValidError));
}

/**
 * 文件上传时,缺少file 字段
 */
@ExceptionHandler(MissingServletRequestPartException.class)
public ResponseBean exceptionHandle(HttpServletRequest request, MissingServletRequestPartException ex) {
 logger.debug(ex);
 return new ResponseBean(HttpStatus.BAD_REQUEST.value(), ex.getMessage());
}

/**
 * 请求路径不存在
 */
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseBean exceptionHandle(HttpServletRequest request, NoHandlerFoundException ex) {
 logger.debug(ex);
 String message = "No resource found for " + ex.getHttpMethod() + " " + ex.getRequestURL();
 return new ResponseBean(HttpStatus.NOT_FOUND.value(), message);
}

/**
 * 缺少路径参数
 * Controller方法中定义了@PathVariable(required=true)的参数,但是却没有在url中提供
 */
@ExceptionHandler(MissingPathVariableException.class)
public ResponseBean exceptionHandle(HttpServletRequest request, MissingPathVariableException ex) {
 return internalServiceError(ex);
}

/**
 * 其他所有的异常
 */
@ExceptionHandler()
public ResponseBean handleAll(HttpServletRequest request, Exception ex) {
 return internalServiceError(ex);
}

private String codeToMessage(int code) {
 //TODO 这个需要进行自定,每个 code 会匹配到一个相应的 msg
 return "The code is " + code;
}

private ResponseBean internalServiceError(Exception ex) {
 logException(ex);
 // do something else
 return ResponseBean.systemError();
}

private <T extends Throwable> void logException(T e) {
 logger.error(String.format(logExceptionFormat, e.getMessage()), e);
}
}

通过上面的配置,可以有效地将异常进行统一的处理,同时对返回的结果进行统一的封装。

来源:https://segmentfault.com/a/1190000020885579

标签:spring,mvc,异常
0
投稿

猜你喜欢

  • mybatis 运行时加载自定义mapper文件方式

    2023-08-16 10:00:16
  • Android实现异步加载图片

    2021-08-14 16:44:20
  • java实现电话本管理系统

    2023-10-11 14:51:19
  • C#中异步和多线程的区别介绍

    2021-08-23 07:06:49
  • Java8中CompletableFuture的用法全解

    2023-09-08 15:08:55
  • Android Lottie实现中秋月饼变明月动画特效实例

    2023-06-19 12:41:17
  • winform实现五子棋游戏

    2023-05-31 19:14:24
  • 使用SmtpClient发送邮件的方法

    2022-12-07 16:32:58
  • Android 大文件切割与合并的实现代码

    2023-05-25 21:58:58
  • C#实现多文件压缩与解压功能

    2022-03-05 04:45:54
  • C++语言io流处理基本操作教程示例

    2023-11-02 22:07:39
  • winform用datagridview制作课程表实例

    2023-11-24 06:08:28
  • 解决idea打包成功但是resource下的文件没有成功的问题

    2023-10-11 06:21:12
  • 最优雅地整合 Spring & Spring MVC & MyBatis 搭建 Java 企业级应用(附源码)

    2023-09-26 18:27:57
  • c#分页读取GB文本文件实例

    2021-09-13 10:18:39
  • Java java.sql.Timestamp时间戳案例详解

    2023-11-10 13:50:47
  • 为何Linq的Distinct实在是不给力

    2023-05-11 17:03:18
  • springboot与vue详解实现短信发送流程

    2023-05-24 14:55:29
  • Android开发之RecyclerView控件

    2023-08-14 08:57:37
  • java清除u盘内存卡里的垃圾文件示例

    2023-05-12 06:41:38
  • asp之家 软件编程 m.aspxhome.com