SpringMVC | Controller 返回值及異常的統一處理

舊的設計方案

開發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);
    }
}

通過上面的配置,可以有效地將異常進行統一的處理,同時對返回的結果進行統一的封裝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章