SpringBoot參數(shù)校驗@Validated、@Valid方式詳解
SpringBoot參數(shù)校驗@Validated、@Valid(javax.validation)
一、應(yīng)用場景
在實際開發(fā)中,前端校驗并不安全,任何人都可以通過接口來調(diào)用我們的服務(wù),就算加了一層token的校驗,有心人總會轉(zhuǎn)空子,來傳各式各樣錯誤的參數(shù),如果后端不校驗,導(dǎo)致數(shù)據(jù)庫數(shù)據(jù)混亂、特別是關(guān)于金額的數(shù)據(jù),可能一個接口把公司都給干倒了
二、原生參數(shù)校驗
返回類(可以不用看)
/**
* 用于返回
* @param <T>
*/
@ApiModel("統(tǒng)一返回類")
public class Results<T> {
public static final String ERROR = "500";
public static final String SUCCESS = "200";
/**
* 返回碼
*/
@ApiModelProperty("返回碼,正確碼為:200")
private String resCode ;
/**
* 返回消息
*/
@ApiModelProperty("返回消息")
private String msg ;
/**
* 返回實體
*/
@ApiModelProperty("返回實體")
private T obj;
public static <T> Results<T> success(){
return success(SUCCESS,"成功",null);
}
public static <T> Results<T> success(String msg){
return success(SUCCESS,msg,null);
}
public static <T> Results<T> success(T obj){
return success(SUCCESS,"成功",obj);
}
public static <T> Results<T> success(String msg,T obj){
return success(SUCCESS,msg,obj);
}
public static <T> Results<T> success(String resCode,String msg,T obj){
Results<T> result = new Results<T>();
result.setResCode(resCode);
result.setMsg(msg);
result.setObj(obj);
return result;
}
public static <T> Results<T> failed() {
return failed(ERROR,"失敗",null);
}
public static <T> Results<T> failed(String msg) {
return failed(ERROR,msg,null);
}
public static <T> Results<T> failed(String msg,T obj) {
return failed(ERROR,msg,obj);
}
public static <T> Results<T> failed(String resCode,String msg) {
return failed(resCode,msg,null);
}
public static <T> Results<T> failed(Integer resCode,String msg) {
return failed(String.valueOf(resCode),msg);
}
public static <T> Results<T> failed(String resCode,String msg,T obj) {
Results<T> result = new Results<T>();
result.setResCode(resCode);
result.setMsg(msg);
result.setObj(obj);
return result;
}
public static <T> Results<T> failedNoPermission() {
return failed(90005,"沒有權(quán)限");
}
public static <T> Results<T> failedNoPermission(String msg) {
return failed(90005,msg);
}
public static <T> Results<T> failedParameterException() {
return failed(90004,"參數(shù)異常");
}
public static <T> Results<T> failedParameterException(String msg) {
return failed(90004,msg);
}
public static <T> Results<T> failedLoginException() {
return failed(90002,"登錄失敗");
}
public static <T> Results<T> failedLoginException(String msg) {
return failed(90002,msg);
}
public String getResCode() {
return resCode;
}
public void setResCode(String resCode) {
this.resCode = resCode;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
@Override
public String toString() {
return "Results{" +
"resCode='" + resCode + '\'' +
", msg='" + msg + '\'' +
", obj=" + obj +
'}';
}
}
實體類
@ApiModel("測試 validation 入?yún)?)
@Data
public class TestDto {
@ApiModelProperty(value = "名字",required = true)
private String name;
@ApiModelProperty(value = "年齡",required = true)
private Integer age;
@ApiModelProperty(value = "愛好",required = true)
private List<String> hobbies;
}
服務(wù)層(為了方便,我直接跟Controller寫在一起了)
我們可以看見如果參數(shù)過大,要一個一個篩選條件十分浪費時間
@RestController
// lombok 的日志注解
@Slf4j
// swagger 的注解
@Api("測試")
public class TestController {
@PostMapping("/testValidation")
// swagger 的注解
@ApiOperation("測試 validation")
public Results testValidation(@RequestBody TestDto dto){
try {
log.info("test 入?yún)?dto={}",dto);
// 這要一個一個的塞,很浪費時間
if (dto.getName() == null || "".equals(dto.getName().trim())){
return Results.failed("名字不能為空");
}
if (dto.getAge() == null){
return Results.failed("年齡不能為空");
}
if (dto.getHobbies() == null || dto.getHobbies().size() == 0){
return Results.failed("愛好不能為空");
}
return Results.success();
} catch (Exception e) {
log.error("test 報錯",e);
return Results.failed();
}
}
}
三、使用javax.validation進行參數(shù)校驗
導(dǎo)包
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
全局異常處理類
springboot開發(fā)中,在進行 入?yún)⑿r?時,會拋出異常,最終全局統(tǒng)一捕獲異常。
- 表單綁定到 java bean 出錯時,會拋出
BindException異常 - 將請求體解析并綁定到 java bean 時,如果出錯,則拋出
MethodArgumentNotValidException異常 - 普通參數(shù)(非 java bean)校驗出錯時,會拋出
ConstraintViolationException異常
@RestControllerAdvice
public class ExceptionControllerAdvice {
/**
* 將請求體解析并綁定到 java bean 時,如果出錯
* 表單綁定到 java bean 出錯
* 校驗參數(shù) @RequestBody 時的異常
* 注意 @Validated 需要放在 自定義實體類(入?yún)ⅲ?的類上
* 例如 :
* public Results testUpdate(@Validated(Update.class) @RequestBody TestDto dto){}
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Results MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
// 從異常對象中拿到ObjectError對象
BindingResult br = e.getBindingResult();
if (br.hasFieldErrors()) {
List<FieldError> fieldErrorList = br.getFieldErrors();
List<String> errors = new ArrayList<>(fieldErrorList.size());
for (FieldError error : fieldErrorList) {
errors.add(error.getField() + ":" + error.getDefaultMessage());
}
// 然后提取錯誤提示信息進行返回
return Results.failed(errors.toString());
}
// 然后提取錯誤提示信息進行返回
return Results.failed("校驗錯誤");
}
/**
* 表單綁定到 java bean 出錯
*
* @param e
* @return
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(BindException.class)
public Results MethodArgumentNotValidExceptionHandler(BindException e) {
// 從異常對象中拿到ObjectError對象
BindingResult br = e.getBindingResult();
if (br.hasFieldErrors()) {
List<FieldError> fieldErrorList = br.getFieldErrors();
List<String> errors = new ArrayList<>(fieldErrorList.size());
for (FieldError error : fieldErrorList) {
errors.add(error.getField() + ":" + error.getDefaultMessage());
}
// 然后提取錯誤提示信息進行返回
return Results.failed(errors.toString());
}
// 然后提取錯誤提示信息進行返回
return Results.failed("校驗錯誤");
}
/**
* 普通參數(shù)(非 java bean)校驗出錯
* 校驗參數(shù) @RequestParam @PathVariable 時的異常
* 注意 @Validated 需要放在controller的類上
* 例如:
*
* @RestController
* @RequestMapping("/cs")
* @Validated public class TestController {
* @PostMapping("/test") public Results test(
* @Pattern(regexp = "^\\d{19}$", message = "用戶ID,應(yīng)為19位數(shù)字") String id,
* @NotBlank(message = "名字不能為空") String name
* ) {}
* }
*/
@ResponseStatus(HttpStatus.OK)
@ExceptionHandler(ConstraintViolationException.class)
public Results constraintViolationException(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
if (CollectionUtils.isEmpty(violations)) {
log.error("constraintViolationException violations 為空", e);
return Results.failed();
}
Map<String, String> map = violations.stream()
.collect(Collectors.toMap(o -> {
PathImpl x = (PathImpl) o.getPropertyPath();
return x.getLeafNode().toString();
}, ConstraintViolation::getMessage, (k1, k2) -> k1));
return Results.failed(map.toString());
}
}
如果不加這個全局處理類,只會給前端返回這樣的參數(shù)

加上全局配置類

實體類
@ApiModel("測試 validation 入?yún)?)
@Data
public class TestDto {
@ApiModelProperty(value = "名字",required = true)
// 適用于 String 類型的數(shù)據(jù)上,加了@NotBlank 注解的參數(shù)不能為 Null 且 trim() 之后 size > 0,必須有實際字符
@NotBlank(message = "名字不能為空")
private String name;
@ApiModelProperty(value = "年齡",required = true)
@NotNull(message = "年齡不能為空")
// 適用于基本數(shù)據(jù)類型(Integer,Long,Double等等),當(dāng) @NotNull 注解被使用在 String 類型的數(shù)據(jù)上,則表示該數(shù)據(jù)不能為 Null(但是可以為 Empty)
private Integer age;
@ApiModelProperty(value = "愛好",required = true)
// 適用于 String、Collection集合、Map、數(shù)組等等,加了@NotEmpty 注解的參數(shù)不能為 Null 或者 長度為 0
@NotEmpty(message = "年齡不能為空")
private List<String> hobbies;
}
服務(wù)層(為了方便,我直接跟Controller寫在一起了)
必須要加上 @Valid 或者 @Validated,后續(xù)我會講解這兩個有什么不同,目前來說,都可以用,但推薦用 @Validated
@RestController
@Slf4j
@Api("測試")
public class TestController {
@PostMapping("/testValidation")
@ApiOperation("測試 validation")
// 必須要加上 @Valid 或者 @Validated
public Results testValidation(
// 必須要加上 @Valid 或者 @Validated
@Valid @RequestBody TestDto dto){
try {
log.info("test 入?yún)?dto={}",dto);
return Results.success();
} catch (Exception e) {
log.error("test 報錯",e);
return Results.failed();
}
}
}
測試

四、javax.validation包下其它常用的校驗注解:
這個顏色的是常用的
| 注解 | 含義 |
|---|---|
| @Null | 任何類型 必須為null |
| @NotBlank | 字符串、字符 字符類不能為null,且去掉空格之后長度大于 |
| @NotNull | 任何類型 不能為null |
| @Length(min = 6, max = 8, message = “密碼長度為6-8位。”) | 字符串的大小必須在指定的范圍內(nèi) |
| @NotEmpty | 適用于 String、Collection集合、Map、數(shù)組等等,加了@NotEmpty 注解的參數(shù)不能為 Null 或者 長度為 0 |
| @AssertTrue | Boolean、boolean 布爾屬性必須是true |
| @AssertFalse | Boolean、boolean 布爾屬性必須是false |
| @Min(10) | 必須是一個數(shù)字,其值必須大于等于指定的最小值(我這填的是10)(整型) |
| @Max(10) | 必須是一個數(shù)字,其值必須小于等于指定的最大值(我這填的是10)(整型) |
| @DecimalMin(“10”) | 必須是一個數(shù)字,其值必須大于等于指定的最小值(我這填的是10)(字符串,可以是小數(shù)) |
| @DecimalMax(“10”) | 必須是一個數(shù)字,其值必須小于等于指定的最大值(我這填的是10)(字符串,可以是小數(shù)) |
| @Size(max = 10,min = 1) | 集合 限定集合大小 |
| @Digits(integer = 3, fraction = 2, message = “請輸入有效的數(shù)字”) private double number; | @Digits 用于驗證數(shù)字的整數(shù)位數(shù)和小數(shù)位數(shù)。該注解的 integer 和 fraction 屬性分別用于指定整數(shù)位數(shù)和小數(shù)位數(shù)的限制。 integer 屬性用于指定數(shù)字的最大整數(shù)位數(shù)。它是一個整數(shù)值,表示數(shù)字允許的最大整數(shù)位數(shù)。例如,integer = 3 表示數(shù)字最多可以有三位整數(shù)部分。 fraction 屬性用于指定數(shù)字的最大小數(shù)位數(shù)。它是一個整數(shù)值,表示數(shù)字允許的最大小數(shù)位數(shù)。例如,fraction = 2 表示數(shù)字最多可以有兩位小數(shù)部分。 |
| @Past | 時間、日期 必須是一個過去的時間或日期 |
| @Future | 時期、時間 必須是一個未來的時間或日期 |
| 字符串 必須是一個郵箱格式 | |
| @Pattern(regexp = “[a-zA-Z]*”, message = “密碼不合法”) | 字符串、字符 正則匹配字符串 |
| @Range(max = 150, min = 1, message = “年齡范圍應(yīng)該在1-150內(nèi)。”) | 數(shù)字類型(原子和包裝) 限定數(shù)字范圍(長整型) |
| @URL(protocol=,host=, port=,regexp=, flags=) | 被注釋的字符串必須是一個有效的url |
| @CreditCardNumber | 被注釋的字符串必須通過Luhn校驗算法,銀行卡,信用卡等號碼一般都用Luhn計算合法性 |
| @ScriptAssert(lang=, script=, alias=) | 要有Java Scripting API 即JSR 223 (“Scripting for the JavaTM Platform”)的實現(xiàn) |
| @SafeHtml(whitelistType=, additionalTags=) | classpath中要有jsoup包 |
五、@Validated 與 @Valid 比較
文字講解
Spring Validation驗證框架對參數(shù)的驗證機制提供了@Validated(Spring’s JSR-303規(guī)范,是標(biāo)準(zhǔn)JSR-303的一個變種),javax提供了@Valid(標(biāo)準(zhǔn)JSR-303規(guī)范),配合BindingResult可以直接提供參數(shù)驗證結(jié)果。
@Valid屬于javax.validation包下,是jdk給提供的 是使用Hibernate validation的時候使用
@Validated是org.springframework.validation.annotation包下的,是spring提供的 是只用Spring Validator校驗機制使用
說明:java的JSR303聲明了@Valid這類接口,而Hibernate-validator對其進行了實現(xiàn)
@Validation對@Valid進行了二次封裝,在使用上并沒有區(qū)別,但在分組、注解位置、嵌套驗證等功能上有所不同,這里主要就這幾種情況進行說明。
在檢驗Controller的入?yún)⑹欠穹弦?guī)范時,使用@Validated或者@Valid在基本驗證功能上沒有太多區(qū)別。但是在分組、注解地方、嵌套驗證等功能上兩個有所不同:
- 分組:
@Validated:提供了一個分組功能,可以在入?yún)Ⅱ炞C時,根據(jù)不同的分組采用不同的驗證機制,這個網(wǎng)上也有資料,不詳述。@Valid:作為標(biāo)準(zhǔn)JSR-303規(guī)范,還沒有吸收分組的功能。
- 注解地方:
@Validated:用在類型、方法和方法參數(shù)上。但不能用于成員屬性(field)
@Valid:可以用在方法、構(gòu)造函數(shù)、方法參數(shù)和成員屬性(field)上 所以可以用@Valid實現(xiàn)嵌套驗證
總結(jié):
@Valid 和 @Validated 兩者都可以對數(shù)據(jù)進行校驗,待校驗字段上打的規(guī)則注解(@NotNull, @NotEmpty等)都可以對 @Valid 和 @Validated 生效;
@Valid 進行校驗的時候,需要用 BindingResult 來做一個校驗結(jié)果接收。當(dāng)校驗不通過的時候,如果手動不 return ,則并不會阻止程序的執(zhí)行;
@Validated 進行校驗的時候,當(dāng)校驗不通過的時候,程序會拋出400異常,阻止方法中的代碼執(zhí)行,這時需要再寫一個全局校驗異常捕獲處理類,然后返回校驗提示。
總體來說,@Validated 使用起來要比 @Valid 方便一些,它可以幫我們節(jié)省一定的代碼,并且使得方法看上去更加的簡潔。
代碼講解,groups屬性
在開發(fā)中,新增、修改兩個接口,一般關(guān)系就在于新增時ID可以為空,修改時ID不能為空,那我們?nèi)绻褂?validation 用于參數(shù)校驗,創(chuàng)建兩個實體類就非常的不劃算,這時
①、創(chuàng)建一個update接口
import javax.validation.groups.Default;
public interface Update extends Default {
}
②、修改實體類
@ApiModel("測試 validation 入?yún)?)
@Data
public class TestDto {
@ApiModelProperty(value = "ID",required = true)
// 新增時ID為空,修改時ID不能為空
@NotNull(message = "ID不能為空",groups = Update.class)
private Integer id;
@ApiModelProperty(value = "名字",required = true)
@NotBlank(message = "名字不能為空")
private String name;
}
③、服務(wù)層(為了方便,我直接跟Controller寫在一起了)
@RestController
@Slf4j
@Api("測試")
public class TestController {
@PostMapping("/testAdd")
@ApiOperation("測試 新增")
public Results testAdd(@Validated @RequestBody TestDto dto){
try {
log.info("testAdd 入?yún)?dto={}",dto);
return Results.success();
} catch (Exception e) {
log.error("testAdd 報錯",e);
return Results.failed();
}
}
@PostMapping("/testUpdate")
@ApiOperation("測試 新增")
public Results testUpdate(@Validated(Update.class) @RequestBody TestDto dto){
try {
log.info("testUpdate 入?yún)?dto={}",dto);
return Results.success();
} catch (Exception e) {
log.error("testUpdate 報錯",e);
return Results.failed();
}
}
}
⑤、測試
- 新增:

- 修改:

六、校驗List
錯誤案例
僅在外層包一個@Valid是校驗不了List中實體類的參數(shù)的
@ResponseBody
@PostMapping("/testList")
public Results testList(@RequestBody @Valid List<TestDto> dto){
try {
log.info("testList 入?yún)?dto={}",dto);
return Results.success();
} catch (Exception e) {
log.error("testList 報錯",e);
return Results.failed();
}
}
方法其實有兩種,我就只講比較優(yōu)雅且方便的方法
創(chuàng)建ValidList類(可以直接復(fù)制使用)
/**
* 可被校驗的List
*
* @param <E> 元素類型
* @author Deolin
*/
@Data
public class ValidList<E> implements List<E> {
@Valid
private List<E> list = new ArrayList<>();
@Override
public int size() {
return list.size();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public boolean contains(Object o) {
return list.contains(o);
}
@Override
public Iterator<E> iterator() {
return list.iterator();
}
@Override
public Object[] toArray() {
return list.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}
@Override
public boolean add(E e) {
return list.add(e);
}
@Override
public boolean remove(Object o) {
return list.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return list.addAll(c);
}
@Override
public boolean addAll(int index, Collection<? extends E> c) {
return list.addAll(index, c);
}
@Override
public boolean removeAll(Collection<?> c) {
return list.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return list.retainAll(c);
}
@Override
public void clear() {
list.clear();
}
@Override
public E get(int index) {
return list.get(index);
}
@Override
public E set(int index, E element) {
return list.set(index, element);
}
@Override
public void add(int index, E element) {
list.add(index, element);
}
@Override
public E remove(int index) {
return list.remove(index);
}
@Override
public int indexOf(Object o) {
return list.indexOf(o);
}
@Override
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
@Override
public ListIterator<E> listIterator() {
return list.listIterator();
}
@Override
public ListIterator<E> listIterator(int index) {
return list.listIterator(index);
}
@Override
public List<E> subList(int fromIndex, int toIndex) {
return list.subList(fromIndex, toIndex);
}
}
測試
controller:
@ResponseBody
@PostMapping("/testList")
public Results testList(@RequestBody @ValidList List<TestDto> dto){
try {
log.info("testList 入?yún)?dto={}",dto);
return Results.success();
} catch (Exception e) {
log.error("testList 報錯",e);
return Results.failed();
}
}
經(jīng)過我們寫的全局異常處理類,得出的結(jié)果

七、自定義 枚舉校驗注解
注解
/**
* 枚舉校驗注解
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EnumValueValidator.class})
public @interface EnumValue {
// 默認錯誤消息
String message() default "必須為指定值";
String[] strValues() default {};
int[] intValues() default {};
// 分組
Class<?>[] groups() default {};
// 負載
Class<? extends Payload>[] payload() default {};
// 指定多個時使用
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Documented
@interface List {
EnumValue[] value();
}
}
枚舉校驗注解處理類
/**
* 枚舉校驗注解處理類
*/
public class EnumValueValidator implements ConstraintValidator<EnumValue, Object> {
private String[] strValues;
private int[] intValues;
@Override
public void initialize(EnumValue constraintAnnotation) {
strValues = constraintAnnotation.strValues();
intValues = constraintAnnotation.intValues();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value instanceof String) {
for (String s : strValues) {
if (s.equals(value)) {
return true;
}
}
} else if (value instanceof Integer) {
for (int s : intValues) {
if (s == ((Integer) value).intValue()) {
return true;
}
}
}
return false;
}
}
使用
@Data
@Accessors(chain = true)
public class Test {
@EnumValue(strValues = {"1", "2", "3"}, message = "傳入類型不正確")
private String type;
@EnumValue(intValues = {1, 2, 3}, message = "傳入類型不正確")
private String status;
}
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
springboot2整合redis使用lettuce連接池的方法(解決lettuce連接池?zé)o效問題)
這篇文章主要介紹了springboot2整合redis使用lettuce連接池(解決lettuce連接池?zé)o效問題),本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-12-12
Springboot整合Dubbo教程之項目創(chuàng)建和環(huán)境搭建
本篇文章主要介紹了Springboot整合Dubbo教程之項目創(chuàng)建和環(huán)境搭建,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-12-12
Java 中使用數(shù)組存儲和操作數(shù)據(jù)
本文將介紹Java中常用的數(shù)組操作方法,通過詳細的示例和解釋,幫助讀者全面理解和掌握這些方法,具有一定的參考價值,感興趣的可以了解一下2023-09-09

