Spring MVC | controller return value and exception handling

Posted by buceta on Sun, 03 Nov 2019 02:22:34 +0100

Old design

When developing an api, you need to define the data response result of the interface first. Here is a very simple and direct Controller implementation method and response result definition

@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: {}, // It can be an object or an array
    msg: ""
}

From the above code, we can see that there will be a lot of duplicate code for each Controller method. We should try to avoid duplicate code. After removing the duplicate code, you can get the following code, which is easy to understand.

@RestController
@RequestMapping("/users")
public class UserController {
        
    @Inject
    private UserService userService;

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

In the above implementation, a necessary requirement is also made, that is, ServiceException needs to be defined as a subclass of RuntimeException, rather than a subclass of Exception. Because ServiceException represents a service Exception, it should prompt the front-end directly when such Exception occurs, without any other special processing. After being defined as a subclass of RuntimeException, a large number of Exception throw declarations will be reduced, and special declarations in transaction @ Transactional will no longer be required.

Unified Controller return value format

During the development process, I found the above structure

@ControllerAdvice
public class ControllerResponseHandler implements ResponseBodyAdvice<Object> {
    
    private Logger logger = LogManager.getLogger(getClass());

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        // All return value types are supported
        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 {
            // All results that do not return the ResponseBean structure are considered successful
            return ResponseBean.success(body);
        }
    }
}

Unified exception handling

In the following code, ServiceException ServiceMessageException ValidatorErrorType FieldValidatorError is a custom class.

@ControllerAdvice
public class ControllerExceptionHandler {

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

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

    /**
     * Custom exception
     */
    @ExceptionHandler(ServiceMessageException.class)
    public ResponseBean handleServiceMessageException(HttpServletRequest request, ServiceMessageException ex) {
        logger.debug(ex);
        return new ResponseBean(ex.getMsgCode(), ex.getMessage());
    }

    /**
     * Custom exception
     */
    @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: Entity class property verification failed
     * For example: 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: Directly verify the method parameters. The verification fails.
     * For example: 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: Data binding exception, similar to MethodArgumentNotValidException, is the parent class of MethodArgumentNotValidException
     */
    @ExceptionHandler(BindException.class)
    public ResponseBean handleBindException(HttpServletRequest request, BindException ex) {
        logger.debug(ex);
        return validatorErrors(ex.getBindingResult());
    }

    /**
     * Return value type conversion error
     */
    @ExceptionHandler(HttpMessageConversionException.class)
    public ResponseBean exceptionHandle(HttpServletRequest request,
            HttpMessageConversionException ex) {
        return internalServiceError(ex);
    }
    
    /**
     * accept corresponding to Http request header
     * The type the client wants to accept is not the same as the server's return type.
     * Although interception is set here, it doesn't work. The reason needs to be further determined through the http request process.
     */
    @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 corresponding to the request header
     * The data type sent by the client is inconsistent with the data that the server wants to receive
     */
    @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);
    }

    /**
     * The data sent from the front end cannot be processed normally
     * For example, the day after tomorrow I hope to receive a json data, but the front-end sends data in xml format or a wrong json format
     */
    @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);
    }

    /**
     * The problem caused by converting the returned result to the data of the response.
     * When using json as the result format, the possible cause is a serialization error.
     * At present, it is known that if an object without properties is returned as a result, this exception will be caused.
     */
    @ExceptionHandler(HttpMessageNotWritableException.class)
    public ResponseBean handlerHttpMessageNotWritableException(HttpServletRequest request,
            HttpMessageNotWritableException ex) {
        return internalServiceError(ex);
    }

    /**
     * Request method not supported
     */
    @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);
    }

    /**
     * Parameter type mismatch
     */
    @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));
    }

    /**
     * Missing required fields
     */
    @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 field is missing during file upload
     */
    @ExceptionHandler(MissingServletRequestPartException.class)
    public ResponseBean exceptionHandle(HttpServletRequest request, MissingServletRequestPartException ex) {
        logger.debug(ex);
        return new ResponseBean(HttpStatus.BAD_REQUEST.value(), ex.getMessage());
    }

    /**
     * Request path does not exist
     */
    @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);
    }

    /**
     * Missing path parameter
     * Controller The @ PathVariable(required=true) parameter is defined in the method, but is not provided in the url
     */
    @ExceptionHandler(MissingPathVariableException.class)
    public ResponseBean exceptionHandle(HttpServletRequest request, MissingPathVariableException ex) {
        return internalServiceError(ex);
    }

    /**
     * All other exceptions
     */
    @ExceptionHandler()
    public ResponseBean handleAll(HttpServletRequest request, Exception ex) {
        return internalServiceError(ex);
    }

    private String codeToMessage(int code) {
        //TODO needs to be customized. Each code will match to a corresponding 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);
    }
}

Through the above configuration, you can effectively handle exceptions in a unified way, and encapsulate the returned results in a unified way.

Topics: Java JSON xml