SpringBoot如何使用validator框架優(yōu)雅地校驗(yàn)參數(shù)
1、為什么要校驗(yàn)參數(shù)?
在日常的開發(fā)中,為了防止非法參數(shù)對業(yè)務(wù)造成影響,需要對接口的參數(shù)進(jìn)行校驗(yàn),以便正確性地入庫。
例如:登錄時,就需要判斷用戶名、密碼等信息是否為空。雖然前端也有校驗(yàn),但為了接口的安全性,后端接口還是有必要進(jìn)行參數(shù)校驗(yàn)的。
同時,為了校驗(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 是對這個規(guī)范的實(shí)現(xiàn),并增加了校驗(yàn)注解。如:@Email、@Length。
Spring Validation 是對 hibernate validation 的二次封裝,用于支持 spring mvc 參數(shù)自動校驗(yàn)。
2、引入依賴
如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 會自動傳入 hibernate-validator 依賴。如果 spring-boot 版本大于等于 2.3.x,則需要手動引入依賴。
<dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <version>8.0.0.Final</version> </dependency>
對于 web 服務(wù)來說,為防止非法參數(shù)對業(yè)務(wù)造成影響,在 Controller 層一定要做參數(shù)校驗(yàn)的!大部分情況下,請求參數(shù)分為如下兩種形式:
- POST、PUT 請求,使用
@requestBody
接收參數(shù) - GET 請求,使用
@requestParam、@PathVariable
接收參數(shù)
3、@requestBody 參數(shù)校驗(yàn)
對于 POST、PUT 請求,后端一般會使用 @requestBody + 對象
接收參數(shù)。此時,只需要給對象添加 @Validated 或 @Valid
注解,即可輕松實(shí)現(xiàn)自動校驗(yàn)參數(shù)。如果校驗(yàn)失敗,會拋出 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 請求一般會使用 @requestParam、@PathVariable
注解接收參數(shù)。如果參數(shù)比較多(比如超過 5 個),還是推薦使用對象接收。否則,推薦將一個個參數(shù)平鋪到方法入?yún)⒅小?/p>
在這種情況下,必須在 Controller 類上標(biāo)注 @Validated
注解,并在入?yún)⑸下暶骷s束注解(如:@Min
)。如果校驗(yàn)失敗,會拋出 ConstraintViolationException
異常
@RestController @RequestMapping("/user") @Validated public class UserController { @GetMapping("/getUser") public String getUser(@Min(1L) Long id) { return "getUser"; } }
5、統(tǒng)一異常處理
如果校驗(yàn)失敗,會拋出 MethodArgumentNotValidException
或者 ConstraintViolationException
異常。在實(shí)際項(xiàng)目開發(fā)中,通常會用統(tǒng)一異常處理來返回一個更友好的提示
@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)目中,可能多個方法需要使用同一個類對象來接收參數(shù),而不同方法的校驗(yàn)規(guī)則很可能是不一樣的。這個時候,簡單地在類的字段上加約束注解無法解決這個問題。因此,spring-validation
支持了分組校驗(yàn)的功能,專門用來解決這類問題。
如:保存 User 的時候,userId 是可空的,但是更新 User 的時候,userId 的值必須 >= 1L;其它字段的校驗(yàn)規(guī)則在兩種情況下一樣。這個時候使用分組校驗(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。添加時,一定為 null;更新時,一定不為 null
- userName:分配分組:Save。添加時,一定不能為空
- email:分配分組:無。即:使用默認(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"; } }
測試校驗(yàn)。
6.4、默認(rèn)分組
如果 ValidGroup
接口 不繼承 Default
接口,那么,將無法校驗(yàn) email
字段(未分配分組);
繼承后,ValidGroup
就屬于 Default
類型,即:默認(rèn)分組/所以,可以對 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 長度)校驗(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)錯誤消息 String message() default "加密id格式錯誤"; // 分組 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) 性別只允許兩個值
UserVo 類中的 sex 性別屬性,只允許前端傳遞傳 M,F(xiàn) 這2個枚舉值,如何實(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)錯誤消息 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、測試 ```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ī)則。舉個例子:業(yè)務(wù)系統(tǒng)的用戶需要保證其唯一性,用戶屬性不能與其他用戶產(chǎn)生沖突,不允許與數(shù)據(jù)庫中任何已有用戶的用戶名稱、手機(jī)號碼、郵箱產(chǎn)生重復(fù)。 這就要求在創(chuàng)建用戶時需要校驗(yàn)用戶名稱、手機(jī)號碼、郵箱是否被注冊;編輯用戶時不能將信息修改成已有用戶的屬性。
最優(yōu)雅的實(shí)現(xiàn)方法應(yīng)該是參考 Bean Validation 的標(biāo)準(zhǔn)方式,借助自定義校驗(yàn)注解完成業(yè)務(wù)規(guī)則校驗(yàn)。
9.1、自定義約束注解
首先我們需要創(chuàng)建兩個自定義注解,用于業(yè)務(wù)規(guī)則校驗(yàn):
UniqueUser
:表示一個用戶是唯一的,唯一性包含:用戶名,手機(jī)號碼、郵箱NotConflictUser
:表示一個用戶的信息是無沖突的,無沖突是指該用戶的敏感信息與其他用戶不重合
@Documented @Retention(RUNTIME) @Target({FIELD, METHOD, PARAMETER, TYPE}) @Constraint(validatedBy = UserValidator.UniqueUserValidator.class) public @interface UniqueUser { String message() default "用戶名、手機(jī)號碼、郵箱不允許與現(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ī)號碼與現(xiàn)存用戶產(chǎn)生重復(fù)"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
9.2、編寫約束校驗(yàn)器
想讓自定義驗(yàn)證注解生效,需要實(shí)現(xiàn) ConstraintValidator
接口。接口的第一個參數(shù)是 自定義注解類型,第二個參數(shù)是 被注解字段的類,因?yàn)樾枰r?yàn)多個參數(shù),我們直接傳入用戶對象。 需要提到的一點(diǎn)是 ConstraintValidator
接口的實(shí)現(xiàn)類無需添加 @Component
它在啟動的時候就已經(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、測試
@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 會有精度丟失)。其值必須大于等于指定的最小值 |
@Max(value) | 數(shù)值類型(double、float 會有精度丟失)。其值必須小于等于指定的最大值 |
@DecimalMin(value) | 數(shù)值類型(double、float 會有精度丟失)。其值必須大于等于指定的最小值 |
@DecimalMax(value) | 數(shù)值類型(double、float 會有精度丟失)。其值必須小于等于指定的最大值 |
@Size(max, min) | 字符串、集合、Map、數(shù)組類型。被注釋的元素的大小(長度)必須在指定的范圍內(nèi) |
@Digits (integer, fraction) | 數(shù)值類型、數(shù)值型字符串類型。其值必須在可接受的范圍內(nèi)。 integer:整數(shù)精度;fraction:小數(shù)精度 |
@Past | 日期類型。被注釋的元素必須是一個過去的日期 |
@Future | 日期類型。被注釋的元素必須是一個將來的日期 |
@Pattern(value) | 字符串類型。被注釋的元素必須符合指定的正則表達(dá)式 |
Hibernate Validator
在原有的基礎(chǔ)上也內(nèi)嵌了幾個注解,如下:
注解 | 詳細(xì)信息 |
---|---|
字符串類型。被注釋的元素必須是電子郵箱地址 | |
@Length | 字符串類型。被注釋的字符串的長度必須在指定的范圍內(nèi) |
@NotEmpty | 字符串、集合、Map、數(shù)組類型。 被注釋的元素的長度必須非空 |
@Range | 數(shù)值類型、字符串類型。 被注釋的元素必須在合適的范圍內(nèi) |
總結(jié)
以上為個人經(jīng)驗(yàn),希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
IDEA2020.1啟動SpringBoot項(xiàng)目出現(xiàn)java程序包:xxx不存在
這篇文章主要介紹了IDEA2020.1啟動SpringBoot項(xiàng)目出現(xiàn)java程序包:xxx不存在,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06spring學(xué)習(xí)教程之@ModelAttribute注解運(yùn)用詳解
這篇文章主要給大家介紹了關(guān)于spring學(xué)習(xí)教程之@ModelAttribute注釋運(yùn)用的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-06-06JPA?通過Specification如何實(shí)現(xiàn)復(fù)雜查詢
這篇文章主要介紹了JPA?通過Specification如何實(shí)現(xiàn)復(fù)雜查詢,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11java使用正則表達(dá)校驗(yàn)手機(jī)號碼示例(手機(jī)號碼正則)
這篇文章主要介紹了java使用正則表達(dá)校驗(yàn)手機(jī)號碼示例,可校驗(yàn)三個號碼段:13*、15*、18*,大家根據(jù)自己的需要增加自己的號碼段就可以了2014-03-03Spring事件監(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ū)別及說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-06-06