1. 程式人生 > 其它 >SpringMVC引數校驗(針對`@RequestBody`返回`400`)

SpringMVC引數校驗(針對`@RequestBody`返回`400`)

SpringMVC引數校驗(針對@RequestBody返回400

From https://ryan-miao.github.io/2017/05/20/spring400/

前言

習慣別人幫忙做事的結果是自己不會做事了。一直以來,spring幫我解決了程式執行中的各種問題,我只要關心我的業務邏輯,設計好我的業務程式碼,返回正確的結果即可。直到遇到了400

spring返回400的時候通常沒有任何錯誤提示,當然也通常是引數不匹配。這在引數少的情況下還可以一眼看穿,但當引數很大是,排除引數也很麻煩,更何況,既然錯誤了,為什麼指出來原因呢。好吧,springmvc把這個權力交給了使用者自己。

springmvc異常處理

最開始的時候也想過自己攔截會出異常的method來進行異常處理,但顯然不需要這麼做。spring提供了內嵌的以及全域性的異常處理方法,基本可以滿足我的需求了。

1. 內嵌異常處理

如果只是這個controller的異常做單獨處理,那麼就適合繫結這個controller本身的異常。

具體做法是使用註解@ExceptionHandler.

在這個controller中新增一個方法,並新增上述註解,並指明要攔截的異常。

@RequestMapping(value = "saveOrUpdate", method = RequestMethod.POST)
public String saveOrUpdate(HttpServletResponse response, @RequestBody Order order){
    CodeMsg result = null;
    try {
        result = orderService.saveOrUpdate(order);
    } catch (Exception e) {
        logger.error("save failed.", e);
        return this.renderString(response, CodeMsg.error(e.getMessage()));
    }
    return this.renderString(response, result);
}

@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public CodeMsg messageNotReadable(HttpMessageNotReadableException exception, HttpServletResponse response){
    LOGGER.error("請求引數不匹配。", exception);
    return CodeMsg.error(exception.getMessage());
}

這裡saveOrUpdate是我們想要攔截一樣的請求,而messageNotReadable則是處理異常的程式碼。 @ExceptionHandler(HttpMessageNotReadableException.class)表示我要攔截何種異常。在這裡,由於springmvc預設採用jackson作為json序列化工具,當反序列化失敗的時候就會丟擲HttpMessageNotReadableException異常。具體如下:

{
  "code": 1,
  "msg": "Could not read JSON: Failed to parse Date value '2017-03-' (format: "yyyy-MM-dd HH:mm:ss"): Unparseable date: "2017-03-" (through reference chain: com.test.modules.order.entity.Order["serveTime"]); nested exception is com.fasterxml.jackson.databind.JsonMappingException: Failed to parse Date value '2017-03-' (format: "yyyy-MM-dd HH:mm:ss"): Unparseable date: "2017-03-" (through reference chain: com.test.modules.order.entity.Order["serveTime"])",
  "data": ""
}

這是個典型的jackson反序列化失敗異常,也是造成我遇見過的400原因最多的。通常是日期格式不對。

另外,@ResponseStatus(HttpStatus.BAD_REQUEST)這個註解是為了標識這個方法返回值的HttpStatus code。我設定為400,當然也可以自定義成其他的。

2. 批量異常處理

看到大多數資料寫的是全域性異常處理,我覺得對我來說批量更合適些,因為我只是希望部分controller被攔截而不是全部。

springmvc提供了@ControllerAdvice來做批量攔截。

第一次看到註釋這麼少的原始碼,忍不住多讀幾遍。

Indicates the annotated class assists a "Controller".

表示這個註解是服務於Controller的。

Serves as a specialization of {@link Component @Component}, allowing for implementation classes to be autodetected through classpath scanning.

用來當做特殊的Component註解,允許使用者掃描發現所有的classpath

It is typically used to define {@link ExceptionHandler @ExceptionHandler},
 * {@link InitBinder @InitBinder}, and {@link ModelAttribute @ModelAttribute}
 * methods that apply to all {@link RequestMapping @RequestMapping} methods.

典型的應用是用來定義xxxx.

One of {@link #annotations()}, {@link #basePackageClasses()},
 * {@link #basePackages()} or its alias {@link #value()}
 * may be specified to define specific subsets of Controllers
 * to assist. When multiple selectors are applied, OR logic is applied -
 * meaning selected Controllers should match at least one selector.

這幾個引數指定了掃描範圍。

the default behavior (i.e. if used without any selector),
 * the {@code @ControllerAdvice} annotated class will
 * assist all known Controllers.

預設掃描所有的已知的的Controllers。

Note that those checks are done at runtime, so adding many attributes and using
 * multiple strategies may have negative impacts (complexity, performance).

注意這個檢查是在執行時做的,所以注意效能問題,不要放太多的引數。

說的如此清楚,以至於用法如此簡單。

@ResponseBody
@ControllerAdvice("com.api")
public class ApiExceptionHandler extends BaseClientController {
    private static final Logger LOGGER = LoggerFactory.getLogger(ApiExceptionHandler.class);

    /**
     *
     * @param exception UnexpectedTypeException
     * @param response
     * @return
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(UnexpectedTypeException.class)
    public CodeMsg unexpectedType(UnexpectedTypeException exception, HttpServletResponse response){
        LOGGER.error("校驗方法太多,不確定合適的校驗方法。", exception);
        return CodeMsg.error(exception.getMessage());
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public CodeMsg messageNotReadable(HttpMessageNotReadableException exception, HttpServletResponse response){
        LOGGER.error("請求引數不匹配。", exception);
        return CodeMsg.error(exception.getMessage());
    }

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(Exception.class)
    public CodeMsg ex(MethodArgumentNotValidException exception, HttpServletResponse response){
        LOGGER.error("請求引數不合法。", exception);
        BindingResult bindingResult = exception.getBindingResult();
        String msg = "校驗失敗";
        return new CodeMsg(CodeMsgConstant.error, msg, getErrors(bindingResult));
    }

    private Map<String, String> getErrors(BindingResult result) {
        Map<String, String> map = new HashMap<>();
        List<FieldError> list = result.getFieldErrors();
        for (FieldError error : list) {
            map.put(error.getField(), error.getDefaultMessage());
        }
        return map;
    }
}

3. Hibernate-validate

使用引數校驗如果不catch異常就會返回400. 所以這個也要規範一下。

3.1 引入hibernate-validate
<dependency>  
   <groupId>org.hibernate</groupId>  
   <artifactId>hibernate-validator</artifactId>  
   <version>5.0.2.Final</version>  
</dependency>
<mvc:annotation-driven validator="validator" />
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
  <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
  <property name="validationMessageSource" ref="messageSource"/>
</bean>
3.2 使用
  1. 在實體類欄位上標註要求 public class AlipayRequest { @NotEmpty private String out_trade_no; private String subject; @DecimalMin(value = "0.01", message = "費用最少不能小於0.01") @DecimalMax(value = "100000000.00", message = "費用最大不能超過100000000") private String total_fee; /** * 訂單型別 */ @NotEmpty(message = "訂單型別不能為空") private String business_type; //.... }
  2. controller裡新增@Valid
@RequestMapping(value = "sign", method = RequestMethod.POST)
    public String sign(@Valid @RequestBody AlipayRequest params
    ){
        ....
    }

3.錯誤處理 前面已經提到,如果不做處理的結果就是400,415. 這個對應Exception是MethodArgumentNotValidException,也是這樣:

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(Exception.class)
public CodeMsg ex(MethodArgumentNotValidException exception, HttpServletResponse response){
    LOGGER.error("請求引數不合法。", exception);
    BindingResult bindingResult = exception.getBindingResult();
    String msg = "校驗失敗";
    return new CodeMsg(CodeMsgConstant.error, msg, getErrors(bindingResult));
}

private Map<String, String> getErrors(BindingResult result) {
    Map<String, String> map = new HashMap<>();
    List<FieldError> list = result.getFieldErrors();
    for (FieldError error : list) {
        map.put(error.getField(), error.getDefaultMessage());
    }
    return map;
}

返回結果:

{
  "code": 1,
  "msg": "校驗失敗",
  "data": {
    "out_trade_no": "不能為空",
    "business_type": "訂單型別不能為空"
  }
}

大概有這麼幾個限制註解:

/**
 * Bean Validation 中內建的 constraint       
 * @Null   被註釋的元素必須為 null       
 * @NotNull    被註釋的元素必須不為 null       
 * @AssertTrue     被註釋的元素必須為 true       
 * @AssertFalse    被註釋的元素必須為 false       
 * @Min(value)     被註釋的元素必須是一個數字,其值必須大於等於指定的最小值       
 * @Max(value)     被註釋的元素必須是一個數字,其值必須小於等於指定的最大值       
 * @DecimalMin(value)  被註釋的元素必須是一個數字,其值必須大於等於指定的最小值       
 * @DecimalMax(value)  被註釋的元素必須是一個數字,其值必須小於等於指定的最大值       
 * @Size(max=, min=)   被註釋的元素的大小必須在指定的範圍內       
 * @Digits (integer, fraction)     被註釋的元素必須是一個數字,其值必須在可接受的範圍內       
 * @Past   被註釋的元素必須是一個過去的日期       
 * @Future     被註釋的元素必須是一個將來的日期       
 * @Pattern(regex=,flag=)  被註釋的元素必須符合指定的正則表示式       
 * Hibernate Validator 附加的 constraint       
 * @NotBlank(message =)   驗證字串非null,且長度必須大於0       
 * @Email  被註釋的元素必須是電子郵箱地址       
 * @Length(min=,max=)  被註釋的字串的大小必須在指定的範圍內       
 * @NotEmpty   被註釋的字串的必須非空       
 * @Range(min=,max=,message=)  被註釋的元素必須在合適的範圍內 
 */