SpringBoot如何使用validator框架優(yōu)雅地校驗(yàn)參數(shù)
1、為什么要校驗(yàn)參數(shù)?
在日常的開發(fā)中,為了防止非法參數(shù)對(duì)業(yè)務(wù)造成影響,需要對(duì)接口的參數(shù)進(jìn)行校驗(yàn),以便正確性地入庫(kù)。
例如:登錄時(shí),就需要判斷用戶名、密碼等信息是否為空。雖然前端也有校驗(yàn),但為了接口的安全性,后端接口還是有必要進(jìn)行參數(shù)校驗(yàn)的。
同時(shí),為了校驗(yàn)參數(shù)更加優(yōu)雅,這里就介紹了 Spring Validation 方式。
- Java API 規(guī)范(JSR303:JAVA EE 6 中的一項(xiàng)子規(guī)范,叫做 Bean Validation)定義了 Bean 校驗(yàn)的標(biāo)準(zhǔn) validation-api,但沒有提供實(shí)現(xiàn)。
- hibernate validation 是對(duì)這個(gè)規(guī)范的實(shí)現(xiàn),并增加了校驗(yàn)注解。如:@Email、@Length。
Spring Validation 是對(duì) hibernate validation 的二次封裝,用于支持 spring mvc 參數(shù)自動(dòng)校驗(yàn)。
2、引入依賴
如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 會(huì)自動(dòng)傳入 hibernate-validator 依賴。如果 spring-boot 版本大于等于 2.3.x,則需要手動(dòng)引入依賴。
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>8.0.0.Final</version>
</dependency>對(duì)于 web 服務(wù)來說,為防止非法參數(shù)對(duì)業(yè)務(wù)造成影響,在 Controller 層一定要做參數(shù)校驗(yàn)的!大部分情況下,請(qǐng)求參數(shù)分為如下兩種形式:
- POST、PUT 請(qǐng)求,使用
@requestBody接收參數(shù) - GET 請(qǐng)求,使用
@requestParam、@PathVariable接收參數(shù)
3、@requestBody 參數(shù)校驗(yàn)
對(duì)于 POST、PUT 請(qǐng)求,后端一般會(huì)使用 @requestBody + 對(duì)象 接收參數(shù)。此時(shí),只需要給對(duì)象添加 @Validated 或 @Valid 注解,即可輕松實(shí)現(xiàn)自動(dòng)校驗(yàn)參數(shù)。如果校驗(yàn)失敗,會(huì)拋出 MethodArgumentNotValidException 異常。
UserVo :添加校驗(yàn)注解
@Data
public class UserVo {
private Long id;
@NotNull
@Length(min = 2, max = 10)
private String userName;
@NotNull
@Length(min = 6, max = 20)
private String account;
@NotNull
@Length(min = 6, max = 20)
private String password;
}UserController :
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid UserVo userVo) {
return "addUser";
}
}或者使用 @Validated 注解:
@PostMapping("/addUser")
public String addUser(@RequestBody @Validated UserVo userVo) {
return "addUser";
}4、@requestParam、@PathVariable 參數(shù)校驗(yàn)
GET 請(qǐng)求一般會(huì)使用 @requestParam、@PathVariable 注解接收參數(shù)。如果參數(shù)比較多(比如超過 5 個(gè)),還是推薦使用對(duì)象接收。否則,推薦將一個(gè)個(gè)參數(shù)平鋪到方法入?yún)⒅小?/p>
在這種情況下,必須在 Controller 類上標(biāo)注 @Validated 注解,并在入?yún)⑸下暶骷s束注解(如:@Min )。如果校驗(yàn)失敗,會(huì)拋出 ConstraintViolationException 異常
@RestController
@RequestMapping("/user")
@Validated
public class UserController {
@GetMapping("/getUser")
public String getUser(@Min(1L) Long id) {
return "getUser";
}
}5、統(tǒng)一異常處理
如果校驗(yàn)失敗,會(huì)拋出 MethodArgumentNotValidException 或者 ConstraintViolationException 異常。在實(shí)際項(xiàng)目開發(fā)中,通常會(huì)用統(tǒng)一異常處理來返回一個(gè)更友好的提示
@RestControllerAdvice
public class ExceptionControllerAdvice {
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
public String handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder sb = new StringBuilder("校驗(yàn)失敗:");
for (FieldError fieldError : bindingResult.getFieldErrors()) {
sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
}
String msg = sb.toString();
return "參數(shù)校驗(yàn)失敗" + msg;
}
@ExceptionHandler({ConstraintViolationException.class})
public String handleConstraintViolationException(ConstraintViolationException ex) {
return "參數(shù)校驗(yàn)失敗" + ex;
}
}6、分組校驗(yàn)
在實(shí)際項(xiàng)目中,可能多個(gè)方法需要使用同一個(gè)類對(duì)象來接收參數(shù),而不同方法的校驗(yàn)規(guī)則很可能是不一樣的。這個(gè)時(shí)候,簡(jiǎn)單地在類的字段上加約束注解無(wú)法解決這個(gè)問題。因此,spring-validation 支持了分組校驗(yàn)的功能,專門用來解決這類問題。
如:保存 User 的時(shí)候,userId 是可空的,但是更新 User 的時(shí)候,userId 的值必須 >= 1L;其它字段的校驗(yàn)規(guī)則在兩種情況下一樣。這個(gè)時(shí)候使用分組校驗(yàn)的代碼示例如下:
約束注解上聲明適用的分組信息 groups
6.1、定義分組接口
public interface ValidGroup extends Default {
// 添加操作
interface Save extends ValidGroup {}
// 更新操作
interface Update extends ValidGroup {}
// ...
}為什么要繼承 Default ?下文有。
6.2、給需要校驗(yàn)的字段分配分組
@Data
public class UserVo {
@Null(groups = ValidGroup.Save.class, message = "id要為空")
@NotNull(groups = ValidGroup.Update.class, message = "id不能為空")
private Long id;
@NotBlank(groups = ValidGroup.Save.class, message = "用戶名不能為空")
@Length(min = 2, max = 10)
private String userName;
@Email
@NotNull
private String email;
}根據(jù)校驗(yàn)字段看:
- id:分配分組:Save、Update。添加時(shí),一定為 null;更新時(shí),一定不為 null
- userName:分配分組:Save。添加時(shí),一定不能為空
- email:分配分組:無(wú)。即:使用默認(rèn)的分組
6.3、給需要校驗(yàn)的參數(shù)指定分組
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/addUser")
public String addUser(@RequestBody @Validated(ValidGroup.Save.class) UserVo userVo) {
return "addUser";
}
@PostMapping("/updateUser")
public String updateUser(@RequestBody @Validated(ValidGroup.Update.class) UserVo userVo) {
return "updateUser";
}
}測(cè)試校驗(yàn)。
6.4、默認(rèn)分組
如果 ValidGroup 接口 不繼承 Default 接口,那么,將無(wú)法校驗(yàn) email 字段(未分配分組);
繼承后,ValidGroup 就屬于 Default 類型,即:默認(rèn)分組/所以,可以對(duì) email 校驗(yàn)
7、嵌套校驗(yàn)
必須要用 @Valid 注解
@Data
public class UserVo {
@NotNull(groups = {ValidGroup.Save.class, ValidGroup.Update.class})
@Valid
private Address address;
}8、自定義校驗(yàn)
8.1、案例一、自定義校驗(yàn) 加密id
假設(shè)我們自定義加密 id(由數(shù)字或者 a-f 的字母組成,32-256 長(zhǎng)度)校驗(yàn),主要分為兩步:
8.1.1、自定義約束注解
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class}) // 自定義驗(yàn)證器
public @interface EncryptId {
// 默認(rèn)錯(cuò)誤消息
String message() default "加密id格式錯(cuò)誤";
// 分組
Class<?>[] groups() default {};
// 負(fù)載
Class<? extends Payload>[] payload() default {};
}8.1.2、編寫約束校驗(yàn)器
public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {
private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (value != null) {
Matcher matcher = PATTERN.matcher(value);
return matcher.find();
}
return true;
}
}8.1.3、使用
@Data
public class UserVo {
@EncryptId
private String id;
}8.2、案例二、自定義校驗(yàn) 性別只允許兩個(gè)值
UserVo 類中的 sex 性別屬性,只允許前端傳遞傳 M,F(xiàn) 這2個(gè)枚舉值,如何實(shí)現(xiàn)呢?
8.2.1、自定義約束注解
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {SexValidator.class})
public @interface SexValid {
// 默認(rèn)錯(cuò)誤消息
String message() default "value not in enum values";
// 分組
Class<?>[] groups() default {};
// 負(fù)載
Class<? extends Payload>[] payload() default {};
String[] value();
}8.2.2、編寫約束校驗(yàn)器
public class SexValidator implements ConstraintValidator<SexValid, String> {
private List<String> sexs;
@Override
public void initialize(SexValid constraintAnnotation) {
sexs = Arrays.asList(constraintAnnotation.value());
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (StringUtils.isEmpty(value)) {
return true;
}
return sexs.contains(value);
}
}8.2.3、使用
@Data
public class UserVo {
@SexValid(value = {"F", "M"}, message = "性別只允許為F或M")
private String sex;
}
```### 8.2.4、測(cè)試
```java
@GetMapping("/get")
private String get(@RequestBody @Validated UserVo userVo) {
return "get";
}9、實(shí)現(xiàn)校驗(yàn)業(yè)務(wù)規(guī)則
業(yè)務(wù)規(guī)則校驗(yàn) 指 接口需要滿足某些特定的業(yè)務(wù)規(guī)則。舉個(gè)例子:業(yè)務(wù)系統(tǒng)的用戶需要保證其唯一性,用戶屬性不能與其他用戶產(chǎn)生沖突,不允許與數(shù)據(jù)庫(kù)中任何已有用戶的用戶名稱、手機(jī)號(hào)碼、郵箱產(chǎn)生重復(fù)。 這就要求在創(chuàng)建用戶時(shí)需要校驗(yàn)用戶名稱、手機(jī)號(hào)碼、郵箱是否被注冊(cè);編輯用戶時(shí)不能將信息修改成已有用戶的屬性。
最優(yōu)雅的實(shí)現(xiàn)方法應(yīng)該是參考 Bean Validation 的標(biāo)準(zhǔn)方式,借助自定義校驗(yàn)注解完成業(yè)務(wù)規(guī)則校驗(yàn)。
9.1、自定義約束注解
首先我們需要?jiǎng)?chuàng)建兩個(gè)自定義注解,用于業(yè)務(wù)規(guī)則校驗(yàn):
UniqueUser:表示一個(gè)用戶是唯一的,唯一性包含:用戶名,手機(jī)號(hào)碼、郵箱NotConflictUser:表示一個(gè)用戶的信息是無(wú)沖突的,無(wú)沖突是指該用戶的敏感信息與其他用戶不重合
@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidator.UniqueUserValidator.class)
public @interface UniqueUser {
String message() default "用戶名、手機(jī)號(hào)碼、郵箱不允許與現(xiàn)存用戶重復(fù)";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidator.NotConflictUserValidator.class)
public @interface NotConflictUser {
String message() default "用戶名稱、郵箱、手機(jī)號(hào)碼與現(xiàn)存用戶產(chǎn)生重復(fù)";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}9.2、編寫約束校驗(yàn)器
想讓自定義驗(yàn)證注解生效,需要實(shí)現(xiàn) ConstraintValidator 接口。接口的第一個(gè)參數(shù)是 自定義注解類型,第二個(gè)參數(shù)是 被注解字段的類,因?yàn)樾枰r?yàn)多個(gè)參數(shù),我們直接傳入用戶對(duì)象。 需要提到的一點(diǎn)是 ConstraintValidator 接口的實(shí)現(xiàn)類無(wú)需添加 @Component 它在啟動(dòng)的時(shí)候就已經(jīng)被加載到容器中了。
public class UserValidator<T extends Annotation> implements ConstraintValidator<T, UserVo> {
protected Predicate<UserVo> predicate = c -> true;
@Override
public boolean isValid(UserVo userVo, ConstraintValidatorContext constraintValidatorContext) {
return predicate.test(userVo);
}
public static class UniqueUserValidator extends UserValidator<UniqueUser>{
@Override
public void initialize(UniqueUser uniqueUser) {
UserDao userDao = ApplicationContextHolder.getBean(UserDao.class);
predicate = c -> !userDao.existsByUserNameOrEmailOrTelphone(c.getUserName(),c.getEmail(),c.getTelphone());
}
}
public static class NotConflictUserValidator extends UserValidator<NotConflictUser>{
@Override
public void initialize(NotConflictUser notConflictUser) {
predicate = c -> {
UserDao userDao = ApplicationContextHolder.getBean(UserDao.class);
Collection<UserVo> collection = userDao.findByUserNameOrEmailOrTelphone(c.getUserName(), c.getEmail(), c.getTelphone());
// 將用戶名、郵件、電話改成與現(xiàn)有完全不重復(fù)的,或者只與自己重復(fù)的,就不算沖突
return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId()));
};
}
}
}@Component
public class ApplicationContextHolder implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static ApplicationContext getContext() {
return context;
}
public static Object getBean(String name) {
return context != null ? context.getBean(name) : null;
}
public static <T> T getBean(Class<T> clz) {
return context != null ? context.getBean(clz) : null;
}
public static <T> T getBean(String name, Class<T> clz) {
return context != null ? context.getBean(name, clz) : null;
}
public static void addApplicationListenerBean(String listenerBeanName) {
if (context != null) {
ApplicationEventMulticaster applicationEventMulticaster = (ApplicationEventMulticaster)context.getBean(ApplicationEventMulticaster.class);
applicationEventMulticaster.addApplicationListenerBean(listenerBeanName);
}
}
}9.3、測(cè)試
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/addUser")
public String addUser(@RequestBody @UniqueUser UserVo userVo) {
return "addUser";
}
@PostMapping("/updateUser")
public String updateUser(@RequestBody @NotConflictUser UserVo userVo) {
return "updateUser";
}
}10、@Valid 和 @Validated 的區(qū)別
區(qū)別如下:

11、常用注解
Bean Validation 內(nèi)嵌的注解很多,基本實(shí)際開發(fā)中已經(jīng)夠用了,注解如下:
| 注解 | 詳細(xì)信息 |
|---|---|
| @Null | 任意類型。被注釋的元素必須為 null |
| @NotNull | 任意類型。被注釋的元素不為 null |
| @Min(value) | 數(shù)值類型(double、float 會(huì)有精度丟失)。其值必須大于等于指定的最小值 |
| @Max(value) | 數(shù)值類型(double、float 會(huì)有精度丟失)。其值必須小于等于指定的最大值 |
| @DecimalMin(value) | 數(shù)值類型(double、float 會(huì)有精度丟失)。其值必須大于等于指定的最小值 |
| @DecimalMax(value) | 數(shù)值類型(double、float 會(huì)有精度丟失)。其值必須小于等于指定的最大值 |
| @Size(max, min) | 字符串、集合、Map、數(shù)組類型。被注釋的元素的大?。ㄩL(zhǎng)度)必須在指定的范圍內(nèi) |
| @Digits (integer, fraction) | 數(shù)值類型、數(shù)值型字符串類型。其值必須在可接受的范圍內(nèi)。 integer:整數(shù)精度;fraction:小數(shù)精度 |
| @Past | 日期類型。被注釋的元素必須是一個(gè)過去的日期 |
| @Future | 日期類型。被注釋的元素必須是一個(gè)將來的日期 |
| @Pattern(value) | 字符串類型。被注釋的元素必須符合指定的正則表達(dá)式 |
Hibernate Validator 在原有的基礎(chǔ)上也內(nèi)嵌了幾個(gè)注解,如下:
| 注解 | 詳細(xì)信息 |
|---|---|
| 字符串類型。被注釋的元素必須是電子郵箱地址 | |
| @Length | 字符串類型。被注釋的字符串的長(zhǎng)度必須在指定的范圍內(nèi) |
| @NotEmpty | 字符串、集合、Map、數(shù)組類型。 被注釋的元素的長(zhǎng)度必須非空 |
| @Range | 數(shù)值類型、字符串類型。 被注釋的元素必須在合適的范圍內(nèi) |
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
IDEA2020.1啟動(dòng)SpringBoot項(xiàng)目出現(xiàn)java程序包:xxx不存在
這篇文章主要介紹了IDEA2020.1啟動(dòng)SpringBoot項(xiàng)目出現(xiàn)java程序包:xxx不存在,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
java實(shí)現(xiàn)延遲/超時(shí)/定時(shí)問題
這篇文章主要介紹了java實(shí)現(xiàn)延遲/超時(shí)/定時(shí)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-04-04
spring學(xué)習(xí)教程之@ModelAttribute注解運(yùn)用詳解
這篇文章主要給大家介紹了關(guān)于spring學(xué)習(xí)教程之@ModelAttribute注釋運(yùn)用的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-06-06
JPA?通過Specification如何實(shí)現(xiàn)復(fù)雜查詢
這篇文章主要介紹了JPA?通過Specification如何實(shí)現(xiàn)復(fù)雜查詢,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11
java使用正則表達(dá)校驗(yàn)手機(jī)號(hào)碼示例(手機(jī)號(hào)碼正則)
這篇文章主要介紹了java使用正則表達(dá)校驗(yàn)手機(jī)號(hào)碼示例,可校驗(yàn)三個(gè)號(hào)碼段:13*、15*、18*,大家根據(jù)自己的需要增加自己的號(hào)碼段就可以了2014-03-03
Spring事件監(jiān)聽機(jī)制之@EventListener實(shí)現(xiàn)方式詳解
這篇文章主要介紹了Spring事件監(jiān)聽機(jī)制之@EventListener實(shí)現(xiàn)方式詳解,ApplicationContext的refresh方法還是初始化了SimpleApplicationEventMulticaster,發(fā)送事件式還是先獲取ResolvableType類型,再獲取發(fā)送監(jiān)聽列表,需要的朋友可以參考下2023-12-12
關(guān)于Nacos和Eureka的區(qū)別及說明
這篇文章主要介紹了關(guān)于Nacos和Eureka的區(qū)別及說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06

