Spring @Valid
@Valid基本用法
強烈推薦如果要學習@Valid JSR303, 建議看這裡的API Bean Validation規範 !
Controller控制器中在需要校驗的實體類上新增 @Valid 即可使用JSR303校驗(前提記得新增hibernate-validator相關jar,<mvc:annotation-driven/>);
modelMap是為了將校驗失敗資訊寫回到request屬性中返回給JSP頁面展示
@RequestMapping("/demo2") public String test2(@Valid User user, BindingResult result, ModelMap modelMap){ System.out.println(user); List<FieldError> fieldErrors = result.getFieldErrors(); for (FieldError e:fieldErrors) { System.out.println(e.getDefaultMessage());//驗證不通過的資訊 System.out.println(e.getField()); modelMap.addAttribute(e.getField(),e.getDefaultMessage()); } return "test"; }
校驗的實體類User
@Setter @Getter @ToString public class User { @NotBlank private String name; @Min(1) @Max(120) private int age; public User(String name, int age) { this.name = name; this.age = age; } public User() { } }
瀏覽器輸入localhost:8090/binding/demo2?name=lvbinbin&age=150, 結果校驗不通過
從上述用例看出來,我們沒有指定message屬性,預設校驗不通過的提示訊息 最大不能超過120 , 該資訊是在hibernate-Validator.jar的ValidationMessages.properties中定義;
如果想要自定義校驗不通過資訊,我們可以指定message屬性
@Min(value = 1,message = "年齡大於一歲") @Max(value = 120,message = "常人活不到120歲") private int age;
突然考慮到問題,國際化的問題由於對國際化沒有過了解,我理解的國際化問題就是,請求頭資訊包含的地區資訊Accpet-Language可以判斷當前需要中文還是英文,於是有了下面進一步的改善;
Hibernate預設會查詢classPath下的ValidationMessages.properties檔案,我們只需要將國際化校驗檔案在classpath下新增即可。
classpath下新增ValidationMessages_en.properties (英文校驗失敗資訊)
myValidation.min=can not be lower than {value} myValidation.max=can not be bigger than {value} age=age
classpath下新增ValidationMessages_zh.properties (中文校驗失敗資訊)
myValidation.min=不能小於{value} myValidation.max=不能大於{value} age=年齡
在註解驗證的message屬性用{}來取ValidationMessages中的值
@Min(value = 1,message = "{age}{myValidation.min}") @Max(value = 120,message = "{age}{myValidation.max}") private int age;
使用POSTMAN模擬中文、英文測試一下:
英文測試:請求頭Accpet-Language:en-Us , 結果的確是英文
中文測試:請求頭Accpet-Language:zh-CN, 結果發現亂碼問題
亂碼問題解決方案:自定義Validator註冊到SpringMvc中,指定國際化資原始檔編碼為UTF-8
<mvc:annotation-driven validator="validator"/> <bean id="validator" class="org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean"> <property name="validationMessageSource" ref="messageSource"/> <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/> </bean> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basenames"> <list> <value>classpath:ValidationMessages</value><!--國際化資源地址--> </list> </property> <property name="defaultEncoding" value="UTF-8"/> <property name="cacheSeconds" value="120"/> </bean>
再次測試中文,就不存在問題,同樣中文也是沒有問題的
校驗不通過返回給前端兩種方案
方案一.存到request屬性中,在前端檢視JSP 等渲染
@RequestMapping("/demo2") public String test2(@Valid User user, BindingResult result, ModelMap modelMap){ System.out.println(user); List<FieldError> fieldErrors = result.getFieldErrors(); for (FieldError e:fieldErrors) { System.out.println(e.getDefaultMessage());//驗證不通過的資訊 System.out.println(e.getField()); modelMap.addAttribute(e.getField(),e.getDefaultMessage()); } return "test"; }
方案二.校驗不通過返回異常資訊JSON串給前端
通過檢視丟擲異常資訊,Spring4.3.0校驗@Valid不通過丟擲異常資訊為BindException,捕獲該種異常返回JSON,異常捕獲方式見我的部落格。
@ExceptionHandler(value = {BindException.class}) public ResponseEntity invalidArgument(BindException ex){ Map result=new HashMap<String,Object>(); result.put("status_code",500); System.out.println("捕獲到異常"); List<FieldError> fieldErrors = ex.getFieldErrors(); StringBuffer sb=new StringBuffer(); for (FieldError error:fieldErrors) { sb.append(error.getDefaultMessage()); } result.put("message",sb.toString()); return new ResponseEntity(result, HttpStatus.INTERNAL_SERVER_ERROR); }
補充說明:@RequestMapping方法中你寫了引數 BindingResult就代表告訴Spring 我自己來處理異常,你別管了,這種情況程式不會丟擲異常;所以方式一程式是不會丟擲異常。
順帶提及Spring擴充套件JSR303的註解@Validated
個人對於為什麼會存在@Validated註解的看法:
@Valid功能很豐富,有幸搜尋到這樣一篇典範API Bean Validation技術規範 ,弊病是@Valid的組、組順序功能,需要對Spring、JavaxValidation有一定基礎,不夠簡易上手,在此基礎上Spring封裝了@Validated來完成 組校驗、組順序校驗的功能,我們只需要一個@Validated(value={xxx.class})即可指定組,對於我們來說不能在方便了! 以上就是個人對於@Validated存在的合理性分析,這裡看來存在是合理的!
假設這樣一個情景介紹@Validate 裡組的概念,也可以看Bean Validation技術規範裡的介紹;
@RequestMapping("/demo3") public String test3(@Validated Item item){ System.out.println(item); return "test"; } @ExceptionHandler(value = {BindException.class}) public ResponseEntity invalidArgument(BindException ex){ Map result=new HashMap<String,Object>(); result.put("status_code",500); System.out.println("捕獲到異常"); List<FieldError> fieldErrors = ex.getFieldErrors(); StringBuffer sb=new StringBuffer(); for (FieldError error:fieldErrors) { sb.append(error.getDefaultMessage()).append(","); } result.put("message",sb.substring(0,sb.length()-1)); return new ResponseEntity(result, HttpStatus.INTERNAL_SERVER_ERROR); }
校驗實體類Item
@Data @AllArgsConstructor @NoArgsConstructor public class Item { @NotBlank(message = "商品名稱不建議為空") private String name; @DecimalMin(value = "0.5",message = "商品價格小於0.5") private double price; @Past(message = "生產日期偽冒") @NotNull(message = "生產日期不能不報") private Date produceDate; }
嘗試不輸入任何屬性,果然三個校驗都沒有通過;
對了,有個日期型別引數,這裡就簡單用@InitBinder解決一下子吧,在@Controller裡新增方法:這樣就可以將String轉換成Date型別引數了.
@InitBinder public void registryStringToDate(DataBinder binder){ binder.registerCustomEditor(Date.class,new CustomDateEditor(new SimpleDateFormat("yyyy/MM/dd"),true)); }
再次測試,沒有問題了,我們就可以開始介紹 組校驗的方式了
比如現在只需要校驗商品名字,其他的價格、日期都不需要管了:
@Data @AllArgsConstructor @NoArgsConstructor public class Item { @NotBlank(message = "商品名稱不建議為空",groups = {ItemNameValid.class}) private String name; @DecimalMin(value = "0.5",message = "商品價格小於0.5",groups = {ItemPriceValid.class}) private double price; @Past(message = "生產日期偽冒") @NotNull(message = "生產日期不能不報") private Date produceDate; public static interface ItemNameValid{} public static interface ItemPriceValid extends ItemNameValid{} }
@Controller寫法:
@RequestMapping("/demo3") public String test3(@Validated({Item.ItemNameValid.class}) Item item){ System.out.println(item); return "test"; }
@Validated註解中value指定某個且必須是介面型別,ItemNameValid組校驗時候只校驗name屬性,ItemPriceValid 組校驗時候會校驗name和price組;
級聯驗證方式:
Item類新增屬性ItemProp
@Valid @NotNull(message=”產品屬性不能為空”) private ItemProp prop;
@Setter @Getter @NoArgsConstructor public class ItemProp { @Pattern(regexp = "^白色$",message = "小布丁只能是白色的") @NotNull private String color; @NotBlank(message = "如實填報產地") private String Location; }
注意:@Valid新增到級聯屬性上完成驗證,前提是: 如果級聯的屬性沒有初始化new,且是必須驗證的項,@Valid下面跟上@NotNull才能級聯驗證,否則根本不去校驗ItemProp屬性.
總結:@Valid和@Validated異同
@Valid可以用來作為級聯屬性校驗,@Validated沒這個功能;級聯校驗時Bean Validation的特性,而非Spring特性.
@Validated擴充套件JSR303,可以用來指定校驗組驗證,且只見過標註在@RequestMapping方法需要校驗的入參中;
除了使用Bean Validation規範來完成JavaBean校驗,Spring另外提供一個介面Validator,供我們實現複雜校驗邏輯。 下面完成了一個簡單的Person入參校驗,使用Spring的Validator實現
@Controller @RequestMapping("/valid") public class ValidateController { @RequestMapping("/demo1") public String demo1(@Valid Person person){//此處@valid不能省略,@Validated也一樣使用,作用標識person開啟校驗 System.out.println(person); return "test"; } @InitBinder public void register(DataBinder binder){ binder.setValidator(new PersonValidator());//替換原有validator; //binder.addValidators(new PersonValidator()); //在原有validator基礎上新增 } @Setter @Getter @ToString @NoArgsConstructor private static class Person{ String name; int age; } privatestatic class PersonValidator implements Validator{ @Override public boolean supports(Class<?> clazz) { System.out.println(clazz==Person.class); return clazz==Person.class; } @Override public void validate(Object target, Errors errors) { //validate手動就需要校驗 System.out.println("validate"); Person person = (Person) target; if (null==person.getName()||person.getName().isEmpty()) { errors.rejectValue("name", "field.empty",new Object[]{person.getName()}, "使用者名稱不得為空"); } if(person.getAge()==0||person.getAge()>150){ errors.rejectValue("age", "field.max",new Object[]{person.getAge()}, "使用者年齡虛假"); } } } //異常捕獲,目的:返回JSON給前端,可以設定成全域性的異常捕獲結合@ControllerAdvice @ExceptionHandler(value = {BindException.class}) public ResponseEntity invalidArgument(BindException ex){ Map result=new HashMap<String,Object>(); result.put("status_code",500); System.out.println("捕獲到異常"); List<FieldError> fieldErrors = ex.getFieldErrors(); StringBuffer sb=new StringBuffer(); for (FieldError error:fieldErrors) { sb.append(error.getField()+":"+error.getDefaultMessage()).append(","); } result.put("message",sb.substring(0,sb.length()-1)); return new ResponseEntity(result, HttpStatus.INTERNAL_SERVER_ERROR); } }
測試效果圖:
說明:其中需要注意如果多種引數校驗,一定要注意binder.addValidator(….)方法,它只是新增自定義的Validator實現類,比如一個實體類有很多String欄位,避免在Validator實現類中重複地判斷不為空,結合@NotEmpty等會節約篇幅,有利於程式碼整潔。