不說廢話開頭先給出maven依賴,寫教程不貼出依賴趕緊都是耍流氓。
<!-- 屬性效驗-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
用場景說話
場景一:前端傳過來的字段如何在后台做效驗,最老的方法就是if else顯得不是很靈活。如果前端傳來100個字段就得寫許多多余的代碼。
第一個場景就是在后台創建的實體和前端傳來的字段做對應映射,加上JSR303注解來做靈活的效驗
1:給Bean實體添加校驗注解:javax.validation.constraints(大部分注解都在這個包下),並定義自己的message提示如下:
@NotBlank(message = "品牌名不能為空") private String name; @NotNull(message = "顯示狀態不能為空") private Integer showStatus;
注意:@NotBlank這個注解效驗非空是只能效驗字符串而@NotNull可以效驗所有。所以我這里單獨特意拿這兩個做演示
2:開啟校驗功能@Validated很多人問為什么不用@Valid其實這個也可以但是不利於后面擴展功能。@Validated是在@Valid上又封裝了一層,
比如加上了分組功能這個后面說 (效果:校驗錯誤以后會有默認的響應;)在給校驗的bean后緊跟一個BindingResult,就可以獲取到校驗的結果
示列Controller:
/** * 保存 */ @RequestMapping("/save") //@RequiresPermissions("product:brand:save") public R save(@Validated @RequestBody BrandEntity brand, BindingResult result) { /** * result.hasErrors()如果前端傳來的值和在實體類標注的注解不對應就返回false。 * 比如在實體在name字段標注了@NotBlank(message = "品牌名不能為空")而前端沒傳name就返回了false */ if (result.hasErrors()) { Map<String, String> map = new HashMap<>(); //1、result.getFieldErrors()獲取校驗的錯誤結果對象然后遍歷 for (FieldError fieldError : result.getFieldErrors()) { //FieldError 獲取到錯誤提示 String message = fieldError.getDefaultMessage(); //獲取錯誤的屬性的名字 String field = fieldError.getField(); map.put(field, message); } return R.error(400, "提交的數據不合法").put("data", map); } else { brandService.save(brand); } return R.ok(); }
示列實體
import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; public class Test { /** * 品牌名 */ @NotBlank(message = "品牌名不能為空") private String name; /** * 顯示狀態[0-不顯示;1-顯示] */ @NotNull(message = "顯示狀態不能為空") private Integer showStatus; }
效果演示1:什么參數都不傳的
效果演示:只傳一個
效果演示:全部參數都傳
場景二:綜合以上場景我們知道了如何效驗和捕獲錯誤信息然后返回給前端,但接下來就遇到了一個問題。
如果按照這樣的寫法就會在每個方法都寫判斷和捕獲消息遍歷的代碼。母庸質疑這是愚蠢人的做法,接下來這個場景就解決這種愚蠢人的做法
可能很多小伙伴已經想到了全局異常捕獲,沒錯就是全局異常捕獲。首先我們得知道前端傳來的值和我們效驗的規則不一樣然后拋出的是什么異常。
MethodArgumentNotValidException沒錯就是這樣異常你們可以自己去試我就不再演示了,具體開接下來完整代碼列子
import com.weikang.common.exception.BizCodeEnume; import com.weikang.common.utils.R; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindingResult; 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.RestControllerAdvice; import org.springframework.web.servlet.ModelAndView; import java.util.HashMap; import java.util.Map; /** * 集中處理所有異常 */ @Slf4j //@ResponseBody //@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller") //RestControllerAdvice這個注解的意思是ControllerAdvice增加實現了 // ResponseBody返回全 // 異常處理 //全局數據綁定 //全局數據預處理 //basePackages = "com.weikang.gulimall.product.controller"掃描需要捕獲的控制層 @RestControllerAdvice(basePackages = "com.weikang.gulimall.product.controller") public class GulimallExceptionControllerAdvice { /** * 寫應該針對某些異常的方法來捕獲MethodArgumentNotValidException * 這個就是數據效驗拋出的異常我們來進行捕獲 * @ExceptionHandler用這個注解把需要捕獲的異常類給定義上 */ @ExceptionHandler(value= MethodArgumentNotValidException.class) public R handleVaildException(MethodArgumentNotValidException e){ log.error("數據校驗出現問題{},異常類型:{}",e.getMessage(),e.getClass()); //通過異常類的這個方法getBindingResult()可以得到BindingResult得到了這個就還是和之前在控制層的操作一樣了 BindingResult bindingResult = e.getBindingResult(); //遍歷循環我這是流遍歷大家都懂的 Map<String,String> errorMap = new HashMap<>(); bindingResult.getFieldErrors().forEach((fieldError)->{ errorMap.put(fieldError.getField(),fieldError.getDefaultMessage()); }); //統一返回我這用到了枚舉哈哈哈大家可以根據自己方便來我是為了規范 return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap); } }
場景三:以上兩個場景我們解決了統一處理前端傳來的字段效驗和統一處理了返回效驗的錯誤消息提示返回。接下來一個場景就是這樣的。
比如我們添加一個賬戶有name,age,sex這些字段必填然后在實體類加上了效驗注解都現在了這些字段必填。接下來我們又有一個
修改接口。根據id修改name。前端只傳了id和name這樣的話我們在實體標注了很多效驗注解這樣就已經沖突了。接下來我們
就用分組效驗來解決這種場景。意思是在添加的時候我們使用添加分組的效驗注解,
在修改的時候使用修改效驗的注解這樣就不會沖突(分組校驗(多場景的復雜校驗)
1:做分組效驗第一步就是@NotBlank(message = "品牌名必須提交",groups = {AddGroup.class})加上groups = {AddGroup.class}
這個屬性AddGroup.class這個類是什么呢,其實這個是我們自己定義的一個接口來標注這個屬於什么分組
這樣既可
2:第二步我們就在控制層方法上來定義使用這個分組
注意:在方法上標記了這個分組后,就同樣只會效驗在實體類標記了這個分組的字段會對應上,如果實體類沒標記這個分組就不會效驗。
同理如果修改也想效驗這個字段就在定義一個修改分組
最后效果就是這樣了在實體類標注了添加分組和修改分組這樣的話無論是添加的時候還是修改的時候都會效驗這個字段了,如果只加上添加分組不添加修改分組的話這個就只會在添加上起作用
場景四:針對以上方案我們基本滿足了大部分場景接下來有一個場景就是showStatus字段狀態判段1代表顯示0代表不顯示接下來我們用什么注解呢。大家都想得到的是用以下注解
@Pattern(regexp="^[a-zAz]$",message = "檢索首字母必須是一個字母",groups={AddGroup.class,UpdateGroup.class})
這個注解可以在里面寫正則表達式,但是當我們遇到以下比較復雜的邏輯判斷的時候這個注解也會滿足不了我們需求,
所以接下來我們來實現一個自定義注解來處理我們業務需求
自定義校驗步驟:
1:編寫一個自定義的校驗注解,怎樣編寫呢嗯來看看jsr303官方寫的我們就模仿他們,我們就以@NotNull來模仿看以下參數圖
通過以上官方JSR303規范我們可以模仿寫應該我們自己的效驗器注解
2:在我們實體類上標注上我們自定義的這個注解
@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class},message = "顯示狀態只能為0或者1") private Integer showStatus;
我們自定義注解上是沒有定義注解值的vals={0,1}所以我們在去寫一個來做效驗
import javax.validation.Constraint; import javax.validation.Payload; import javax.validation.constraints.NotNull; 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.ElementType.PARAMETER; import static java.lang.annotation.ElementType.TYPE_USE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {ListValueConstraintValidator.class }) //自己編寫的注解效驗器 public @interface ListValue { String message() default "{com.weikang.common.valid.ListValue.message}"; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; int[] vals() default { }; }
看上面代碼我寫了ListValueConstraintValidator.class自定義效驗器所以我們去定義一個來做效驗讓自定義注解和自定義效驗器關聯上
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.lang.annotation.Annotation; import java.util.HashSet; import java.util.Set; /** * implements ConstraintValidator<ListValue,Integer>實現這個接口,但是這個接口有兩個泛型 * 一個是我們自定義的注解另一個是在自定義注解里面定義的效驗值的類型 */ public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> { Set<Integer> val=new HashSet<Integer>(); @Override public void initialize(ListValue constraintAnnotation) { int[] vals = constraintAnnotation.vals(); //這個我們可以取到所有在注解上填寫的條件然后我們循環放進set for (int i : vals) { val.add(i); } } @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { return val.contains(value); //包含返回true不包含返回false } }
結束end