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, 結果校驗不通過

image

 

從上述用例看出來,我們沒有指定message屬性,默認校驗不通過的提示消息  最大不能超過120  , 該信息是在hibernate-Validator.jar的ValidationMessages.properties中定義;

如果想要自定義校驗不通過信息,我們可以指定message屬性

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

image

 

突然考慮到問題,國際化的問題由於對國際化沒有過了解,我理解的國際化問題就是,請求頭信息包含的地區信息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 , 結果的確是英文

image

 

中文測試:請求頭Accpet-Language:zh-CN, 結果發現亂碼問題

image

 

亂碼問題解決方案:自定義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>

 

再次測試中文,就不存在問題,同樣中文也是沒有問題的

image

 

 

校驗不通過返回給前端兩種方案

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

}

嘗試不輸入任何屬性,果然三個校驗都沒有通過;

image

 

對了,有個日期類型參數,這里就簡單用@InitBinder解決一下子吧,在@Controller里添加方法:這樣就可以將String轉換成Date類型參數了.

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

 

再次測試,沒有問題了,我們就可以開始介紹 組校驗的方式了

image

 

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

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

image

image

 

級聯驗證方式:

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

 

image

 

image

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

 

測試效果圖:

image

 

說明:其中需要注意如果多種參數校驗,一定要注意binder.addValidator(….)方法,它只是添加自定義的Validator實現類,比如一個實體類有很多String字段,避免在Validator實現類中重復地判斷不為空,結合@NotEmpty等會節約篇幅,有利於代碼整潔。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM