SpringBoot通過自定義注解實(shí)現(xiàn)參數(shù)校驗(yàn)
1. 為什么要進(jìn)行參數(shù)校驗(yàn)
在后端進(jìn)行工作時(shí),需要接收前端傳來的數(shù)據(jù)去數(shù)據(jù)庫查詢,但是如果有些數(shù)據(jù)過于離譜,我們就可以直接把它pass掉,不讓這種垃圾數(shù)據(jù)接觸數(shù)據(jù)庫,減小數(shù)據(jù)庫的壓力。
有時(shí)候會(huì)有不安分的人通過一些垃圾數(shù)據(jù)攻擊咱們的程序,讓咱們的服務(wù)器或數(shù)據(jù)庫崩潰,這種攻擊雖然低級(jí)但不得不防,就像QQ進(jìn)行登錄請(qǐng)求時(shí),它們向后端發(fā)送 賬號(hào)=123,密碼=123 的數(shù)據(jù),一秒鐘還發(fā)1w次,這很明顯就是找事的好吧,什么人類的手速能達(dá)到1秒1萬次?
解決方法是:一方面我們可以通過Redis記錄ip/賬號(hào)的方式拒絕一部分請(qǐng)求,例如1s中同一個(gè)ip/賬號(hào)最多請(qǐng)求100次。另一方面就是進(jìn)行數(shù)據(jù)校驗(yàn)pass一部分?jǐn)?shù)據(jù),這100里又多少次是垃圾數(shù)據(jù)。這樣就可以盡量減小服務(wù)器數(shù)據(jù)庫的壓力。
2. 如何實(shí)現(xiàn)參數(shù)校驗(yàn)
實(shí)現(xiàn)參數(shù)校驗(yàn)說實(shí)話方式還挺多,個(gè)人使用過直接在Controller代碼里面寫、AOP+自定義注解、ConstraintValidator。本篇博客講的是ConstraintValidator實(shí)現(xiàn)。
直接在Controller代碼里面寫,說實(shí)話,寫起來是簡單,但是臃腫,耦合性高,最主要是,不夠優(yōu)雅。
AOP實(shí)現(xiàn)有難度,代碼繁瑣,顯得邏輯雜亂。
所以我建議使用ConstraintValidator。
在這里先提供一個(gè)工具類進(jìn)行參數(shù)校驗(yàn),提供了對(duì)于手機(jī)號(hào)、郵箱、驗(yàn)證碼、密碼、身份證號(hào)的驗(yàn)證方法,可以直接copy來用。等下進(jìn)行參數(shù)校驗(yàn)時(shí)我使用的就是這個(gè)類里的校驗(yàn)方法。
/**
* @description : 驗(yàn)證手機(jī)號(hào)、身份證號(hào)、密碼、驗(yàn)證碼、郵箱的工具類
* @author : 小何
*/
public class VerifyUtils {
/**
* 手機(jī)號(hào)正則
*/
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
/**
* 郵箱正則
*/
public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
/**
* 密碼正則。4~32位的字母、數(shù)字、下劃線
*/
public static final String PASSWORD_REGEX = "^\\w{4,32}$";
/**
* 驗(yàn)證碼正則, 6位數(shù)字或字母
*/
public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";
/**
* 身份證號(hào)正則
*/
public static final String ID_CARD_NUMBER_REGEX_18 = "^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";
public static final String ID_CARD_NUMBER_REGEX_15 = "^[1-9]\\d{5}\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{2}$";
/**
* 手機(jī)號(hào)是否合法
* @param phone 要校驗(yàn)的手機(jī)號(hào)
* @return true:符合,false:不符合
*/
public static boolean isPhoneLegal(String phone){
return match(phone, PHONE_REGEX);
}
/**
* 是否是無效郵箱格式
* @param email 要校驗(yàn)的郵箱
* @return true:符合,false:不符合
*/
public static boolean isEmailLegal(String email){
return match(email, EMAIL_REGEX);
}
/**
* 是否是無效驗(yàn)證碼格式
* @param code 要校驗(yàn)的驗(yàn)證碼
* @return true:符合,false:不符合
*/
public static boolean isCodeLegal(String code){
return match(code, VERIFY_CODE_REGEX);
}
// 校驗(yàn)是否不符合正則格式
private static boolean match(String str, String regex){
if (str == null || "".equals(str)) {
return false;
}
return str.matches(regex);
}
/**
* 驗(yàn)證身份證號(hào)是否合法
* @param idCard 身份證號(hào)
* @return true: 合法; false:不合法
*/
public static boolean isIdCardLegal(String idCard) {
if (idCard.length() == 18) {
return match(idCard, ID_CARD_NUMBER_REGEX_18);
} else {
return match(idCard, ID_CARD_NUMBER_REGEX_15);
}
}
}
使用案例:
public static void main(String[] args) {
String phone = "15039469595";
boolean phoneLegal = VerifyUtils.isPhoneLegal(phone);
System.out.println(phoneLegal);
}
3. 注解實(shí)現(xiàn)參數(shù)校驗(yàn)
首先導(dǎo)入依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
導(dǎo)入依賴后可以嘗試使用一下它自帶的參數(shù)校驗(yàn)注解:@NotNull 非空校驗(yàn)
先來說一下這注解實(shí)現(xiàn)參數(shù)校驗(yàn)的使用步驟。
在平時(shí)寫的demo中,本人比較喜歡對(duì)接口另外定義vo來接收數(shù)據(jù),例如前端傳的數(shù)據(jù)是user對(duì)象里的username和password,我們的user里有很多字段,如果單純使用user就太浪費(fèi)了,而且如果直接在實(shí)體類上進(jìn)行自定義注解會(huì)對(duì)實(shí)體類造成代碼污染。所以個(gè)人認(rèn)為定義vo類是很有必要的。
以下是我的登錄接口:
@PostMapping("/login")
public String login(@RequestBody @Validated LoginVo user) {
return "user:" + user.toString();
}
以下是我登錄接口的vo類:
@Data
public class LoginVo {
// 郵箱
@NotNull(message = "郵箱不能為空")
private String email;
// 密碼
private String password;
}
大家可能注意到我多寫了兩個(gè)注解:@Validated、@NotNull(message = “郵箱不能為空”)
對(duì),使用注解進(jìn)行參數(shù)校驗(yàn)就分為兩步:
- 在需要進(jìn)行校驗(yàn)的字段上加對(duì)應(yīng)校驗(yàn)方式,如@NotNull
- 在需要進(jìn)行校驗(yàn)的接口參數(shù)前加@Validated,告訴Spring,這個(gè)類你給我看一下,里面有的字段加了校驗(yàn)注解,符合要求就放行,不符合要求就報(bào)錯(cuò)。
如圖所示:


使用postman發(fā)起請(qǐng)求,故意使得郵箱為空:

會(huì)發(fā)現(xiàn)報(bào)錯(cuò):
Resolved [org.springframework.web.bind.MethodArgumentNotValidException:
Validation failed for argument [0] in public java.lang.String com.example.demo.controller.UserController.login(com.example.demo.domain.vo.LoginVo):
[Field error in object 'loginVo' on field 'email': rejected value [null]; codes [NotNull.loginVo.email,NotNull.email,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [loginVo.email,email]; arguments [];
default message [email]];
default message [郵箱不能為空]] ]
出現(xiàn)這個(gè)異常:MethodArgumentNotValidException,我們就可以在全局異常處理器中捕獲它,返回一個(gè)較為規(guī)范的信息。
4. 自定義注解實(shí)現(xiàn)參數(shù)校驗(yàn)
學(xué)習(xí)了如何使用注解進(jìn)行參數(shù)校驗(yàn),我們就可以進(jìn)行接下來的工作:自定義注解。
由于需求的復(fù)雜,我們現(xiàn)在需要完成注冊(cè)接口,注冊(cè)時(shí)需要身份證號(hào)、電話號(hào)碼、郵箱、密碼,這些字段的注解校驗(yàn)Spring并沒有幫我們實(shí)現(xiàn),此時(shí)就需要DIY注解滿足需求。
如何實(shí)現(xiàn)自定義注解?我們先模仿,先來看看@NotNull注解里面有什么:

@Target、@Retention、@Repeatable、@Documented這些常用的注解就不再解釋,
@Constraint:表示此注解是一個(gè)參數(shù)校驗(yàn)的注解,validateBy指定校驗(yàn)規(guī)則實(shí)現(xiàn)類,這里需要填實(shí)現(xiàn)類.class。
各個(gè)字段的含義:
- message :數(shù)據(jù)不符合校驗(yàn)規(guī)則后的報(bào)錯(cuò)信息。可以是字符串也可以是文件,如果校驗(yàn)字段較多,建議實(shí)現(xiàn)文件形式。
- groups :指定注解使用場景,例如新增、刪除
- payload :往往對(duì)Bean使用
以上這三個(gè)字段都是必須的,每一個(gè)使用ConstraintValidator完成參數(shù)校驗(yàn)都要有這三個(gè)字段。
后面的那個(gè)List是NotNull專屬的,所以不必關(guān)心。
那么我們大可以模仿@NotNull來實(shí)現(xiàn)自定義注解。
第一步:實(shí)現(xiàn)校驗(yàn)類:
需要實(shí)現(xiàn)一個(gè)接口:ConstraintValidator<?, ?>
# ConstraintValidator<?, ?>
第一個(gè)參數(shù)是自定義注解
第二個(gè)參數(shù)是需要進(jìn)行校驗(yàn)的數(shù)據(jù)的數(shù)據(jù)類型
例如想對(duì)手機(jī)號(hào)校驗(yàn),第一個(gè)參數(shù)是Phone,第二個(gè)參數(shù)是String
這個(gè)接口提供了一個(gè)方法:
boolean isValid(T value, ConstraintValidatorContext context);
第一個(gè)參數(shù)就是前端傳來的數(shù)據(jù)。我們可以對(duì)這個(gè)數(shù)據(jù)進(jìn)行判斷,返回一個(gè)布爾值
public class VerifyPhone implements ConstraintValidator<Phone, String> {
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
// 判斷手機(jī)號(hào)是否合法
return VerifyUtils.isPhoneLegal(s);
}
}
第二步:實(shí)現(xiàn)注解,這個(gè)注解的名稱需要與ConstraintValidator的第一個(gè)參數(shù)保持一致。
特別注意的是,@Constraint注解里面的validatedBy的值是第一步的Class實(shí)例。
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {VerifyPhone.class}
)
public @interface Phone {
boolean isRequired() default false;
String message() default "手機(jī)號(hào)格式錯(cuò)誤";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
第三步:在字段上加上相應(yīng)注解。
@Data
public class RegisterVo {
private String name;
// 身份證號(hào)
private String id;
// 電話號(hào)碼
@Phone
private String phone;
// 郵箱
private String email;
// 密碼
private String password;
}
第四步:在參數(shù)前加上@Validated。
@PutMapping("/register")
public String register(@RequestBody @Validated RegisterVo user) {
return "user: " + user.toString();
}
這樣一來,就優(yōu)雅的實(shí)現(xiàn)了參數(shù)校驗(yàn)。別以為我們搞這么多類很麻煩,除非你想每一個(gè)controller里都這樣寫:
@PutMapping("/register")
public String register(@RequestBody @Validated RegisterVo user) {
if (VerifyUtils.isPhoneLegal("xxx")) {
return "手機(jī)號(hào)格式錯(cuò)誤";
}
if (VerifyUtils.isCodeLegal("xxx")) {
return "驗(yàn)證碼格式錯(cuò)誤";
}
if (VerifyUtils.isIdCardLegal("xxx")) {
return "身份證格式錯(cuò)誤";
}
if (VerifyUtils.isEmailLegal("xxx")) {
return "郵箱格式錯(cuò)誤";
}
return "user: " + user.toString();
}
真的很low很麻煩好嗎。
可能步驟有點(diǎn)繁瑣,不過也就4步,畫張圖加強(qiáng)一下記憶:

以上就是SpringBoot通過自定義注解實(shí)現(xiàn)參數(shù)校驗(yàn)的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot參數(shù)校驗(yàn)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談Java list.remove( )方法需要注意的兩個(gè)坑
這篇文章主要介紹了淺談Java list.remove( )方法需要注意的兩個(gè)坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-12-12
Java 本地方法Native Method詳細(xì)介紹
這篇文章主要介紹了 Java 本地方法Native Method詳細(xì)介紹的相關(guān)資料,需要的朋友可以參考下2017-02-02
Mac配置 maven以及環(huán)境變量設(shè)置方式
這篇文章主要介紹了Mac配置 maven以及環(huán)境變量設(shè)置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
Logback與Log4j2日志框架性能對(duì)比與調(diào)優(yōu)方式
這篇文章主要介紹了Logback與Log4j2日志框架性能對(duì)比與調(diào)優(yōu)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-12-12
springboot2.0如何通過fastdfs實(shí)現(xiàn)文件分布式上傳
這篇文章主要介紹了springboot2.0如何通過fastdfs實(shí)現(xiàn)文件分布式上傳,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
將Java程序打包成EXE文件的實(shí)現(xiàn)方式
這篇文章主要介紹了將Java程序打包成EXE文件的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-04-04
詳解Springboot2.3集成Spring security 框架(原生集成)
這篇文章主要介紹了詳解Springboot2.3集成Spring security 框架(原生集成),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08
Java中Runnable和Callable分別什么時(shí)候使用
提到 Java 就不得不說多線程了,就算你不想說,面試官也得讓你說呀,那說到線程,就不得不說Runnable和Callable這兩個(gè)家伙了,二者在什么時(shí)候使用呢,下面就來和簡單講講2023-08-08
MybatisPlus為何可以不用@MapperScan詳解
這篇文章主要給大家介紹了關(guān)于MybatisPlus為何可以不用@MapperScan的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用MybatisPlus具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-04-04

