Java SpringBoot Validation用法案例詳解
提到輸入?yún)?shù)的基本驗證(非空、長度、大小、格式…),在以前我們還是通過手寫代碼,各種if、else、StringUtils.isEmpty、CollectionUtils.isEmpty…,真感覺快要瘋了,太繁瑣,Low爆了…,其實(shí)在Java生態(tài)提供了一套標(biāo)準(zhǔn)JSR-380(aka. Bean Validation 2.0,part of Jakarta EE and JavaSE),它已成為對象驗證事實(shí)上的標(biāo)準(zhǔn),這套標(biāo)準(zhǔn)可以通過注解的形式(如@NotNull, @Size…)來對bean的屬性進(jìn)行驗證。而Hibernate Validator對這套標(biāo)準(zhǔn)進(jìn)行了實(shí)現(xiàn),SpringBoot Validation無縫集成了Hibernate Validator、自定義驗證器、自動驗證的功能。下文將對SpringBoot集成Validation進(jìn)行展開。
注: 完整示例代碼可參見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 | 匹配正則表達(dá)式 |
格式 | @Email @Email(regexp=".*", flags={}) |
CharSequence | Yes | 匹配郵箱格式 |
格式 | @Digts(integer=0, fraction=0) | BigDecimal, BigInteger, CharSequence, byte, short, int, long | Yes | 必須是數(shù)字類型,且滿足整數(shù)位數(shù)<=digits.integer, 浮點(diǎn)位數(shù)<=digits.fraction |
布爾 | @AssertTrue | boolean | Yes | 必須是true |
布爾 | @AssertFalse | boolean | Yes | 必須是false |
注: 后續(xù)還需補(bǔ)充Hibernate Validator中實(shí)現(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})形式進(jìn)行分組的指定 - 可通過@Valid注解進(jìn)行級聯(lián)驗證(Cascaded Validation,即嵌套對象驗證)
如上示例中@Valid添加在 List<OrgDto> orgs上,即會對list中的每個OrgDto進(jìn)行驗證
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驗證框架拋出的異常進(jìn)行統(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增強(qiáng) - 通用異常處理 * * @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ù)進(jìn)行調(diào)整
自定義constraints
自定義field constraint注解主要分為以下幾步:
(1)定義constraint annotation注解及其屬性
(2)通過注解的元注解@Constraint(validatedBy = {})關(guān)聯(lián)的具體的驗證器實(shí)現(xiàn)
(3)實(shí)現(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約束實(shí)現(xiàn)時,發(fā)現(xiàn)可以通過元注解的形式去復(fù)用constraint實(shí)現(xiàn)(如@Pattern),故參考如上方式實(shí)現(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(); } }
注: 同理可以實(shí)現(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)部有實(shí)現(xiàn)過基于xml配置的形式進(jìn)行驗證,
這種方式不侵入?yún)?shù)對象,且集成也還算方便,
但是用起來還是沒有直接在代碼里寫注解來的順手(代碼有補(bǔ)全、有提示、程序員友好),
所以一方開發(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最佳實(shí)踐及其實(shí)現(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-08Java并發(fā)系列之AbstractQueuedSynchronizer源碼分析(條件隊列)
這篇文章主要為大家詳細(xì)介紹了Java并發(fā)系列之AbstractQueuedSynchronizer源碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-02-02java實(shí)現(xiàn)上傳圖片并壓縮圖片大小功能
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)上傳圖片并壓縮圖片大小功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05SpringBoot實(shí)現(xiàn)WEB的常用功能案例詳解
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)WEB的常用功能,本文將對Spring Boot實(shí)現(xiàn)Web開發(fā)中涉及的三大組件Servlet、Filter、Listener以及文件上傳下載功能以及打包部署進(jìn)行實(shí)現(xiàn),需要的朋友可以參考下2022-04-04基于SpringBoot+Redis實(shí)現(xiàn)分布式鎖
本文主要介紹了基于SpringBoot+Redis實(shí)現(xiàn)分布式鎖,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05詳細(xì)聊一聊java語言中的package和import機(jī)制
這篇文章主要給大家介紹了關(guān)于java語言中package和import機(jī)制的相關(guān)資料,Java中的package是指將相關(guān)的類組織在一起的一種機(jī)制,它可以用來避免命名沖突,也可以方便地管理和維護(hù)代碼,需要的朋友可以參考下2024-01-01