@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;
}
private static 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);
}
}
測試效果圖:

