大家好,我是飄渺。
前幾天寫了一篇 SpringBoot如何統一後端返回格式?老鳥們都是這樣玩的! 閱讀效果還不錯,而且被很多號主都轉載過,今天我們繼續第二篇,來聊聊在SprinBoot中如何整合引數校驗Validator,以及引數校驗的高階技巧(自定義校驗,分組校驗)。
此文是依賴於前文的程式碼基礎,已經在專案中加入了全域性異常校驗器。(程式碼倉庫在文末)
首先我們來看看什麼是Validator引數校驗器,為什麼需要引數校驗?
為什麼需要引數校驗
在日常的介面開發中,為了防止非法引數對業務造成影響,經常需要對介面的引數做校驗,例如登入的時候需要校驗使用者名稱密碼是否為空,建立使用者的時候需要校驗郵件、手機號碼格式是否準確。靠程式碼對介面引數一個個校驗的話就太繁瑣了,程式碼可讀性極差。
Validator框架就是為了解決開發人員在開發的時候少寫程式碼,提升開發效率;Validator專門用來進行介面引數校驗,例如常見的必填校驗,email格式校驗,使用者名稱必須位於6到12之間 等等…
Validator校驗框架遵循了JSR-303驗證規範(引數校驗規範), JSR是
Java Specification Requests
的縮寫。
接下來我們看看在SpringbBoot中如何整合引數校驗框架。
SpringBoot中整合引數校驗
第一步,引入依賴
<
dependency
>
<
groupId
>
org。springframework。boot
groupId
>
<
artifactId
>
spring-boot-starter-web
artifactId
>
dependency
>
<
dependency
>
<
groupId
>
org。springframework。boot
groupId
>
<
artifactId
>
spring-boot-starter-validation
artifactId
>
dependency
>
注:從
springboot-2。3
開始,校驗包被獨立成了一個
starter
元件,所以需要引入validation和web,而
springboot-2。3
之前的版本只需要引入 web 依賴就可以了。
第二步,定義要引數校驗的實體類
@Data
public
class
ValidVO
{
private
String
id
;
@Length
(
min
=
6
,
max
=
12
,
message
=
“appId長度必須位於6到12之間”
)
private
String
appId
;
@NotBlank
(
message
=
“名字為必填項”
)
private
String
name
;
(
message
=
“請填寫正確的郵箱地址”
)
private
String
;
private
String
sex
;
@NotEmpty
(
message
=
“級別不能為空”
)
private
String
level
;
}
在實際開發中對於需要校驗的欄位都需要設定對應的業務提示,即message屬性。
常見的約束註解如下:
注:此表格只是簡單的對註解功能的說明,並沒有對每一個註解的屬性進行說明;可詳見原始碼。
第三步,定義校驗類進行測試
@RestController
@Slf4j
@Validated
public
class
ValidController
{
@ApiOperation
(
“RequestBody校驗”
)
@PostMapping
(
“/valid/test1”
)
public
String
test1
(
@Validated
@RequestBody
ValidVO
validVO
)
{
log
。
info
(
“validEntity is {}”
,
validVO
)
;
return
“test1 valid success”
;
}
@ApiOperation
(
“Form校驗”
)
@PostMapping
(
value
=
“/valid/test2”
)
public
String
test2
(
@Validated
ValidVO
validVO
)
{
log
。
info
(
“validEntity is {}”
,
validVO
)
;
return
“test2 valid success”
;
}
@ApiOperation
(
“單引數校驗”
)
@PostMapping
(
value
=
“/valid/test3”
)
public
String
test3
(
String
)
{
log
。
info
(
“email is {}”
,
)
;
return
“email valid success”
;
}
}
這裡我們先定義三個方法test1,test2,test3,test1使用了
@RequestBody
註解,用於接受前端傳送的json資料,test2模擬表單提交,test3模擬單引數提交。
注意,當使用單引數校驗時需要在Controller上加上@Validated註解,否則不生效
。
第四步,體驗效果
呼叫test1方法,提示的是
org。springframework。web。bind。MethodArgumentNotValidException
異常
POST
http
:
/
/
localhost
:
8080
/
valid
/
test1Content
-
Type
:
application
/
json
{
“id”
:
1
,
“level”
:
“12”
,
“email”
:
“47693899”
,
“appId”
:
“ab1c”
}
{
“status”
:
500
,
“message”
:
“Validation failed for argument [0] in public java。lang。String com。jianzh5。blog。valid。ValidController。test1(com。jianzh5。blog。valid。ValidVO) with 3 errors: [Field error in object ‘validVO’ on field ‘email’: rejected value [47693899]; codes [Email。validVO。email,Email。email,Email。java。lang。String,Email]; arguments [org。springframework。context。support。DefaultMessageSourceResolvable: codes [validVO。email,email]; arguments []; default message [email],[Ljavax。validation。constraints。Pattern$Flag;@26139123,。*]; default message [不是一個合法的電子郵件地址]]。。。”
,
“data”
:
null
,
“timestamp”
:
1628239624332
}
呼叫test2方法,提示的是
org。springframework。validation。BindException
異常
POST
http
:
/
/
localhost
:
8080
/
valid
/
test2Content
-
Type
:
application
/
x
-
www
-
form
-
urlencodedid
=
1
&
level
=
12
&
=
476938977
&
appId
=
ab1c
{
“status”
:
500
,
“message”
:
“org。springframework。validation。BeanPropertyBindingResult: 3 errors\nField error in object ‘validVO’ on field ‘name’: rejected value [null]; codes [NotBlank。validVO。name,NotBlank。name,NotBlank。java。lang。String,NotBlank]; arguments [org。springframework。context。support。DefaultMessageSourceResolvable: codes [validVO。name,name]; arguments []; default message [name]]; default message [名字為必填項]。。。”
,
“data”
:
null
,
“timestamp”
:
1628239301951
}
呼叫test3方法,提示的是
javax。validation。ConstraintViolationException
異常
POST
http
:
/
/
localhost
:
8080
/
valid
/
test3Content
-
Type
:
application
/
x
-
www
-
form
-
urlencodedemail
=
476938977
{
“status”
:
500
,
“message”
:
“test3。email: 不是一個合法的電子郵件地址”
,
“data”
:
null
,
“timestamp”
:
1628239281022
}
透過加入
Validator
校驗框架可以幫助我們自動實現引數的校驗。
引數異常加入全域性異常處理器
雖然我們之前定義了全域性異常攔截器,也看到了攔截器確實生效了,但是
Validator
校驗框架返回的錯誤提示太臃腫了,不便於閱讀,為了方便前端提示,我們需要將其簡化一下。
直接修改之前定義的
RestExceptionHandler
,單獨攔截引數校驗的三個異常:
javax。validation。ConstraintViolationException
,
org。springframework。validation。BindException
,
org。springframework。web。bind。MethodArgumentNotValidException
,程式碼如下:
@ExceptionHandler
(
value
=
{
BindException
。
class
,
ValidationException
。
class
,
MethodArgumentNotValidException
。
class
}
)
public
ResponseEntity
<
ResultData
<
String
>
>
handleValidatedException
(
Exception
e
)
{
ResultData
<
String
>
resp
=
null
;
if
(
e
instanceof
MethodArgumentNotValidException
)
{
// BeanValidation exception
MethodArgumentNotValidException
ex
=
(
MethodArgumentNotValidException
)
e
;
resp
=
ResultData
。
fail
(
HttpStatus
。
BAD_REQUEST
。
value
(
)
,
ex
。
getBindingResult
(
)
。
getAllErrors
(
)
。
stream
(
)
。
map
(
ObjectError
::
getDefaultMessage
)
。
collect
(
Collectors
。
joining
(
“; ”
)
)
)
;
}
else
if
(
e
instanceof
ConstraintViolationException
)
{
// BeanValidation GET simple param
ConstraintViolationException
ex
=
(
ConstraintViolationException
)
e
;
resp
=
ResultData
。
fail
(
HttpStatus
。
BAD_REQUEST
。
value
(
)
,
ex
。
getConstraintViolations
(
)
。
stream
(
)
。
map
(
ConstraintViolation
::
getMessage
)
。
collect
(
Collectors
。
joining
(
“; ”
)
)
)
;
}
else
if
(
e
instanceof
BindException
)
{
// BeanValidation GET object param
BindException
ex
=
(
BindException
)
e
;
resp
=
ResultData
。
fail
(
HttpStatus
。
BAD_REQUEST
。
value
(
)
,
ex
。
getAllErrors
(
)
。
stream
(
)
。
map
(
ObjectError
::
getDefaultMessage
)
。
collect
(
Collectors
。
joining
(
“; ”
)
)
)
;
}
return
new
ResponseEntity
<
>
(
resp
,
HttpStatus
。
BAD_REQUEST
)
;
}
體驗效果
POST http
:
/
/
localhost
:
8080
/
valid
/
test1
Content
-
Type
:
application
/
json
{
“id”
:
1
,
“level”
:
“12”
,
“email”
:
“47693899”
,
“appId”
:
“ab1c”
}
{
“status”
:
400
,
“message”
:
“名字為必填項; 不是一個合法的電子郵件地址; appId長度必須位於6到12之間”
,
“data”
:
null
,
“timestamp”
:
1628435116680
}
是不是感覺清爽多了?
自定義引數校驗
雖然Spring Validation 提供的註解基本上夠用,但是面對複雜的定義,我們還是需要自己定義相關注解來實現自動校驗。
比如上面實體類中的sex性別屬性,只允許前端傳遞傳 M,F 這2個列舉值,如何實現呢?
第一步,建立自定義註解
@Target
(
{
METHOD
,
FIELD
,
ANNOTATION_TYPE
,
CONSTRUCTOR
,
PARAMETER
,
TYPE_USE
}
)
@Retention
(
RUNTIME
)
@Repeatable
(
EnumString
。
List
。
class
)
@Documented
@Constraint
(
validatedBy
=
EnumStringValidator
。
class
)
//標明由哪個類執行校驗邏輯
public
@interface
EnumString
{
String
message
(
)
default
“value not in enum values。”
;
Class
<
?
>
[
]
groups
(
)
default
{
}
;
Class
<
?
extends
Payload
>
[
]
payload
(
)
default
{
}
;
/** * @return date must in this value array */
String
[
]
value
(
)
;
/** * Defines several {@link EnumString} annotations on the same element。 * * @see EnumString */
@Target
(
{
METHOD
,
FIELD
,
ANNOTATION_TYPE
,
CONSTRUCTOR
,
PARAMETER
,
TYPE_USE
}
)
@Retention
(
RUNTIME
)
@Documented
@interface
List
{
EnumString
[
]
value
(
)
;
}
}
第二步,自定義校驗邏輯
public
class
EnumStringValidator
implements
ConstraintValidator
<
EnumString
,
String
>
{
private
List
<
String
>
enumStringList
;
@Override
public
void
initialize
(
EnumString
constraintAnnotation
)
{
enumStringList
=
Arrays
。
asList
(
constraintAnnotation
。
value
(
)
)
;
}
@Override
public
boolean
isValid
(
String
value
,
ConstraintValidatorContext
context
)
{
if
(
value
==
null
)
{
return
true
;
}
return
enumStringList
。
contains
(
value
)
;
}
}
第三步,在欄位上增加註解
@ApiModelProperty
(
value
=
“性別”
)
@EnumString
(
value
=
{
“F”
,
“M”
}
,
message
=
“性別只允許為F或M”
)
private
String
sex
;
第四步,體驗效果
POST http
:
/
/
localhost
:
8080
/
valid
/
test2
Content
-
Type
:
application
/
x
-
www
-
form
-
urlencodedid
=
1
&
name
=
javadaily
&
level
=
12
&
=
476938977
@qq。com
&
appId
=
ab1cdddd
&
sex
=
N
{
“status”
:
400
,
“message”
:
“性別只允許為F或M”
,
“data”
:
null
,
“timestamp”
:
1628435243723
}
分組校驗
一個VO物件在新增的時候某些欄位為必填,在更新的時候又非必填。如上面的
ValidVO
中 id 和 appId 屬性在新增操作時都是
非必填
,而在編輯操作時都為
必填
,name在新增操作時為
必填
,面對這種場景你會怎麼處理呢?
在實際開發中我見到很多同學都是建立兩個VO物件,
ValidCreateVO
,
ValidEditVO
來處理這種場景,這樣確實也能實現效果,但是會造成類膨脹,而且極其容易被開發老鳥們嘲笑。
其實
Validator
校驗框架已經考慮到了這種場景並且提供瞭解決方案,就是
分組校驗
,只不過很多同學不知道而已。要使用分組校驗,只需要三個步驟:
第一步:定義分組介面
public
interface
ValidGroup
extends
Default
{
interface
Crud
extends
ValidGroup
{
interface
Create
extends
Crud
{
}
interface
Update
extends
Crud
{
}
interface
Query
extends
Crud
{
}
interface
Delete
extends
Crud
{
}
}
}
這裡我們定義一個分組介面ValidGroup讓其繼承
javax。validation。groups。Default
,再在分組介面中定義出多個不同的操作型別,Create,Update,Query,Delete。至於為什麼需要繼承Default我們稍後再說。
第二步,在模型中給引數分配分組
@Data
@ApiModel
(
value
=
“引數校驗類”
)
public
class
ValidVO
{
@ApiModelProperty
(
“ID”
)
@Null
(
groups
=
ValidGroup
。
Crud
。
Create
。
class
)
@NotNull
(
groups
=
ValidGroup
。
Crud
。
Update
。
class
,
message
=
“應用ID不能為空”
)
private
String
id
;
@Null
(
groups
=
ValidGroup
。
Crud
。
Create
。
class
)
@NotNull
(
groups
=
ValidGroup
。
Crud
。
Update
。
class
,
message
=
“應用ID不能為空”
)
@ApiModelProperty
(
value
=
“應用ID”
,
example
=
“cloud”
)
private
String
appId
;
@ApiModelProperty
(
value
=
“名字”
)
@NotBlank
(
groups
=
ValidGroup
。
Crud
。
Create
。
class
,
message
=
“名字為必填項”
)
private
String
name
;
@ApiModelProperty
(
value
=
“郵箱”
)
(
message
=
“請填寫正取的郵箱地址”
)
privte
String
;
。
。
。
}
給引數指定分組,對於未指定分組的則使用的是預設分組。
第三步,給需要引數校驗的方法指定分組
@RestController
@Api
(
“引數校驗”
)
@Slf4j
@Validated
public
class
ValidController
{
@ApiOperation
(
“新增”
)
@PostMapping
(
value
=
“/valid/add”
)
public
String
add
(
@Validated
(
value
=
ValidGroup
。
Crud
。
Create
。
class
)
ValidVO
validVO
)
{
log
。
info
(
“validEntity is {}”
,
validVO
)
;
return
“test3 valid success”
;
}
@ApiOperation
(
“更新”
)
@PostMapping
(
value
=
“/valid/update”
)
public
String
update
(
@Validated
(
value
=
ValidGroup
。
Crud
。
Update
。
class
)
ValidVO
validVO
)
{
log
。
info
(
“validEntity is {}”
,
validVO
)
;
return
“test4 valid success”
;
}
}
這裡我們透過
value
屬性給
add()
和
update()
方法分別指定Create和Update分組。
第四步,體驗效果
POST http
:
/
/
localhost
:
8080
/
valid
/
add
Content
-
Type
:
application
/
x
-
www
-
form
-
urlencodedname
=
javadaily
&
level
=
12
&
=
476938977
@qq。com
&
sex
=
F
在Create時我們沒有傳遞id和appId引數,校驗透過。
當我們使用同樣的引數呼叫update方法時則提示引數校驗錯誤。
{
“status”
:
400
,
“message”
:
“ID不能為空; 應用ID不能為空”
,
“data”
:
null
,
“timestamp”
:
1628492514313
}
由於email屬於預設分組,而我們的分組介面
ValidGroup
已經繼承了
Default
分組,所以也是可以對email欄位作引數校驗的。如:
POST http
:
/
/
localhost
:
8080
/
valid
/
add
Content
-
Type
:
application
/
x
-
www
-
form
-
urlencodedname
=
javadaily
&
level
=
12
&
=
476938977
&
sex
=
F
{
“status”
:
400
,
“message”
:
“請填寫正取的郵箱地址”
,
“data”
:
null
,
“timestamp”
:
1628492637305
}
當然如果你的ValidGroup沒有繼承Default分組,那在程式碼屬性上就需要加上
@Validated(value = {ValidGroup。Crud。Create。class, Default。class}
才能讓
欄位的校驗生效。
小結
引數校驗在實際開發中使用頻率非常高,但是很多同學還只是停留在簡單的使用上,像分組校驗,自定義引數校驗這2個高階技巧基本沒怎麼用過,經常出現譬如建立多個VO用於接受Create,Update場景的情況,很容易被老鳥被所鄙視嘲笑,希望大家好好掌握。
最後,我是飄渺Jam,一名寫程式碼的架構師,做架構的程式設計師,期待您的轉發與關注,當然也可以新增我的個人微信
jianzh5
,咱們一起聊技術!
github地址:https://github。com/jianzh5/cloud-blog/
,https://blog。csdn。net/jianzhang11/article/details/119632467