@ validated annotation exception returns JSON value mode

  • 2021-11-29 23:55:59
  • OfStack

Directory @ validated Annotation Exception Returns JSON Value Using @ Valid Annotation, Unified 1 Handling of Parameter Error Exception

@ validated Annotation Exception Returns JSON Value


@ControllerAdvice
public class ValidParamExceptionHandler {
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Map<String, Object> allExceptionHandler(Exception e){
         Map<String, Object> map = new HashMap<>(2);
         if(e instanceof BindException) {
            BindException ex = (BindException)e;
            BindingResult bindingResult = ex.getBindingResult();
            StringBuilder errMsg = new StringBuilder(bindingResult.getFieldErrors().size() * 16);
            errMsg.append("Invalid request:");
            for (int i = 0 ; i < bindingResult.getFieldErrors().size() ; i++) {
                if(i > 0) {
                    errMsg.append(",");
                }
                FieldError error = bindingResult.getFieldErrors().get(i);
                errMsg.append(error.getField()+":"+error.getDefaultMessage());
            }
            map.put("errcode", 500);
            map.put("errmsg", errMsg.toString());
        
        }
        else {    
            map.put("errcode", 500);
            map.put("errmsg", e.getMessage());
        }
        return map;
    }

(1) Here, @ ControllerAdvice is annotated, and @ ControllerAdvice is an enhanced version of @ Controller, which is generally used with @ ExceptionHandler.

If @ Controller is annotated, exception handling works only for methods in the current controller class, but if @ ControllerAdvice is used, it is globally effective.

(2) The @ ExceptionHandler annotation is filled with the exception class class object you want to catch

Use @ Valid annotation to handle its parameter error exception in a unified way

When we use springboot as a microservice framework for agile development, In order to ensure the security of transmitted data, The transmitted data needs to be verified, But in the past development, Developers spend a lot of time checking parameters in tedious judgment statements such as if else, This way not only slows down our development speed, And there is a lot of redundant code in the written code, The code is not elegant enough. In order to decouple the verification logic of parameters from the business logic of the code, Java provides us with @ Valid annotation to help us check parameters, which realizes the decoupling of business logic and parameter verification logic to a certain extent and increases the simplicity and readability of the code.

springboot comes with spring validation parameter verification framework, which is similar to @ valid in use, so it will not be described here. This paper mainly explains the use of @ valid to deal with the error 1 sample after its parameter verification failure.

Firstly, the unified handling of exceptions in micro-service development is introduced. The @ RestControllerAdvice annotation in spring can get exceptions with @ controller annotation class, and the exception handling is completed together through @ ExceptionHandler (MyException.class) annotation. Examples are as follows:


/**
 *  Generic exception interception handling class (intercepts all of the controller Exception) 
 */
@Slf4j
@RestControllerAdvice
public class CommonExceptionHandler {
 
    /**
     *  Unify runtime exceptions 1 Exception management method 
     * @param e
     * @return
     */
    @ExceptionHandler(FlyException.class) // FlyException Class inherits from the RuntimeException
    public ResponseEntity<Map<String, Object>> handlerException(FlyException e) {
        Map<String, Object> result = new HashMap<>(1);
        result.put("message", e.getMessage());
        return ResponseEntity.status(e.getCode()).body(result);
    }

Uniform handling of exceptions is achieved through the combination of annotation @ RestControllerAdvice and annotation @ ExceptionHandler, and then displayed in a friendly way at the front end.

Examples of using @ Valid annotations are as follows:


  @PostMapping
    public ResponseEntity save(@Valid BrandCreateRequestDto dto, BindingResult bindingResult) {
        //  Determine whether there is a parameter error with mismatch in verification 
        if (bindingResult.hasErrors()) {
            //  Gets a parameter collection where all field parameters do not match 
            List<FieldError> fieldErrorList = bindingResult.getFieldErrors();
            Map<String, Object> result = new HashMap<>(fieldErrorList.size());
            fieldErrorList.forEach(error -> {
                //  Save the error parameter name and parameter error reason in map In the collection 
                result.put(error.getField(), error.getDefaultMessage());
            });
            return ResponseEntity.status(HttpStatus.BAD_REQUEST.value()).body(result);
        } 
        brandService.save(dto);
        return ResponseEntity.status(HttpStatus.CREATED.value()).build();
    }

The @ Valid annotation does simplify our original problem of parameter validation, but if we have multiple handler to deal with, we should not write such redundant code every time. By looking at the @ valid implementation mechanism (not described here), an MethodArgumentNotValidException exception is thrown when the parameter validation fails (this exception is thrown when the parameter annotated with {@ code @ Valid} fails validation):


/**
 * Exception to be thrown when validation on an argument annotated with {@code @Valid} fails.
 *
 * @author Rossen Stoyanchev
 * @since 3.1
 */
@SuppressWarnings("serial")
public class MethodArgumentNotValidException extends Exception { 
 private final MethodParameter parameter; 
 private final BindingResult bindingResult; 
 
 /**
  * Constructor for {@link MethodArgumentNotValidException}.
  * @param parameter the parameter that failed validation
  * @param bindingResult the results of the validation
  */
 public MethodArgumentNotValidException(MethodParameter parameter, BindingResult bindingResult) {
  this.parameter = parameter;
  this.bindingResult = bindingResult;
 }

According to our expectation, we only need to capture MethodArgumentNotValidException exception in the original definition of Uniform 1 exception handling class, and then analyze and process its error information to achieve general use. The code is as follows:


/**
 *  Exception handling method for method parameter verification 
 */
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handlerNotValidException(MethodArgumentNotValidException exception) {
    log.debug("begin resolve argument exception");
    BindingResult result = exception.getBindingResult();
    Map<String, Object> maps;
 
    if (result.hasErrors()) {
        List<FieldError> fieldErrors = result.getFieldErrors();
        maps = new HashMap<>(fieldErrors.size());
        fieldErrors.forEach(error -> {
            maps.put(error.getField(), error.getDefaultMessage());
        });
    } else {
        maps = Collections.EMPTY_MAP;
    } 
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(maps);
}

But after testing, You will find it ineffective to catch and then handle the exception, This could be me, It is also one of the problems that everyone encounters. After analyzing the source code of the execution process of @ Valid, The execution process of data transfer to spring is roughly as follows: the front end transfers data to spring through http protocol, spring converts stream data into Map type through HttpMessageConverter class, and then binds parameters to method objects through ModelAttributeMethodProcessor class, and verifies parameters with @ Valid or @ Validated annotations. The method for processing and verifying parameters is ModelAttributeMethodProcessor. resolveArgument (...), and some source codes are as follows:


public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
 
...
 public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
   NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        ...
        Object attribute = null;
  BindingResult bindingResult = null;
 
  if (mavContainer.containsAttribute(name)) {
   attribute = mavContainer.getModel().get(name);
  }
  else {
   // Create attribute instance
   try {
    attribute = createAttribute(name, parameter, binderFactory, webRequest);
   }
   catch (BindException ex) {
    if (isBindExceptionRequired(parameter)) {
     // No BindingResult parameter -> fail with BindException
     throw ex;
    }
    // Otherwise, expose null/empty value and associated BindingResult
    if (parameter.getParameterType() == Optional.class) {
     attribute = Optional.empty();
    }
    bindingResult = ex.getBindingResult();
   }
  } 
        // Perform parameter binding and verification 
  if (bindingResult == null) {
   //  Binding and data verification of attribute objects ;
   //  Skipped when binding properties with constructors fails .
   WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
           
   if (binder.getTarget() != null) {
    if (!mavContainer.isBindingDisabled(name)) {
     bindRequestParameters(binder, webRequest);
    }
                //  Check the binding parameters, and if the check fails, give the result information to bindingResult Object 
    validateIfApplicable(binder, parameter);
                //  Throw an exception if the result of getting parameter binding contains wrong information 
    if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
     throw new BindException(binder.getBindingResult());
    }
   }
   // Value type adaptation, also covering java.util.Optional
   if (!parameter.getParameterType().isInstance(attribute)) {
    attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
   }
   bindingResult = binder.getBindingResult();
  }
 
  // Add resolved attribute and BindingResult at the end of the model
  Map<String, Object> bindingResultModel = bindingResult.getModel();
  mavContainer.removeAttributes(bindingResultModel);
  mavContainer.addAllAttributes(bindingResultModel); 
  return attribute;
}

By looking at the source code, when there is an error message in BindingResult, an BindException exception will be thrown. Look at the source code of BindException as follows:


/**
 * Thrown when binding errors are considered fatal. Implements the
 * {@link BindingResult} interface (and its super-interface {@link Errors})
 * to allow for the direct analysis of binding errors.
 *
 * <p>As of Spring 2.0, this is a special-purpose class. Normally,
 * application code will work with the {@link BindingResult} interface,
 * or with a {@link DataBinder} that in turn exposes a BindingResult via
 * {@link org.springframework.validation.DataBinder#getBindingResult()}.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @author Rob Harrop
 * @see BindingResult
 * @see DataBinder#getBindingResult()
 * @see DataBinder#close()
 */
@SuppressWarnings("serial")
public class BindException extends Exception implements BindingResult { 
 private final BindingResult bindingResult; 
 
 /**
  * Create a new BindException instance for a BindingResult.
  * @param bindingResult the BindingResult instance to wrap
  */
 public BindException(BindingResult bindingResult) {
  Assert.notNull(bindingResult, "BindingResult must not be null");
  this.bindingResult = bindingResult;
 }

We find that BindException implements BindingResult interface (BindResult is a general interface for binding results, and BindResult inherits from Errors interface), so this exception class has all the relevant information of BindingResult, so we can analyze and deal with its error results by catching this exception class. The code is as follows:


/**
 *  Exception handling method for method parameter verification 
 */
@ExceptionHandler(BindException.class)
public ResponseEntity<Map<String, Object>> handlerNotValidException(BindException exception) {
    log.debug("begin resolve argument exception");
    BindingResult result = exception.getBindingResult();
    Map<String, Object> maps;
 
    if (result.hasErrors()) {
        List<FieldError> fieldErrors = result.getFieldErrors();
        maps = new HashMap<>(fieldErrors.size());
        fieldErrors.forEach(error -> {
            maps.put(error.getField(), error.getDefaultMessage());
        });
    } else {
        maps = Collections.EMPTY_MAP;
    } 
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(maps);
}

In this way, we solve the exception handling of parameter verification of requests of content-type type and form (form) type. The reason why the exception of MethodArgumentNotValidException does not work is mainly because it is related to the data format of the request (content-type), and different HttpMessageConverter (http parameter converter) is used for different transmission data formats spring. The following is a brief introduction to HttpMessageConverter:

The transport of HTTP requests and responses is a byte stream, meaning that the browser and server communicate through the byte stream. However, using methods in the Spring, controller classes returns pure String types or other Java built-in objects. How to convert an object into a byte stream for transmission?

There is a problem of converting a byte stream to an java object when a message arrives at SpringMVC and exits from SpringMVC. In SpringMVC, it is handled by HttpMessageConverter.

When the request message comes to java, it will be encapsulated into an ServletInputStream input stream for us to read the message. The response message is to output the response message through the output stream of an ServletOutputStream. The http request and the corresponding processing procedure are as follows:

springmvc uses different message converters for different data formats. The following is the built-in message converter of springmvc:

From this, we can see that when json is used as the transmission format, springmvc will adopt MappingJacksonHttpMessageConverter message converter, and the underlying layer throws MethodArgumentNotValidException exception when checking errors on parameters, so we need to manage BindException and MethodArgumentNotValidException in a unified way. The final code demonstration is as follows:


/**
     *  Exception handling method for method parameter verification ( Valid only for form submission, and valid for the json Format submission will be invalid )
     *  If it is a form type submission, the spring Will be processed by the processing class of the form data (will be thrown when the parameter validation error is made BindException Exception) 
     */
    @ExceptionHandler(BindException.class)
    public ResponseEntity<Map<String, Object>> handlerBindException(BindException exception) {
        return handlerNotValidException(exception);
    }
 
    /**
     *  Exception handling method for method parameter verification ( The front-end submission method is json Is handled by the exception class when an exception occurs in the format )
     * json When the format is submitted, spring Will adopt json The data converter of the data is processed (the error is thrown when the parameter is checked MethodArgumentNotValidException Exception) 
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handlerArgumentNotValidException(MethodArgumentNotValidException exception) {
        return handlerNotValidException(exception);
    }
 
    public ResponseEntity<Map<String, Object>> handlerNotValidException(Exception e) {
        log.debug("begin resolve argument exception");
        BindingResult result;
        if (e instanceof BindException) {
            BindException exception = (BindException) e;
            result = exception.getBindingResult();
        } else {
            MethodArgumentNotValidException exception = (MethodArgumentNotValidException) e;
            result = exception.getBindingResult();
        }
 
        Map<String, Object> maps;
        if (result.hasErrors()) {
            List<FieldError> fieldErrors = result.getFieldErrors();
            maps = new HashMap<>(fieldErrors.size());
            fieldErrors.forEach(error -> {
                maps.put(error.getField(), error.getDefaultMessage());
            });
        } else {
            maps = Collections.EMPTY_MAP;
        }
         return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(maps); 
    }

In this way, we perfectly solve the unified 1 treatment of parameter check anomalies.

Here, I only deal with the exception of parameter verification, that is, the response code returned to the front end is 400 (parameter format error). For custom exceptions or other exceptions, this way can be used to deal with exceptions.


Related articles: