@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);
- }
- }
测试效果图:
说明: 其中需要注意如果多种参数校验, 一定要注意 binder.addValidator(....)方法, 它只是添加自定义的 Validator 实现类, 比如一个实体类有很多 String 字段, 避免在 Validator 实现类中重复地判断不为空, 结合 @NotEmpty 等会节约篇幅, 有利于代码整洁.
来源: https://www.cnblogs.com/lvbinbin2yujie/p/10611144.html