有參數傳遞的地方都少不了參數校驗。在web開發中,前端的參數校驗是為了用戶體驗,后端的參數校驗是為了安全。
試想一下,如果在controller層中沒有經過任何校驗的參數通過service層、dao層一路來到了數據庫就可能導致嚴重的后果,最好的結果是查不出數據,
嚴重一點就是報錯,如果這些沒有被校驗的參數中包含了惡意代碼,那就可能導致更嚴重的后果。
因此,對於請求參數,一般上都需要進行參數合法性校驗的,原先的寫法時一個個字段一個個去判斷,這種方式太不通用了,並且當參數較多的時候,代碼回顯的很臃腫,例如:
這是一個 bad practise !
所以java的JSR 303: Bean Validation規范就是解決這個問題的。
JSR 303只是個規范,並沒有具體的實現,目前通常都是由hibernate-validator進行統一參數校驗。
JSR303定義的基礎校驗類型:
|
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(value) |
被注釋的元素必須符合指定的正則表達式 |
Hibernate Validator 中附加的 constraint :
|
Constraint
|
詳細信息
|
|---|---|
@Email |
被注釋的元素必須是電子郵箱地址 |
@Length |
被注釋的字符串的大小必須在指定的范圍內 |
@NotEmpty |
被注釋的字符串的必須非空 |
@Range |
被注釋的元素必須在合適的范圍內 |
下面通過一個例子來使用一下:
@Data
@ApiModel
(
"回帖風控放行后發短消息提醒"
)
public
class
PostCheckPassDTO {
@NotNull
(
message =
"回帖用戶puid不能為空"
)
@ApiModelProperty
(
"回帖用戶puid,必傳"
)
private
Long puid;
@NotNull
(
message =
"回帖用戶username不能為空"
)
@ApiModelProperty
(
"回帖用戶username,必傳"
)
private
String username;
@NotNull
(
message =
"帖子id不能為空"
)
@ApiModelProperty
(
"帖子id,必傳"
)
private
Long tid;
@NotNull
(
message =
"回帖id不能為空"
)
@ApiModelProperty
(
"回帖id"
)
private
Long pid;
@NotNull
(
message =
"回帖內容不能為空"
)
@ApiModelProperty
(
"回帖內容"
)
private
String content;
@ApiModelProperty
(
"回帖引用回帖id"
)
private
Long quote;
}
|
要注意加上@Valid注解開啟校驗
@PostMapping
(
"sendRemindForCheckPassPosts"
)
public
DataResult<Boolean> sendRemindForCheckPassPosts(
@Valid
@RequestBody
PostCheckPassDTO postCheckPassDTO){
userBbsWriteService.sendRemindForCheckPassPosts(postCheckPassDTO);
return
DataResult.ok(
true
);
}
|
這時候我們把參數里面的username去掉,請求一下接口,響應結果如下:
看到在校驗不通過時,返回的異常信息是不友好的,此時可利用統一異常處理,對校驗異常進行特殊處理,
特別說明下,對於異常處理類,共有以下幾種情況(被@RequestBody和@RequestParam注解的請求實體,校驗異常類是不同的),上代碼:
@Slf4j
@ControllerAdvice
public
class
GlobalExceptionHandler {
/**
* 異常處理
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler
(value = Exception.
class
)
public
DataResult exceptionHandler(Exception e) {
log.error(
"GlobalExceptionHandler.exceptionHandler , 異常信息"
,e);
/**cat error 上報*/
Cat.logErrorWithCategory(
"GlobalExceptionHandler"
,
"global error"
,e);
return
DataResult.fail(e.getMessage());
}
/**
* 業務異常
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler
(value = BplCommonException.
class
)
public
DataResult bplCommonExceptionHandler(BplCommonException e) {
log.warn(
""
,e);
return
DataResult.fail(e.getMessage());
}
/**
* 處理所有RequestBody注解參數驗證異常
* @param e
* @return
*/
@ExceptionHandler
(MethodArgumentNotValidException.
class
)
@ResponseBody
public
DataResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
log.warn(e.getMessage());
return
DataResult.fail(e.getBindingResult().getAllErrors().get(
0
).getDefaultMessage());
}
/**
* 處理所有RequestParam注解數據驗證異常
* @param ex
* @return
*/
@ExceptionHandler
(BindException.
class
)
public
DataResult handleBindException(BindException ex) {
FieldError fieldError = ex.getBindingResult().getFieldError();
log.warn(
"必填校驗異常:{}({})"
, fieldError.getDefaultMessage(),fieldError.getField());
return
DataResult.fail(ex.getBindingResult().getAllErrors().get(
0
).getDefaultMessage());
}
}
|
添加全局異常處理器后,這時我們再次請求接口,看看相應結果:
上面是最通用也是最常見的使用方式,可以解決絕大部分參數校驗的問題。還有剩下的小部分,我們可以通過自定義校驗注解的方式解決, 比如我們定義一個檢查版本號的注解:
@Documented
//指定注解的處理類
@Constraint
(validatedBy = {VersionValidatorHandler.
class
})
@Target
({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention
(RUNTIME)
public
@interface
ConstantVersion {
String message()
default
"{constraint.default.const.message}"
;
Class<?>[] groups()
default
{};
Class<?
extends
Payload>[] payload()
default
{};
String value();
}
|
注解處理類 VersionValidatorHandler
public
class
VersionValidatorHandler
implements
ConstraintValidator<Constant, String> {
private
String constant;
@Override
public
void
initialize(Constant constraintAnnotation) {
//獲取設置的字段值
this
.constant = constraintAnnotation.value();
}
@Override
public
boolean
isValid(String value, ConstraintValidatorContext context) {
//判斷參數是否等於設置的字段值,返回結果
return
constant.equals(value);
}
}
|
注解的使用:
@ConstantVersion
(message =
"verson只能為1.0.0"
,value=
"1.0.0"
)
String version;
|
Spring Validation實現原理分析



