@validated註解異常返回JSON值方式

@validated註解異常返回JSON值

@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)這裡@ControllerAdvice註解標註,@ControllerAdvice是@Controller的增強版,一般與@ExceptionHandler搭配使用。

如果標註@Controller,異常處理隻會在當前controller類中的方法起作用,但是使用@ControllerAdvice,則全局有效。

(2)@ExceptionHandler註解裡面填寫想要捕獲的異常類class對象

使用@Valid註解,對其參數錯誤異常的統一處理

在我們使用springboot作為微服務框架進行敏捷開發的時候,為瞭保證傳遞數據的安全性,需要對傳遞的數據進行校驗,但是在以往的開發中,開發人員花費大量的時間在繁瑣的if else 等判斷語句來對參數進行校驗,這種方式不但降低瞭我們的開發速度,而且寫出來的代碼中帶有很多冗餘代碼,使得編寫的代碼不夠優雅,為瞭將參數的驗證邏輯和代碼的業務邏輯進行解耦,Java給我們提供瞭@Valid註解,用來幫助我們進行參數的校驗,實現瞭將業務邏輯和參數校驗邏輯在一定程度上的解耦,增加瞭代碼的簡潔性和可讀性。

springboot中自帶瞭spring validation參數校驗框架,其使用上和@valid差不多,在這裡就不累述瞭,本文主要講解@valid的使用對其參數校驗失敗後的錯誤一樣的統一處理。

首先,簡介對微服務開發中異常的統一處理,spring中的@RestControllerAdvice註解可以獲取帶有@controller註解類的異常,通過@ExceptionHandler(MyException.class)註解來共同完成對異常進行處理。示例如下:

/**
 * 通用異常攔截處理類(通過切面的方式默認攔截所有的controller異常)
 */
@Slf4j
@RestControllerAdvice
public class CommonExceptionHandler {
 
    /**
     * 對運行時異常進行統一異常管理方法
     * @param e
     * @return
     */
    @ExceptionHandler(FlyException.class) // FlyException類繼承於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);
    }

通過註解@RestControllerAdvice和註解@ExceptionHandler的聯合使用來實現對異常的統一處理,然後在前端以友好的方式顯示。

使用@Valid註解的示例如下:

  @PostMapping
    public ResponseEntity save(@Valid BrandCreateRequestDto dto, BindingResult bindingResult) {
        // 判斷是否含有校驗不匹配的參數錯誤
        if (bindingResult.hasErrors()) {
            // 獲取所有字段參數不匹配的參數集合
            List<FieldError> fieldErrorList = bindingResult.getFieldErrors();
            Map<String, Object> result = new HashMap<>(fieldErrorList.size());
            fieldErrorList.forEach(error -> {
                // 將錯誤參數名稱和參數錯誤原因存於map集合中
                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();
    }

@Valid註解確實將我們原來的參數校驗的問題進行瞭簡化,但是,如果我們有多個handler需要處理,那我們豈不是每次都要寫這樣的冗餘代碼。通過查看@valid的實現機制(這裡就不描述瞭),當參數校驗失敗的時候,會拋出MethodArgumentNotValidException異常(當用{@code @Valid}註釋的參數在驗證失敗時,將引發該異常):

/**
 * 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;
 }

按照我們的預想,我們隻需要在原來定義的統一異常處理類中,捕獲MethodArgumentNotValidException異常,然後對其錯誤信息進行分析和處理即可實現通用,代碼如下:

/**
 * 對方法參數校驗異常處理方法
 */
@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);
}

但是經過測試,會發現對該異常進行捕獲然後處理是沒有效果的,這可能是我,也是大傢遇到的問題之一,經過對@Valid的執行過程的源碼進行分析,數據傳遞到spring中的執行過程大致為:前端通過http協議將數據傳遞到spring,spring通過HttpMessageConverter類將流數據轉換成Map類型,然後通過ModelAttributeMethodProcessor類對參數進行綁定到方法對象中,並對帶有@Valid或@Validated註解的參數進行參數校驗,對參數進行處理和校驗的方法為ModelAttributeMethodProcessor.resolveArgument(…),部分源代碼如下所示:

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();
   }
  } 
        //進行參數綁定和校驗
  if (bindingResult == null) {
   // 對屬性對象的綁定和數據校驗;
   // 使用構造器綁定屬性失敗時跳過.
   WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
           
   if (binder.getTarget() != null) {
    if (!mavContainer.isBindingDisabled(name)) {
     bindRequestParameters(binder, webRequest);
    }
                // 對綁定參數進行校驗,校驗失敗,將其結果信息賦予bindingResult對象
    validateIfApplicable(binder, parameter);
                // 如果獲取參數綁定的結果中包含錯誤的信息則拋出異常
    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;
}

通過查看源碼,當BindingResult中存在錯誤信息時,會拋出BindException異常,查看BindException源代碼如下:

/**
 * 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;
 }

我們發現BindException實現瞭BindingResult接口(BindResult是綁定結果的通用接口, BindResult繼承於Errors接口),所以該異常類擁有BindingResult所有的相關信息,因此我們可以通過捕獲該異常類,對其錯誤結果進行分析和處理。代碼如下:

/**
 * 對方法參數校驗異常處理方法
 */
@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);
}

這樣,我們對是content-type類型為form(表單)類型的請求的參數校驗的異常處理就解決瞭,對於MethodArgumentNotValidException異常不起作用的原因主要是因為跟請求發起的數據格式(content-type)有關系,對於不同的傳輸數據的格式spring采用不同的HttpMessageConverter(http參數轉換器)來進行處理.以下是對HttpMessageConverter進行簡介:

HTTP 請求和響應的傳輸是字節流,意味著瀏覽器和服務器通過字節流進行通信。但是,使用 Spring,controller 類中的方法返回純 String 類型或其他 Java 內建對象。如何將對象轉換成字節流進行傳輸?

在報文到達SpringMVC和從SpringMVC出去,都存在一個字節流到java對象的轉換問題。在SpringMVC中,它是由HttpMessageConverter來處理的。

當請求報文來到java中,它會被封裝成為一個ServletInputStream的輸入流,供我們讀取報文。響應報文則是通過一個ServletOutputStream的輸出流,來輸出響應報文。http請求與相應的處理過程如下:

針對不同的數據格式,springmvc會采用不同的消息轉換器進行處理,以下是springmvc的內置消息轉換器:

由此我們可以看出,當使用json作為傳輸格式時,springmvc會采用MappingJacksonHttpMessageConverter消息轉換器, 而且底層在對參數進行校驗錯誤時,拋出的是MethodArgumentNotValidException異常,因此我們需要對BindException和MethodArgumentNotValidException進行統一異常管理,最終代碼演示如下所示:

/**
     * 對方法參數校驗異常處理方法(僅對於表單提交有效,對於以json格式提交將會失效)
     * 如果是表單類型的提交,則spring會采用表單數據的處理類進行處理(進行參數校驗錯誤時會拋出BindException異常)
     */
    @ExceptionHandler(BindException.class)
    public ResponseEntity<Map<String, Object>> handlerBindException(BindException exception) {
        return handlerNotValidException(exception);
    }
 
    /**
     * 對方法參數校驗異常處理方法(前端提交的方式為json格式出現異常時會被該異常類處理)
     * json格式提交時,spring會采用json數據的數據轉換器進行處理(進行參數校驗時錯誤是拋出MethodArgumentNotValidException異常)
     */
    @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); 
    }

這樣就完美解決瞭我們對參數校驗異常的統一處理。

在這裡我僅僅是針對參數校驗的異常進行瞭統一處理,也就是返回給前端的響應碼是400(參數格式錯誤),對於自定義異常或者其他的異常都可以采用這種方式來對異常進行統一處理。

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: