javascript
SpringBoot-Validation优雅实现参数校验
1、是什么?
它簡化了 Java Bean Validation 的集成。Java Bean Validation 通過 JSR 380,也稱為 Bean Validation 2.0,是一種標(biāo)準(zhǔn)化的方式,用于在 Java 應(yīng)用程序中對對象的約束進(jìn)行聲明式驗(yàn)證。它允許開發(fā)人員使用注解來定義驗(yàn)證規(guī)則,并自動(dòng)將規(guī)則應(yīng)用于相應(yīng)的字段或方法參數(shù)
為了我們方便地使用參數(shù)校驗(yàn)功能了
2、怎么玩?
(1) 首先導(dǎo)入相關(guān)依賴
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ly</groupId>
<artifactId>springboot-validate</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.4</version>
</parent>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
(2) 在對應(yīng)實(shí)體上加@Validated注解,開啟校驗(yàn)規(guī)則
package com.ly.valid.controller;
import com.ly.valid.common.R;
import com.ly.valid.common.ResultCodeEnum;
import com.ly.valid.entity.Person;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author ly (個(gè)人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-11 21:01
* @tags 喜歡就去努力的爭取
*/
@RestController
public class TestController {
/**
* 保存
*/
@PostMapping("/test1")
public R save(@Validated @RequestBody Person person, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
List<FieldError> fieldErrorList = bindingResult.getFieldErrors();
Map<String, String> map = new HashMap<>(fieldErrorList.size());
fieldErrorList.forEach(item -> {
String message = item.getDefaultMessage(); // 如果沒有在相應(yīng)的注解中加message,則會(huì)獲取默認(rèn)信息 例如:@NotBlank(message = "品牌名必須提交"),獲取的message則為品牌名必須提交
String field = item.getField(); // 獲取哪個(gè)字段出現(xiàn)的問題
map.put(field, message);
});
return R.fail(ResultCodeEnum.PARAM_ERROR, map);
} else {
// 偽代碼
// personService.save(person);
}
return R.success();
}
/**
* 全局處理
*
* @param person
* @return
*/
@PostMapping("/test2")
public R test(@Validated Person person) {
return R.success(person);
}
}
(3) 給對應(yīng)的實(shí)體Bean添加校驗(yàn)注解,并自定義message提示
package com.ly.valid.entity;
import com.ly.valid.constant.RegularConstant;
import jakarta.validation.constraints.*;
import lombok.Data;
import java.time.LocalDate;
/**
* @author ly (個(gè)人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-11 21:03
* @tags 喜歡就去努力的爭取
*/
@Data
public class Person {
@NotBlank(message = "name 姓名不能為空")
private String name;
@NotNull(message = "age 年齡不能為空")
@Min(value = 0, message = "年齡不能小于0")
private Integer age;
@NotNull(message = "gender 性別不能為空")
private Integer gender;
@Email(regexp = RegularConstant.EMAIL, message = "email 郵箱格式不正確")
private String email;
@Pattern(regexp = RegularConstant.PHONE, message = "phone 手機(jī)號(hào)格式不正確")
private String phone;
@Past(message = "birthday 生日日期有誤")
private LocalDate birthday;
}
(4) 不加@Validate注解,測試一下
(5) 開啟校驗(yàn)功能@Validated:不使用自定義message會(huì)有默認(rèn)提示信息
(6) 方式一: 我想自定義提示信息怎么整?簡單,只需要給響應(yīng)的校驗(yàn)bean后面緊跟著添加一個(gè)BindingResult,就可以獲取到校驗(yàn)的結(jié)果了
/**
* 保存
*/
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
List<FieldError> fieldErrorList = bindingResult.getFieldErrors();
Map<String, String> map = new HashMap<>();
fieldErrorList.forEach(item -> {
String message = item.getDefaultMessage(); // 如果沒有在相應(yīng)的注解中加message,則會(huì)獲取默認(rèn)信息 例如:@NotBlank(message = "品牌名必須提交"),獲取的message則為品牌名必須提交
String field = item.getField(); // 獲取哪個(gè)字段出現(xiàn)的問題
map.put(field, message);
});
return R.error(400, "提交的數(shù)據(jù)不合法").put("data", map);
} else {
brandService.save(brand);
}
return R.ok();
}
(7) 測試一下
(8) 方式二:如果有很多需要這樣手動(dòng)一個(gè)個(gè)處理,就顯得很麻煩了;所以我們需要一個(gè)全局處理
package com.ly.valid.common;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author ly (個(gè)人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-11 22:13
* @tags 喜歡就去努力的爭取
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 處理 form data方式調(diào)用接口校驗(yàn)失敗拋出的異常 (對象參數(shù))
*/
@ExceptionHandler(BindException.class)
public R error(BindException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<String> errorMessages = fieldErrors.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.toList();
return R.fail(ResultCodeEnum.PARAM_ERROR.getCode(), errorMessages.toString());
}
/**
* 處理 json 請求體調(diào)用接口校驗(yàn)失敗拋出的異常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public R error(MethodArgumentNotValidException e) {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
List<String> errorMessages = fieldErrors.stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.toList();
return R.fail(ResultCodeEnum.PARAM_ERROR.getCode(), errorMessages.toString());
}
/**
* 單個(gè)參數(shù)校驗(yàn)失敗拋出的異常
*/
@ExceptionHandler(ConstraintViolationException.class)
public R error(ConstraintViolationException e) {
String errorMsg = e.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessageTemplate)
.collect(Collectors.joining());
return R.fail(errorMsg);
}
}
(9) 測試一下
3、相關(guān)注解信息
| 注解 | 數(shù)據(jù)類型 | 說明 |
|---|---|---|
| @NotBlank | CharSequence | 驗(yàn)證注解的元素值不為空(不為null、去除首位空格后長度為0),不同于@NotEmpty,@NotBlank只應(yīng)用于字符串且在比較時(shí)會(huì)去除字符串的空格 |
| @NotEmpty | CharSequence,Collection,Map,Arrays | 驗(yàn)證注解的元素值不為null且不為空(字符串長度不為0、集合大小不為0) |
| @Length(min=下限, max=上限) | CharSequence | 驗(yàn)證注解的元素值長度在min和max區(qū)間內(nèi) |
| @NotNull | 所有類型 | 驗(yàn)證注解的元素值不是null |
| @Null | 所有類型 | 驗(yàn)證注解的元素值是null |
| @Max(value=n) | BigDecimal,BigInteger,byte,short,int,long和原始類型的相應(yīng)包裝。HV額外支持:CharSequence的任何子類型(評(píng)估字符序列表示的數(shù)字值),Number的任何子類型。 | 驗(yàn)證注解的元素值小于等于@Max指定的value值 |
| @Min(value=n) | BigDecimal,BigInteger,byte,short,int,long和原始類型的相應(yīng)包裝。HV額外支持:CharSequence的任何子類型(評(píng)估char序列表示的數(shù)值),Number的任何子類型。 | 驗(yàn)證注解的元素值大于等于@Min指定的value值 |
| @Size(min=最小值, max=最大值) | 字符串,集合,映射和數(shù)組。HV額外支持:CharSequence的任何子類型。 | 驗(yàn)證注解的元素值的在min和max(包含)指定區(qū)間之內(nèi),如字符長度、集合大小 |
| CharSequence | 驗(yàn)證注解的元素值是Email,也可以通過正則表達(dá)式和flag指定自定義的email格式 | |
| @Pattern(regex=正則表達(dá)式, flag=) | CharSequence | 驗(yàn)證注解的元素值與指定的正則表達(dá)式匹配 |
| @Range(min=最小值, max=最大值 | CharSequence, Collection, Map and Arrays, BigDecimal, BigInteger, CharSequece, byte, short, int, long以及原始類型各自的包裝 | 驗(yàn)證注解的元素值在最小值和最大值之間 |
| @AssertFalse | Boolean, boolean | 驗(yàn)證注解的元素值是false |
| @AssertTrue | Boolean, boolean | 驗(yàn)證注解的元素值是true |
| @DecimalMax(value=n) | BigDecimal,BigInteger,String,byte,short,int,long和原始類型的相應(yīng)包裝。HV額外支持:Number和CharSequence的任何子類型。 | 驗(yàn)證注解的元素值小于等于@ DecimalMax指定的value值 |
| @DecimalMin(value=n) | BigDecimal,BigInteger,String,byte,short,int,long和原始類型的相應(yīng)包裝。HV額外支持:Number和CharSequence的任何子類型。 | 驗(yàn)證注解的元素值小于等于@ DecimalMin指定的value值 |
| @Digits(integer=整數(shù)位數(shù), fraction=小數(shù)位數(shù)) | BigDecimal,BigInteger,String,byte,short,int,long和原始類型的相應(yīng)包裝。HV額外支持:Number和CharSequence的任何子類型。 | 驗(yàn)證注解的元素值的整數(shù)位數(shù)和小數(shù)位數(shù)上限 |
| @Future | java.util.Date,java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, java.time.chrono.MinguoDate, java.time.chrono.ThaiBuddhistDate; Additionally supported by HV, if the Joda Time date/time API is on the classpath: any implementations of ReadablePartial and ReadableInstant | 驗(yàn)證注解的元素值(日期類型)比當(dāng)前時(shí)間晚 |
| @Past | java.util.Date,java.util.Calendar, java.time.Instant, java.time.LocalDate, java.time.LocalDateTime, java.time.LocalTime, java.time.MonthDay, java.time.OffsetDateTime, java.time.OffsetTime, java.time.Year, java.time.YearMonth, java.time.ZonedDateTime, java.time.chrono.HijrahDate, java.time.chrono.JapaneseDate, java.time.chrono.MinguoDate, java.time.chrono.ThaiBuddhistDate; ,則由HV附加支持:ReadablePartial和ReadableInstant的任何實(shí)現(xiàn)。 | 驗(yàn)證注解的元素值(日期類型)比當(dāng)前時(shí)間早 |
| @Valid | Any non-primitive type(引用類型) | 驗(yàn)證關(guān)聯(lián)的對象,如賬戶對象里有一個(gè)訂單對象,指定驗(yàn)證訂單對象 |
注:HV ---> Hibernate-Validator
4、分組校驗(yàn)(當(dāng)我們新增和修改字段時(shí),有可能校驗(yàn)規(guī)則是不一樣的,那么該如何處理呢?)
(1) 定義分組信息(唯一即可)
package com.ly.valid.group;
/**
* @author ly (個(gè)人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-11 23:10
* @tags 喜歡就去努力的爭取
*/
public interface AddGroup {
}
package com.ly.valid.group;
import jakarta.validation.groups.Default;
/**
* @author ly (個(gè)人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-11 23:10
* @tags 喜歡就去努力的爭取
*/
public interface UpdateGroup extends Default {
}
(2) 在分組參數(shù)后指定它的groups,需要指定在什么情況下需要進(jìn)行校驗(yàn),類型是一個(gè)接口數(shù)組
package com.ly.valid.entity;
import com.ly.valid.constant.RegularConstant;
import com.ly.valid.group.AddGroup;
import com.ly.valid.group.UpdateGroup;
import jakarta.validation.constraints.*;
import jakarta.validation.groups.Default;
import lombok.Data;
import java.time.LocalDate;
/**
* @author ly (個(gè)人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-11 21:03
* @tags 喜歡就去努力的爭取
*/
@Data
public class Person {
@NotBlank(message = "name 姓名不能為空", groups = AddGroup.class)
private String name;
@NotNull(message = "age 年齡不能為空", groups = UpdateGroup.class)
@Min(value = 0, message = "年齡不能小于0")
private Integer age;
@NotNull(message = "gender 性別不能為空", groups = {AddGroup.class, UpdateGroup.class})
private Integer gender;
@NotBlank(message = "email 郵箱不能為空", groups = Default.class)
@Email(regexp = RegularConstant.EMAIL, message = "email 郵箱格式不正確", groups = Default.class)
private String email;
@NotBlank(message = "phone 手機(jī)號(hào)不能為空", groups = UpdateGroup.class)
@Pattern(regexp = RegularConstant.PHONE, message = "phone 手機(jī)號(hào)格式不正確")
private String phone;
@Past(message = "birthday 生日日期有誤")
private LocalDate birthday;
}
(3) 然后再我們的controller層參數(shù)前加上@Validated(value = {UpdateGroup.class})注解,指定它是哪一組
/**
* 測試添加分組
*
* @param person
* @return
*/
@PostMapping("/testAddGroup")
public R testAddGroup(@Validated(AddGroup.class) Person person) {
return R.success(person);
}
/**
* 測試修改分組
*
* @param person
* @return
*/
@PostMapping("/testUpdateGroup")
public R testUpdateGroup(@Validated(UpdateGroup.class) Person person) {
return R.success(person);
}
/**
* 測試添加和修改分組
*
* @param person
* @return
*/
@PostMapping("/testAddAndUpdateGroup")
public R testAddAndUpdateGroup(@Validated({UpdateGroup.class, AddGroup.class}) Person person) {
return R.success(person);
}
/**
* 測試默認(rèn)分組
*
* @param person
* @return
*/
@PostMapping("/testDefaultGroup")
public R testDefaultGroup(@Validated(Default.class) Person person) {
return R.success(person);
}
(4) 測試AddGroup
(5) 測試UpdateGroup
注意:細(xì)心的同學(xué)發(fā)現(xiàn)了,為什么email郵箱是Default.class,修改的時(shí)候也觸發(fā)了呢?原因如下圖:
(6) 測試AddAndUpdateGroup
(7) 測試DefaultGroup
注意:默認(rèn)沒有指定分組的情況下@NotBlank,在分組校驗(yàn)的情況下@Validated(value = {AddGroup.class})不生效
5、自定義校驗(yàn)規(guī)則
例如:現(xiàn)在我有一個(gè)字段gender它的取值就三種0:保密 1:男 2:女,像這種有限個(gè)數(shù)的枚舉值我們該如何去限制呢?這就要使用到的自定義校驗(yàn)注解了
(1) 自定義校驗(yàn)注解
package com.ly.valid.anno;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @author ly (個(gè)人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-12 0:27
* @tags 喜歡就去努力的爭取
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {
// 默認(rèn)錯(cuò)誤消息
String message() default "{my.enum-value.err.msg}";
// String message() default ENUM_VALUE_MESSAGE;
String[] strValues() default {};
int[] intValues() default {};
// 分組
Class<?>[] groups() default {};
// 負(fù)載,可以增加自定義校驗(yàn)邏輯
Class<? extends Payload>[] payload() default {};
// 指定多個(gè)時(shí)使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
EnumValue[] value();
}
}
(2) 自定義校驗(yàn)器
package com.ly.valid.anno;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* 枚舉值校驗(yàn)注解處理類
*
* @author ly (個(gè)人博客:https://www.cnblogs.com/ybbit)
* @date 2023-12-12 0:34
* @tags 喜歡就去努力的爭取
*/
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
/**
* 字符串
*/
private final Set<String> strValueSet = new HashSet<>();
/**
* 數(shù)值
*/
private final Set<Integer> intValueSet = new HashSet<>();
@Override
public void initialize(EnumValue constraintAnnotation) {
strValueSet.addAll(Arrays.asList(constraintAnnotation.strValues()));
for (int i : constraintAnnotation.intValues()) {
intValueSet.add(i);
}
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value instanceof Integer) {
// 整數(shù)值類型
return intValueSet.contains(value);
} else if (value instanceof String) {
// 字符串類型
return strValueSet.contains(value);
}
return false;
}
}
(3) 關(guān)聯(lián)自定義的校驗(yàn)器和自定義的校驗(yàn)注解
注意:這里的ValidationMessages.properties是數(shù)據(jù)校驗(yàn)國際化配置文件;名字必須是這個(gè)
my.enum-value.err.msg=性別字段值只能為0、1、2
@EnumValue(intValues = {0, 1, 2}, groups = AddGroup.class)
@NotNull(message = "gender 性別不能為空", groups = {AddGroup.class, UpdateGroup.class})
private Integer gender;
(4) 測試一下
6、paload() 負(fù)載的用法
// TODO 未完待續(xù)
總結(jié)
以上是生活随笔為你收集整理的SpringBoot-Validation优雅实现参数校验的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Chrome扩展的核心:manifest
- 下一篇: 如何将看看新闻App中的新闻推送功能关闭