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.
默認掃描所有的已知的的Controller
s。
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(MethodArgumentNotValidException .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 使用
- 在實體類字段上標注要求
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;
//....
}
- 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=) 被注釋的元素必須在合適的范圍內
*/