Java SpringBoot Validation用法案例詳解
提到輸入?yún)?shù)的基本驗證(非空、長度、大小、格式…),在以前我們還是通過手寫代碼,各種if、else、StringUtils.isEmpty、CollectionUtils.isEmpty…,真感覺快要瘋了,太繁瑣,Low爆了…,其實在Java生態(tài)提供了一套標(biāo)準(zhǔn)JSR-380(aka. Bean Validation 2.0,part of Jakarta EE and JavaSE),它已成為對象驗證事實上的標(biāo)準(zhǔn),這套標(biāo)準(zhǔn)可以通過注解的形式(如@NotNull, @Size…)來對bean的屬性進行驗證。而Hibernate Validator對這套標(biāo)準(zhǔn)進行了實現(xiàn),SpringBoot Validation無縫集成了Hibernate Validator、自定義驗證器、自動驗證的功能。下文將對SpringBoot集成Validation進行展開。
注: 完整示例代碼可參見GitHub:https://github.com/marqueeluo/spring-boot-validation-demo
constraints分類
JSR-380的支持的constrants注解匯總?cè)缦卤恚?/p>
| 分類 | 注解 | 適用對象 | null是否驗證通過 | 說明 |
|---|---|---|---|---|
| 非空 | @NotNull | 所有對象 | No | 不是null |
| 非空 | @NotEmpty | CharSequence, Collection, Map, Array | No | 不是null、不是""、size>0 |
| 非空 | @NotBlank | CharSequence | No | 不是null、trim后長度大于0 |
| 非空 | @Null | 所有對象 | Yes | 是null |
| 長度 | @Size(min=0, max=Integer.MAX_VALUE) | CharSequence, Collection, Map, Array | Yes | 字符串長度、集合size |
| 大小 | @Positive | BigDecimal, BigInteger, byte, short, int, long, float, double | Yes | 數(shù)字>0 |
| 大小 | @PositiveOrZero | BigDecimal, BigInteger, byte, short, int, long, float, double | Yes | 數(shù)字>=0 |
| 大小 | @Negative | BigDecimal, BigInteger, byte, short, int, long, float, double | Yes | 數(shù)字<0 |
| 大小 | @NegativeOrZero | BigDecimal, BigInteger, byte, short, int, long, float, double | Yes | 數(shù)字<=0 |
| 大小 | @Min(value=0L) | BigDecimal, BigInteger, byte, short, int, long | Yes | 數(shù)字>=min.value |
| 大小 | @Max(value=0L) | BigDecimal, BigInteger, byte, short, int, long | Yes | 數(shù)字<=max.value |
| 大小 | @Range(min=0L, max=Long.MAX_VALUE) | BigDecimal, BigInteger, byte, short, int, long | Yes | range.min<=數(shù)字<=range.max |
| 大小 | @DecimalMin(value="") | BigDecimal, BigInteger, CharSequence, byte, short, int, long | Yes | 數(shù)字>=decimalMin.value |
| 大小 | @DecimalMax(value="") | BigDecimal, BigInteger, CharSequence, byte, short, int, long | Yes | 數(shù)字<=decimalMax.value |
| 日期 | @Past |
|
Yes | 時間在當(dāng)前時間之前 |
| 日期 | @PastOrPresent | 同上 | Yes | 時間在當(dāng)前時間之前 或者等于此時 |
| 日期 | @Future | 同上 | Yes | 時間在當(dāng)前時間之后 |
| 日期 | @FutureOrPresent | 同上 | Yes | 時間在當(dāng)前時間之后 或者等于此時 |
| 格式 | @Pattern(regexp="", flags={}) | CharSequence | Yes | 匹配正則表達式 |
| 格式 | @Email @Email(regexp=".*", flags={}) |
CharSequence | Yes | 匹配郵箱格式 |
| 格式 | @Digts(integer=0, fraction=0) | BigDecimal, BigInteger, CharSequence, byte, short, int, long | Yes | 必須是數(shù)字類型,且滿足整數(shù)位數(shù)<=digits.integer, 浮點位數(shù)<=digits.fraction |
| 布爾 | @AssertTrue | boolean | Yes | 必須是true |
| 布爾 | @AssertFalse | boolean | Yes | 必須是false |
注: 后續(xù)還需補充Hibernate Validator中實現(xiàn)的constraints注解,如表中@Range。
對象集成constraints示例
/**
* 用戶 - DTO
*
* @author luohq
* @date 2021-09-04 13:45
*/
public class UserDto {
@NotNull(groups = Update.class)
@Positive
private Long id;
@NotBlank
@Size(max = 32)
private String name;
@NotNull
@Range(min = 1, max = 2)
private Integer sex;
@NotBlank
@Pattern(regexp = "^\\d{8,11}$")
private String phone;
@NotNull
@Email
private String mail;
@NotNull
@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$")
private String birthDateStr;
@NotNull
@PastOrPresent
private LocalDate birthLocalDate;
@NotNull
@Past
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime registerLocalDatetime;
@Valid
@NotEmpty
private List<OrgDto> orgs;
//省略getter、setter、toString方法
}
/**
* 組織 - DTO
*
* @author luohq
* @date 2021-09-04 14:10
*/
public class OrgDto {
@NotNull
@Positive
private Long orgId;
@NotBlank
@Size(min = 1, max = 32)
private String orgName;
//省略getter、setter、toString方法
}
注:
- 可通過constraints注解的groups指定分組
即指定constraints僅在指定group生效,默認(rèn)均為Default分組,
后續(xù)可通過@Validated({MyGroupInterface.class})形式進行分組的指定 - 可通過@Valid注解進行級聯(lián)驗證(Cascaded Validation,即嵌套對象驗證)
如上示例中@Valid添加在 List<OrgDto> orgs上,即會對list中的每個OrgDto進行驗證
SpringBoot集成自動驗證
參考:
https://www.baeldung.com/javax-validation-method-constraints#validation
集成maven依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
驗證RequestBody、Form對象參數(shù)
在參數(shù)前加@Validated

驗證簡單參數(shù)
在controller類上加@Validated

驗證指定分組

全局controller驗證異常處理
通過@ControllerAdvice、@ExceptionHandler來對SpringBoot Validation驗證框架拋出的異常進行統(tǒng)一處理,
并將錯誤信息拼接后統(tǒng)一返回,具體處理代碼如下:
import com.luo.demo.validation.domain.result.CommonResult;
import com.luo.demo.validation.enums.RespCodeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* controller增強 - 通用異常處理
*
* @author luohq
* @date 2021-09-04 13:43
*/
@ControllerAdvice
public class ControllerAdviceHandler {
private static final Logger log = LoggerFactory.getLogger(ControllerAdviceHandler.class);
/**
* 是否在響應(yīng)結(jié)果中展示驗證錯誤提示信息
*/
@Value("${spring.validation.msg.enable:true}")
private Boolean enableValidationMsg;
/**
* 符號常量
*/
private final String DOT = ".";
private final String SEPARATOR_COMMA = ", ";
private final String SEPARATOR_COLON = ": ";
/**
* 驗證異常處理 - 在@RequestBody上添加@Validated處觸發(fā)
*
* @param request
* @param ex
* @return
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public CommonResult handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException ex) {
log.warn("{} - MethodArgumentNotValidException!", request.getServletPath());
CommonResult commonResult = CommonResult.respWith(RespCodeEnum.PARAM_INVALID.getCode(), this.convertFiledErrors(ex.getBindingResult().getFieldErrors()));
log.warn("{} - resp with param invalid: {}", request.getServletPath(), commonResult);
return commonResult;
}
/**
* 驗證異常處理 - form參數(shù)(對象參數(shù),沒有加@RequestBody)觸發(fā)
*
* @param request
* @param ex
* @return
*/
@ExceptionHandler({BindException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public CommonResult handleBindException(HttpServletRequest request, BindException ex) {
log.warn("{} - BindException!", request.getServletPath());
CommonResult commonResult = CommonResult.respWith(RespCodeEnum.PARAM_INVALID.getCode(), this.convertFiledErrors(ex.getFieldErrors()));
log.warn("{} - resp with param invalid: {}", request.getServletPath(), commonResult);
return commonResult;
}
/**
* 驗證異常處理 - @Validated加在controller類上,
* 且在參數(shù)列表中直接指定constraints時觸發(fā)
*
* @param request
* @param ex
* @return
*/
@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public CommonResult handleConstraintViolationException(HttpServletRequest request, ConstraintViolationException ex) {
log.warn("{} - ConstraintViolationException - {}", request.getServletPath(), ex.getMessage());
CommonResult commonResult = CommonResult.respWith(RespCodeEnum.PARAM_INVALID.getCode(), this.convertConstraintViolations(ex));
log.warn("{} - resp with param invalid: {}", request.getServletPath(), commonResult);
return commonResult;
}
/**
* 全局默認(rèn)異常處理
*
* @param request
* @param ex
* @return
*/
@ExceptionHandler({Throwable.class})
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public CommonResult handleException(HttpServletRequest request, Throwable ex) {
log.warn("{} - Exception!", request.getServletPath(), ex);
CommonResult commonResult = CommonResult.failed();
log.warn("{} - resp failed: {}", request.getServletPath(), commonResult);
return commonResult;
}
/**
* 轉(zhuǎn)換FieldError列表為錯誤提示信息
*
* @param fieldErrors
* @return
*/
private String convertFiledErrors(List<FieldError> fieldErrors) {
return Optional.ofNullable(fieldErrors)
.filter(fieldErrorsInner -> this.enableValidationMsg)
.map(fieldErrorsInner -> fieldErrorsInner.stream()
.flatMap(fieldError -> Stream.of(fieldError.getField(), SEPARATOR_COLON, fieldError.getDefaultMessage(), SEPARATOR_COMMA))
.collect(Collectors.joining()))
.map(msg -> msg.substring(0, msg.length() - SEPARATOR_COMMA.length()))
.orElse(null);
}
/**
* 轉(zhuǎn)換ConstraintViolationException異常為錯誤提示信息
*
* @param constraintViolationException
* @return
*/
private String convertConstraintViolations(ConstraintViolationException constraintViolationException) {
return Optional.ofNullable(constraintViolationException.getConstraintViolations())
.filter(constraintViolations -> this.enableValidationMsg)
.map(constraintViolations -> constraintViolations.stream()
.flatMap(constraintViolation -> {
String path = constraintViolation.getPropertyPath().toString();
path = path.substring(path.lastIndexOf(DOT) + 1);
String errMsg = constraintViolation.getMessage();
return Stream.of(path, SEPARATOR_COLON, errMsg, SEPARATOR_COMMA);
}).collect(Collectors.joining())
).map(msg -> msg.substring(0, msg.length() - SEPARATOR_COMMA.length()))
.orElse(null);
}
}
參數(shù)驗證未通過返回結(jié)果示例:

注: 其中CommonResult為統(tǒng)一返回結(jié)果,可根據(jù)自己業(yè)務(wù)進行調(diào)整

自定義constraints
自定義field constraint注解主要分為以下幾步:
(1)定義constraint annotation注解及其屬性
(2)通過注解的元注解@Constraint(validatedBy = {})關(guān)聯(lián)的具體的驗證器實現(xiàn)
(3)實現(xiàn)驗證器邏輯
@DateFormat
具體字符串日期格式constraint @DateFormat定義示例如下:
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
/**
* The annotated {@code CharSequence} must match date format.
* The default date format is "yyyy-MM-dd".
* Can override with property "format".
* see {@link java.time.format.DateTimeFormatter}.
* <p>
* Accepts {@code CharSequence}. {@code null} elements are considered valid.
*
* @author luo
* @date 2021-09-05
*/
@Documented
@Constraint(validatedBy = DateFormatValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD, ANNOTATION_TYPE,})
@Retention(RetentionPolicy.RUNTIME)
public @interface DateFormat {
String message() default "日期格式不正確";
String format() default "yyyy-MM-dd";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
import org.springframework.util.StringUtils;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.time.format.DateTimeFormatter;
/**
* Date Format validator
*
* @author luohq
* @date 2021-09-05
*/
public class DateFormatValidator implements ConstraintValidator<DateFormat, String> {
private String format;
@Override
public void initialize(DateFormat dateFormat) {
this.format = dateFormat.format();
}
@Override
public boolean isValid(String dateStr, ConstraintValidatorContext cxt) {
if (!StringUtils.hasText(dateStr)) {
return true;
}
try {
DateTimeFormatter.ofPattern(this.format).parse(dateStr);
return true;
} catch (Throwable ex) {
return false;
}
}
}
@PhoneNo
在查看hbernate-validator中URL、Email約束實現(xiàn)時,發(fā)現(xiàn)可以通過元注解的形式去復(fù)用constraint實現(xiàn)(如@Pattern),故參考如上方式實現(xiàn)@PhoneNo約束
import javax.validation.Constraint;
import javax.validation.OverridesAttribute;
import javax.validation.Payload;
import javax.validation.ReportAsSingleViolation;
import javax.validation.constraints.Pattern;
import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* The annotated {@code CharSequence} must match phone no format.
* The regular expression follows the Java regular expression conventions
* see {@link java.util.regex.Pattern}.
* <p>
* Accepts {@code CharSequence}. {@code null} elements are considered valid.
*
* @author luo
* @date 2021-09-05
*/
@Documented
@Constraint(validatedBy = {})
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Repeatable(PhoneNo.List.class)
@ReportAsSingleViolation
@Pattern(regexp = "")
public @interface PhoneNo {
String message() default "電話號碼格式不正確";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* @return an additional regular expression the annotated PhoneNo must match. The default is "^\\d{8,11}$"
*/
@OverridesAttribute(constraint = Pattern.class, name = "regexp") String regexp() default "^\\d{8,11}$";
/**
* @return used in combination with {@link #regexp()} in order to specify a regular expression option
*/
@OverridesAttribute(constraint = Pattern.class, name = "flags") Pattern.Flag[] flags() default {};
/**
* Defines several {@code @URL} annotations on the same element.
*/
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
@Retention(RUNTIME)
@Documented
@interface List {
PhoneNo[] value();
}
}
注: 同理可以實現(xiàn)@IdNo約束
使用自定義constraint注解
可將之前的對象集成示例中代碼調(diào)整為使用自定義驗證注解如下:
/**
* 用戶 - DTO
*
* @author luohq
* @date 2021-09-04 13:45
*/
public class UserDto {
...
@NotBlank
//@Pattern(regexp = "^\\d{8,11}$")
@PhoneNo
private String phone;
@NotBlank
@IdNo
private String idNo;
@NotNull
//@Pattern(regexp = "^\\d{4}-\\d{2}-\\d{2}$")
@DateFormat
//@DateTimeFormat
private String birthDateStr;
...
}
同時自定義constraints還支持跨多參數(shù)、驗證對象里的多個field、驗證返回對象等用法,待后續(xù)再詳細(xì)探索。
問題
通過在對象屬性、方法參數(shù)上標(biāo)注注解的形式,需要侵入代碼,之前有的架構(gòu)師不喜歡這種風(fēng)格。
在一方開發(fā)時,我們有全部源碼且在公司內(nèi)部,這種方式還是可以的,且集成比較方便,
但是依賴三方Api jar包(參數(shù)對象定義在jar包中),我們無法直接去修改參數(shù)對象,依舊使用這種侵入代碼的注解方式就不適用了,
針對三方包、或者替代注解這種形式,之前公司內(nèi)部有實現(xiàn)過基于xml配置的形式進行驗證,
這種方式不侵入?yún)?shù)對象,且集成也還算方便,
但是用起來還是沒有直接在代碼里寫注解來的順手(代碼有補全、有提示、程序員友好),
所以一方開發(fā)時,首選推薦SpringBoot Validation這套體系,無法直接編輯參數(shù)對象時再考慮其他方式。
參考:
【自定義validator - field、class level】https://www.baeldung.com/spring-mvc-custom-validator
【Spring boot集成validation、全局異常處理】https://www.baeldung.com/spring-boot-bean-validation
【JSR380、非Spring框架集成validation】https://www.baeldung.com/javax-validation
Spring Validation最佳實踐及其實現(xiàn)原理,參數(shù)校驗沒那么簡單!
https://reflectoring.io/bean-validation-with-spring-boot/
到此這篇關(guān)于Java SpringBoot Validation用法案例詳解的文章就介紹到這了,更多相關(guān)Java SpringBoot Validation用法內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Windows環(huán)境下重啟jar服務(wù)bat代碼的解決方案
在Windows環(huán)境下部署java的jar包,若有多個服務(wù)同時啟動,很難找到相應(yīng)服務(wù)重啟,每次都重啟全部服務(wù)很麻煩,應(yīng)用場景大多用于部署測試,今天給大家分享Windows環(huán)境下重啟jar服務(wù)bat代碼,感興趣的朋友一起看看吧2023-08-08
Java并發(fā)系列之AbstractQueuedSynchronizer源碼分析(條件隊列)
這篇文章主要為大家詳細(xì)介紹了Java并發(fā)系列之AbstractQueuedSynchronizer源碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-02-02
SpringBoot實現(xiàn)WEB的常用功能案例詳解
這篇文章主要介紹了SpringBoot實現(xiàn)WEB的常用功能,本文將對Spring Boot實現(xiàn)Web開發(fā)中涉及的三大組件Servlet、Filter、Listener以及文件上傳下載功能以及打包部署進行實現(xiàn),需要的朋友可以參考下2022-04-04
基于SpringBoot+Redis實現(xiàn)分布式鎖
本文主要介紹了基于SpringBoot+Redis實現(xiàn)分布式鎖,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05
詳細(xì)聊一聊java語言中的package和import機制
這篇文章主要給大家介紹了關(guān)于java語言中package和import機制的相關(guān)資料,Java中的package是指將相關(guān)的類組織在一起的一種機制,它可以用來避免命名沖突,也可以方便地管理和維護代碼,需要的朋友可以參考下2024-01-01

