@Valid基本用法
强烈推荐如果要学习@Valid JSR303, 建议看这里的API Bean Validation规范 !
Controller控制器中在需要校验的实体类上添加 @Valid 即可使用JSR303校验(前提记得添加hibernate-validator相关jar,mvc:annotation-driven/);
modelMap是为了将校验失败信息写回到request属性中返回给JSP页面展示
1@RequestMapping("/demo2") 2 public String test2(@Valid User user, BindingResult result, ModelMap modelMap){ 3 System.out.println(user); 4 List<FieldError> fieldErrors = result.getFieldErrors(); 5 for (FieldError e:fieldErrors) { 6 System.out.println(e.getDefaultMessage()); //验证不通过的信息 7 System.out.println(e.getField()); 8 modelMap.addAttribute(e.getField(),e.getDefaultMessage()); 9 } 10 return "test"; 11}
校验的实体类User
1@Setter 2@Getter 3@ToString 4public class User { 5 @NotBlank 6 private String name; 7 @Min(1) 8 @Max(120) 9 private int age; 10 11 public User(String name, int age) { 12 this.name = name; 13 this.age = age; 14 } 15 16 public User() { 17 } 18}
浏览器输入localhost:8090/binding/demo2?name=lvbinbin&age=150, 结果校验不通过

从上述用例看出来,我们没有指定message属性,默认校验不通过的提示消息 最大不能超过120 , 该信息是在hibernate-Validator.jar的ValidationMessages.properties中定义;
如果想要自定义校验不通过信息,我们可以指定message属性
1@Min(value = 1,message = "年龄大于一岁") 2 @Max(value = 120,message = "常人活不到120岁") 3 private int age;

突然考虑到问题,国际化的问题由于对国际化没有过了解,我理解的国际化问题就是,请求头信息包含的地区信息Accpet-Language可以判断当前需要中文还是英文,于是有了下面进一步的改善;
Hibernate默认会查找classPath下的ValidationMessages.properties文件,我们只需要将国际化校验文件在classpath下添加即可。
classpath下添加ValidationMessages_en.properties (英文校验失败信息)
1myValidation.min=can not be lower than {value} 2myValidation.max=can not be bigger than {value} 3age=age
classpath下添加ValidationMessages_zh.properties (中文校验失败信息)
1myValidation.min=不能小于{value} 2myValidation.max=不能大于{value} 3age=年龄
在注解验证的message属性用{}来取ValidationMessages中的值
1@Min(value = 1,message = "{age}{myValidation.min}") 2 @Max(value = 120,message = "{age}{myValidation.max}") 3 private int age;
使用POSTMAN模拟中文、英文测试一下:
英文测试:请求头Accpet-Language:en-Us , 结果的确是英文

中文测试:请求头Accpet-Language:zh-CN, 结果发现乱码问题

乱码问题解决方案:自定义Validator注册到SpringMvc中,指定国际化资源文件编码为UTF-8
1<mvc:annotation-driven validator="validator"/> 2 3 <bean id="validator" class="org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean"> 4 <property name="validationMessageSource" ref="messageSource"/> 5 <property name="providerClass" value="org.hibernate.validator.HibernateValidator"/> 6 </bean> 7 8 <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> 9 <property name="basenames"> 10 <list> 11 <value>classpath:ValidationMessages</value><!--国际化资源地址--> 12 </list> 13 </property> 14 <property name="defaultEncoding" value="UTF-8"/> 15 <property name="cacheSeconds" value="120"/> 16 </bean>
再次测试中文,就不存在问题,同样中文也是没有问题的

校验不通过返回给前端两种方案
方案一.存到request属性中,在前端视图JSP 等渲染
1@RequestMapping("/demo2") 2 public String test2(@Valid User user, BindingResult result, ModelMap modelMap){ 3 System.out.println(user); 4 List<FieldError> fieldErrors = result.getFieldErrors(); 5 for (FieldError e:fieldErrors) { 6 System.out.println(e.getDefaultMessage()); //验证不通过的信息 7 System.out.println(e.getField()); 8 modelMap.addAttribute(e.getField(),e.getDefaultMessage()); 9 } 10 return "test"; 11}
方案二.校验不通过返回异常信息JSON串给前端
通过查看抛出异常信息,Spring4.3.0校验@Valid不通过抛出异常信息为BindException,捕获该种异常返回JSON,异常捕获方式见我的博客。
1@ExceptionHandler(value = {BindException.class}) 2 public ResponseEntity invalidArgument(BindException ex){ 3 Map result=new HashMap<String,Object>(); 4 result.put("status_code",500); 5 System.out.println("捕获到异常"); 6 List<FieldError> fieldErrors = ex.getFieldErrors(); 7 StringBuffer sb=new StringBuffer(); 8 for (FieldError error:fieldErrors) { 9 sb.append(error.getDefaultMessage()); 10 } 11 result.put("message",sb.toString()); 12 return new ResponseEntity(result, HttpStatus.INTERNAL_SERVER_ERROR); 13 }
补充说明:@RequestMapping方法中你写了参数 BindingResult就代表告诉Spring 我自己来处理异常,你别管了,这种情况程序不会抛出异常;所以方式一程序是不会抛出异常。
顺带提及Spring扩展JSR303的注解@Validated
个人对于为什么会存在@Validated注解的看法:
@Valid功能很丰富,有幸搜索到这样一篇典范API Bean Validation技术规范,弊病是@Valid的组、组顺序功能,需要对Spring、JavaxValidation有一定基础,不够简易上手,在此基础上Spring封装了@Validated来完成 组校验、组顺序校验的功能,我们只需要一个@Validated(value={xxx.class})即可指定组,对于我们来说不能在方便了! 以上就是个人对于@Validated存在的合理性分析,这里看来存在是合理的!
假设这样一个情景介绍@Validate 里组的概念,也可以看Bean Validation技术规范里的介绍;
1@RequestMapping("/demo3") 2 public String test3(@Validated Item item){ 3 System.out.println(item); 4 return "test"; 5 } 6 7 8 @ExceptionHandler(value = {BindException.class}) 9 public ResponseEntity invalidArgument(BindException ex){ 10 Map result=new HashMap<String,Object>(); 11 result.put("status_code",500); 12 System.out.println("捕获到异常"); 13 List<FieldError> fieldErrors = ex.getFieldErrors(); 14 StringBuffer sb=new StringBuffer(); 15 for (FieldError error:fieldErrors) { 16 sb.append(error.getDefaultMessage()).append(","); 17 } 18 19 result.put("message",sb.substring(0,sb.length()-1)); 20 return new ResponseEntity(result, HttpStatus.INTERNAL_SERVER_ERROR); 21 }
校验实体类Item
1@Data 2@AllArgsConstructor 3@NoArgsConstructor 4public class Item { 5 @NotBlank(message = "商品名称不建议为空") 6 private String name; 7 @DecimalMin(value = "0.5",message = "商品价格小于0.5") 8 private double price; 9 @Past(message = "生产日期伪冒") 10 @NotNull(message = "生产日期不能不报") 11 private Date produceDate; 12 13}
尝试不输入任何属性,果然三个校验都没有通过;

对了,有个日期类型参数,这里就简单用@InitBinder解决一下子吧,在@Controller里添加方法:这样就可以将String转换成Date类型参数了.
1@InitBinder 2 public void registryStringToDate(DataBinder binder){ 3 binder.registerCustomEditor(Date.class,new CustomDateEditor(new SimpleDateFormat("yyyy/MM/dd"),true)); 4}
再次测试,没有问题了,我们就可以开始介绍 组校验的方式了

比如现在只需要校验商品名字,其他的价格、日期都不需要管了:
1@Data 2@AllArgsConstructor 3@NoArgsConstructor 4public class Item { 5 @NotBlank(message = "商品名称不建议为空",groups = {ItemNameValid.class}) 6 private String name; 7 @DecimalMin(value = "0.5",message = "商品价格小于0.5",groups = {ItemPriceValid.class}) 8 private double price; 9 @Past(message = "生产日期伪冒") 10 @NotNull(message = "生产日期不能不报") 11 private Date produceDate; 12 13 public static interface ItemNameValid{} 14 15 public static interface ItemPriceValid extends ItemNameValid{} 16}
@Controller写法:
1@RequestMapping("/demo3") 2 public String test3(@Validated({Item.ItemNameValid.class}) Item item){ 3 System.out.println(item); 4 return "test"; 5 }
@Validated注解中value指定某个且必须是接口类型,ItemNameValid组校验时候只校验name属性,ItemPriceValid 组校验时候会校验name和price组;


级联验证方式:
Item类添加属性ItemProp
1@Valid 2 @NotNull(message=”产品属性不能为空”) 3 private ItemProp prop; 4 5@Setter 6@Getter 7@NoArgsConstructor 8public class ItemProp { 9 @Pattern(regexp = "^白色$",message = "小布丁只能是白色的") 10 @NotNull 11 private String color; 12 @NotBlank(message = "如实填报产地") 13 private String Location; 14}


注意:@Valid添加到级联属性上完成验证,前提是: 如果级联的属性没有初始化new,且是必须验证的项,@Valid下面跟上@NotNull才能级联验证,否则根本不去校验ItemProp属性.
总结:@Valid和@Validated异同
@Valid可以用来作为级联属性校验,@Validated没这个功能;级联校验时Bean Validation的特性,而非Spring特性.
@Validated扩展JSR303,可以用来指定校验组验证,且只见过标注在@RequestMapping方法需要校验的入参中;
除了使用Bean Validation规范来完成JavaBean校验,Spring另外提供一个接口Validator,供我们实现复杂校验逻辑。 下面完成了一个简单的Person入参校验,使用Spring的Validator实现
1@Controller 2@RequestMapping("/valid") 3public class ValidateController { 4 5 @RequestMapping("/demo1") 6 public String demo1(@Valid Person person){ //此处@valid不能省略,@Validated也一样使用,作用标识person开启校验 7 System.out.println(person); 8 return "test"; 9 } 10 11 @InitBinder 12 public void register(DataBinder binder){ 13 binder.setValidator(new PersonValidator());//替换原有validator; 14// binder.addValidators(new PersonValidator()); //在原有validator基础上添加 15 } 16 17 @Setter 18 @Getter 19 @ToString 20 @NoArgsConstructor 21 private static class Person{ 22 String name; 23 int age; 24 } 25 26 private static class PersonValidator implements Validator{ 27 @Override 28 public boolean supports(Class<?> clazz) { 29 System.out.println(clazz==Person.class); 30 return clazz==Person.class; 31 } 32 33 @Override 34 public void validate(Object target, Errors errors) { 35 //validate手动就需要校验 36 System.out.println("validate"); 37 Person person = (Person) target; 38 if (null==person.getName()||person.getName().isEmpty()) { 39 errors.rejectValue("name", "field.empty",new Object[]{person.getName()}, "用户名不得为空"); 40 } 41 if(person.getAge()==0||person.getAge()>150){ 42 errors.rejectValue("age", "field.max",new Object[]{person.getAge()}, "用户年龄虚假"); 43 } 44 45 } 46 } 47 //异常捕获,目的:返回JSON给前端,可以设置成全局的异常捕获结合@ControllerAdvice 48 @ExceptionHandler(value = {BindException.class}) 49 public ResponseEntity invalidArgument(BindException ex){ 50 Map result=new HashMap<String,Object>(); 51 result.put("status_code",500); 52 System.out.println("捕获到异常"); 53 List<FieldError> fieldErrors = ex.getFieldErrors(); 54 StringBuffer sb=new StringBuffer(); 55 for (FieldError error:fieldErrors) { 56 sb.append(error.getField()+":"+error.getDefaultMessage()).append(","); 57 } 58 59 result.put("message",sb.substring(0,sb.length()-1)); 60 return new ResponseEntity(result, HttpStatus.INTERNAL_SERVER_ERROR); 61 } 62}
测试效果图:
