Spring @Valid

Stella981
• 阅读 591

@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, 结果校验不通过

Spring @Valid

从上述用例看出来,我们没有指定message属性,默认校验不通过的提示消息  最大不能超过120  , 该信息是在hibernate-Validator.jar的ValidationMessages.properties中定义;

如果想要自定义校验不通过信息,我们可以指定message属性

@Min(value = 1,message = "年龄大于一岁")
    @Max(value = 120,message = "常人活不到120岁")
    private int age;

Spring @Valid

突然考虑到问题,国际化的问题由于对国际化没有过了解,我理解的国际化问题就是,请求头信息包含的地区信息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 , 结果的确是英文

Spring @Valid

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

Spring @Valid

乱码问题解决方案:自定义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>

再次测试中文,就不存在问题,同样中文也是没有问题的

Spring @Valid

校验不通过返回给前端两种方案

方案一.存到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;

}

尝试不输入任何属性,果然三个校验都没有通过;

Spring @Valid

对了,有个日期类型参数,这里就简单用@InitBinder解决一下子吧,在@Controller里添加方法:这样就可以将String转换成Date类型参数了.

@InitBinder
    public void registryStringToDate(DataBinder binder){
        binder.registerCustomEditor(Date.class,new CustomDateEditor(new SimpleDateFormat("yyyy/MM/dd"),true));
}

再次测试,没有问题了,我们就可以开始介绍 组校验的方式了

Spring @Valid

比如现在只需要校验商品名字,其他的价格、日期都不需要管了:

@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组;

Spring @Valid

Spring @Valid

级联验证方式:

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;
}

Spring @Valid

Spring @Valid

注意:@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);
    }
}

测试效果图:

Spring @Valid

说明:其中需要注意如果多种参数校验,一定要注意binder.addValidator(….)方法,它只是添加自定义的Validator实现类,比如一个实体类有很多String字段,避免在Validator实现类中重复地判断不为空,结合@NotEmpty等会节约篇幅,有利于代码整洁。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这