對(duì)于寫(xiě)Java的同學(xué)來(lái)說(shuō),參數(shù)校驗(yàn)是繁瑣且重復(fù)性很高的代碼恢氯。很多時(shí)候我們的業(yè)務(wù)代碼編寫(xiě)之前先要進(jìn)行很多的參數(shù)校驗(yàn)邓馒,浪費(fèi)了大量的時(shí)間和精力墙杯。而java中其實(shí)已經(jīng)內(nèi)置了參數(shù)校驗(yàn)的工具,本篇文章主要介紹如何使用Javax.validation來(lái)進(jìn)行參數(shù)校驗(yàn)丑搔。
@validated注解
@validated
是一套幫助我們繼續(xù)對(duì)傳輸?shù)膮?shù)進(jìn)行數(shù)據(jù)校驗(yàn)的注解厦瓢,通過(guò)配置Validation可以很輕松的完成對(duì)數(shù)據(jù)的約束∑≡拢看到一下注解的源碼煮仇,我們可以看到@Validated
注解可以作用在類(lèi)、方法和參數(shù)上谎仲。
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
Class<?>[] value() default {};
}
廢話(huà)不多說(shuō)浙垫,我們直接舉個(gè)例子來(lái)看看@validated
到底好不好用。實(shí)際項(xiàng)目中很常見(jiàn)的應(yīng)用是分頁(yè)查詢(xún)的接口郑诺,通常分頁(yè)查詢(xún)至少需要當(dāng)前頁(yè)和頁(yè)大小這兩個(gè)字段夹姥。通常我們會(huì)把分頁(yè)請(qǐng)求需要的參數(shù)封裝成一個(gè)PageQuery。一個(gè)常見(jiàn)的分頁(yè)參數(shù)類(lèi)辙诞,在很多接口中需要使用辙售。我們就可以給這樣的參數(shù)加上@Validated
注解。表示此類(lèi)開(kāi)啟參數(shù)校驗(yàn)
public Result<Page<Map<String, Object>>> getPage(@Validated PageQuery pageQuery) {
// 設(shè)置頁(yè)大小倘要,當(dāng)前頁(yè)
Page<Map<String, Object>> page = new Page<>(pageQuery.getCurrentPage(), pageQuery.getPageSize());
page.setRecords(service.findPage(page, pageQuery));
return this.responseBody(Result.ResponseEnum.GET_SUCCESS, page);
}
在類(lèi)的字段上圾亏,我們定義校驗(yàn)的規(guī)則和返回的錯(cuò)誤提示。@validated中所有的校驗(yàn)注解封拧,可以參考下面的表格志鹃。
限制 | 說(shuō)明 |
---|---|
@Null | 限制只能為null |
@NotNull | 限制必須不為null |
@AssertFalse | 限制必須為false |
@AssertTrue | 限制必須為true |
@DecimalMax(value) | 限制必須為一個(gè)不大于指定值的數(shù)字 |
@DecimalMin(value) | 限制必須為一個(gè)不小于指定值的數(shù)字 |
@Digits(integer,fraction) | 限制必須為一個(gè)小數(shù),且整數(shù)部分的位數(shù)不能超過(guò)integer泽西,小數(shù)部分的位數(shù)不能超過(guò)fraction |
@Max(value) | 限制必須為一個(gè)不大于指定值的數(shù)字 |
@Min(value) | 限制必須為一個(gè)不小于指定值的數(shù)字 |
@Past | 限制必須是一個(gè)過(guò)去的日期 |
@Future | 限制必須是一個(gè)將來(lái)的日期 |
@Pattern(value) | 限制必須符合指定的正則表達(dá)式 |
@Size(max,min) | 限制字符長(zhǎng)度必須在min到max之間 |
@NotEmpty | 驗(yàn)證注解的元素值不為null且不為空(字符串長(zhǎng)度不為0曹铃、集合大小不為0) |
@NotBlank | 驗(yàn)證注解的元素值不為空(不為null、去除首位空格后長(zhǎng)度為0)捧杉,不同于@NotEmpty陕见,@NotBlank只應(yīng)用于字符串且在比較時(shí)會(huì)去除字符串的空格 |
驗(yàn)證注解的元素值是Email秘血,也可以通過(guò)正則表達(dá)式和flag指定自定義的email格式 |
對(duì)于頁(yè)大小,我們限制為非空且不能小于1评甜;對(duì)于當(dāng)前頁(yè)我們限制為非空灰粮。
public class PageQuery implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 頁(yè)大小
*/
@NotNull(message = "頁(yè)大小不能為空")
@Min(message = "頁(yè)大小不能小于1", value = 1)
Integer pageSize;
/**
* 當(dāng)前頁(yè)
*/
@NotNull(message = "當(dāng)前頁(yè)不能為空")
Integer currentPage;
我們用一個(gè)明顯校驗(yàn)不通過(guò)的參數(shù)來(lái)請(qǐng)求下這個(gè)接口,看看會(huì)返回什么忍坷。我在請(qǐng)求參數(shù)中不傳遞pageSize
參數(shù)粘舟,然后發(fā)送請(qǐng)求。
{
"timestamp": "2021-12-16T03:09:36.238+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotNull.pageQuery.currentPage",
"NotNull.currentPage",
"NotNull.java.lang.Integer",
"NotNull"
],
"arguments": [
{
"codes": [
"pageQuery.currentPage",
"currentPage"
],
"arguments": null,
"defaultMessage": "currentPage",
"code": "currentPage"
}
],
"defaultMessage": "當(dāng)前頁(yè)不能為空",
"objectName": "pageQuery",
"field": "currentPage",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
},
{
"codes": [
"NotNull.pageQuery.env",
"NotNull.env",
"NotNull.java.lang.String",
"NotNull"
],
"arguments": [
{
"codes": [
"pageQuery.env",
"env"
],
"arguments": null,
"defaultMessage": "env",
"code": "env"
}
],
"defaultMessage": "設(shè)備所屬環(huán)境信息不能為空",
"objectName": "pageQuery",
"field": "env",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
},
{
"codes": [
"NotNull.pageQuery.pageSize",
"NotNull.pageSize",
"NotNull.java.lang.Integer",
"NotNull"
],
"arguments": [
{
"codes": [
"pageQuery.pageSize",
"pageSize"
],
"arguments": null,
"defaultMessage": "pageSize",
"code": "pageSize"
}
],
"defaultMessage": "頁(yè)大小不能為空",
"objectName": "pageQuery",
"field": "pageSize",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
}
],
"message": "Validation failed for object='pageQuery'. Error count: 3",
"path": "/localLoadBalance/approval/getApproval"
}
可以看到佩研,返回的結(jié)果中包含了我們之前預(yù)設(shè)的校驗(yàn)提示和內(nèi)容柑肴。不過(guò)到這里我們的任務(wù)還沒(méi)有結(jié)束,實(shí)際項(xiàng)目中旬薯,我們不允許接口返回這樣的類(lèi)型晰骑。大多數(shù)情況下,我們希望接口的返回結(jié)果有通用的模板格式绊序。而上面那樣的返回方式需要前端做大量的解析硕舆,而且也不符合后端接口的規(guī)范。因此我們希望能有一個(gè)全局的處理器政模,來(lái)解析@validated
拋出的異常岗宣。
使用全局異常處理類(lèi)來(lái)進(jìn)行統(tǒng)一的異常處理
在Spring boot項(xiàng)目中,我們可以使用@ControllerAdvice
注解來(lái)進(jìn)行全局的異常處理淋样,當(dāng)然@ControllerAdvice
的用處不止是異常處理耗式,還可以實(shí)現(xiàn)統(tǒng)一的參數(shù)綁定和數(shù)據(jù)的預(yù)處理。詳情可以參考# SpringMVC 中 @ControllerAdvice 注解的三種使用場(chǎng)景趁猴!
首先我們新增一個(gè)handler刊咳,當(dāng)然你也可以指定一個(gè)包來(lái)掃描包下的所有controller。如@ControllerAdvice(basePackages="com.test.controller")
儡司,然后我們使用@ExceptionHandler
來(lái)進(jìn)行異常的處理娱挨。
此處需要說(shuō)明的是,我們是針對(duì)@validated
進(jìn)行的異常的處理捕犬,因此我們希望異常校驗(yàn)類(lèi)只攔截@validated
注解拋出的異常跷坝。所以在本方法中,我只讓@ExceptionHandler
攔截了BindException碉碉。其次柴钻,針對(duì)參數(shù)校驗(yàn)出現(xiàn)多個(gè)異常的情況,我們把多個(gè)錯(cuò)誤信息通過(guò)逗號(hào)分隔開(kāi)來(lái)垢粮。
@ControllerAdvice
public class GlobalHandler {
private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class);
/**
* 全局處理所有使用了@validation校驗(yàn)參數(shù)的controller
* @param e 捕獲到validation拋出異常
* @return 返回參數(shù)中所有的校驗(yàn)錯(cuò)誤贴届,以,分隔不用的錯(cuò)誤信息
*/
@ResponseBody
@ExceptionHandler(BindException.class)
public Result<Void> exceptionHandler(BindException e) {
String errors=e.getBindingResult().getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(","));
logger.error("Request params error,caught by global exception handler,{}",errors);
return Result.<Void>toBuilder()
.code(0)
.msg(errors)
.builder();
}
}
再次準(zhǔn)備一個(gè)含有錯(cuò)誤參數(shù)的請(qǐng)求,這次我們不傳currentPage,pageSize的值為-1毫蚓。我們看看會(huì)返回什么占键。
{
"code": 0,
"msg": "當(dāng)前頁(yè)不能為空,頁(yè)大小不能小于1",
"data": null
}
可以看到,返回的結(jié)果符合我們的預(yù)期元潘。
在獲取錯(cuò)誤信息的地方我們看到有針對(duì)BindException的異常信息解析畔乙,涉及了多個(gè).
操作。有經(jīng)驗(yàn)的老鳥(niǎo)可能覺(jué)得這里容易出現(xiàn)空指針異常翩概。不過(guò)此處你大可放心啸澡,BindException中的BindingResult是絕對(duì)不會(huì)為null
的。我們看下源碼氮帐,可以看到內(nèi)部是用斷言來(lái)保證結(jié)果不為空的。
/**
* 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;
}
參考文章
SpringMVC 中 @ControllerAdvice 注解的三種使用場(chǎng)景洛姑! - 江南一點(diǎn)雨 - 博客園 (cnblogs.com)