程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> SpringMVC——類型轉換和格式化、數據校驗、客戶端顯示錯誤消息,springmvc校驗

SpringMVC——類型轉換和格式化、數據校驗、客戶端顯示錯誤消息,springmvc校驗

編輯:JAVA綜合教程

SpringMVC——類型轉換和格式化、數據校驗、客戶端顯示錯誤消息,springmvc校驗


在介紹類型轉換和格式化之前,我首先來介紹 <mvc:annotation-driven />。

需要導入的 schema:

xmlns:mvc="http://www.springframework.org/schema/mvc"

一、作用:

1.會自動注冊 RequestMappingHandlerMapping、RequestMappingHandlerAdapter 以及 ExceptionHandlerExceptionResolver 三個 Bean。

若配置該注解後,對於一般的 springmvc 請求來說,不再使用未配置之前的過期的 AnnotationHandlerMapping 和 AnnotationMethodHandlerAdapter。

而是使用 RequestMappingHandlerMapping、RequestMappingHandlerAdatapter,作為對 AnnotationHandlerMapping 和 AnnotationHandlerAdapter 的一種替代。。

所以 DispatcherServlet 中的 HandlerAdapter 的 handler() 方法發生了改變,相當於一套新的邏輯。

清晰內容,請參見:

AnnotationMethodHandlerAdapter 下的 springmvc 運行流程分析。

RequestMappingHandlerAdapter 下的 springmvc 運行流程分析。

2. 支持使用 ConversionService 實例對表單參數進行類型轉換。詳細內容請參見:類型轉換和格式化

3.支持使用 @Valid 對 java bean 進行 JSR-303 校驗。詳細內容請參見:數據校驗

4.支持使用 @RequestBody 和 @ResponseBody 注解。詳細內容請參見:springmvc 對 Ajax 的支持。

二、詳細分析

添加 <mvc:annotation-driven /> 配置後:

默認情況下:

HandlerMapping 注冊了 RequestMappingHandlerMapping 和 BeanNameUrlHandlerMapping。

HandlerAdapter 注冊了 RequestMappingHandlerAdapter 和 HttpReqestHandlerAdapter 和 SimpleControllerHandlerAdapter。

 

org.springframework.beans.factory.xml.BeanDefinitionParser 用來解析 <beans/> 標簽的。

<mvc:annotation-driven /> 屬於 <beans/> 的子節點,由 org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser進行解析。

這個類注冊了下面兩個 HandlerMapping:RequestMappingHandlerMapping 和 BeanNameUrlHandlerMapping。

此外,也可以通過 <mvc:resources mapping="" location=""/>或 <mvc:view-controller path=""/> 來指定需要被注冊的 HandlerMapping。

同時注冊了三個 HandlerAdatper:RequestMappingHandlerAdapter,HttpRequestHandlerAdapter,SimpleControllerHandlerAdapter。

同時注冊了三個 HandlerExceptionResolver:ExceptionHandlerExceptionResolver,ResponseStatusExceptionResolver和DefaultHandlerExceptionResolver。

 

RequestMappingHandlerAdapter 和 ExceptionHandlerExceptionResolver 作為一個默認的配置,由下列實例指定:

ContentNegotiationManager

DefaultFormattingConversionService

LocalValidatorFactoryBean——支持 JSR303

HttpMessageConverter

 

並在 org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser#parse 這裡進行的注冊。

並且發現 RequestMappingHandlerMapping 的 order 為 0,BeanNameUrlHandlerMapping 的 order 為2。所以請求先去映射 RequestMappingHandlerMapping。

 

上面介紹了 <mvc:annotation-driven /> 。下面介紹類型轉換和格式化、數據校驗。

一、在介紹每個模塊之前,首先對整體流程有個清晰的認識。

SpringMVC 通過反射機制對目標方法的簽名進行分析,將請求消息綁定到處理方法入參中。

SpringMVC 將 ServletReqeust 對象以及處理方法入參對象實例傳遞給 DataBinder,DataBinder 調用裝配在 springmvc 上下文的 ConversionService 進行類型轉換和格式化,

將請求信息填充到入參對象中,然後調用 Validator 組件對已入參的對象進行數據合法性校驗,並生成數據綁定結果 BindingResult 對象,若數據轉換或數據格式化失敗,或驗證失敗,

都會將失敗的信息填充到 BinddingResult 對象中。

二、SpringMVC 的類型轉換和格式化:

ConversionService 是 spring 類型轉換體系的核心接口。位於 org.springframework.core.convert 包下,可以利用 org.springframework.context.support.ConversionServiceFactoryBean 

在 springmvc 上下文中配置一個 ConversionService 的實例。SpringMVC 配置文件會自動識別上下文中的 ConversionService,並在類型轉換的時候使用它。

使用<mvc:annotation-driven /> 注解後:

默認會注冊一個 ConversionService ,即 FromattingConversionServiceFactoryBean, 生產的 ConversionService 可以進行類型(日期和數字)的格式化。

若添加自定義類型轉換器後,需要對其裝配自定義的 ConversionService 。如:

<bean id="customizeConversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="com.nucsoft.springmvc.converter.PersonConversionService"/>
            <bean class="com.nucsoft.springmvc.converter.String2MapConversionService"/>
        </set>
    </property>
</bean>
<mvc:annotation-driven conversion-service="customizeConversionService"/>

這裡裝配的自定義 ConversionService 為 customerConversionService, 是由 ConversionServiceFactoryBean 生產的。

這裡會有一個問題,由 ConversionServiceFactoryBean 生產的 ConversionService 會覆蓋默認的 FormattingConversionServiceFactoryBean,會導致類型(日期和數字)的給石化出錯。

Spring 中定義了一個 ConversionService 實現類 FormattingConversionService ,該類擴展於 GenericConversionService ,它既有類型轉換也有格式化的功能。

FormattingConversionService 也擁有一個 FormattingConversionServiceFactoryBean 。通過在 Spring 上下文中構造一個 FormattingConversionService,

既可以注冊自定義類型轉換器,也可以注冊自定義注解格式化。NumberFormatAnnotationFormatterFactoryBean、JodaDateTimeFormatAnnotationFormatFactory 

會自定注冊到 FormattingConversionServiceFactoryBean 中。因此配置 FormattingConversionServiceFactoryBean 後,能很好的解決上述問題。如:

<bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="FormattingConversionService">
    <property name="converters">
        <set>
            <bean class="com.nucsoft.springmvc.converter.PersonConverster"/>
            <bean class="com.nucsoft.springmvc.converter.String2MapConverter"/>
        </set>
    </property>
</bean>
<mvc:annotation-driven conversion-service="FormattingConversionService"/>

來看一個具體的轉換器:

/**
 * @author solverpeng
 * @create 2016-08-15-14:50
 */
public class PersonConverter implements Converter<String, Person>{
    Person person = null;
    @Override
    public Person convert(String s) {
        try {
            if(s != null && s.length() > 0) {
                String[] strings = s.split("\\|");
                person = Person.class.newInstance();
                for(String str : strings) {
                    String[] properties = str.split(":");
                    Field field = Person.class.getDeclaredField(properties[0]);
                    field.setAccessible(true);
                    Class<?> type = field.getType();
                    if(type.equals(Integer.class)) {
                        field.set(person, Integer.parseInt(properties[1]));
                        continue;
                    }
                    field.set(person, properties[1]);
                }
            }
        } catch(InstantiationException | IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
        return person;
    }
}

可以看出,具體解決轉換問題的是:一個個的 Converter。

在 core.convert.support 包下提供了許多默認的類型轉化器,為類型轉換提供和極大的方便。

這些類型轉換器雖然包含了大部分常用類型的轉換,但是有時候我們有些特殊需求,就需要自定義類型轉換器。

1.自定義類型轉換器:

(1)實現 Converter 接口

package org.springframework.core.convert.converter;
public interface Converter<S, T> {
      T convert(S source);
}

創建自定義類型轉換器,只需要實現該接口,參數 S 表示需要轉換的類型,T 表示轉換後的類型。對於每次調用 convert() 方法,必須保證參數 source 不能為 null。

如果轉換失敗,可能會拋出異常。特別的,一個 IllegalArgumentException 會被拋出來指明無效的 source 值。

請注意,需要保證轉換器是線程安全的。

e1: 需要將 person=name:lily|age:23 轉換為對應的 person 對象

自定義的類型轉換器:

/**
 * @author solverpeng
 * @create 2016-08-15-14:50
 */
public class PersonConverter implements Converter<String, Person>{
    Person person = null;
    @Override
    public Person convert(String s) {
        try {
            if(s != null && s.length() > 0) {
                String[] strings = s.split("\\|");
                person = Person.class.newInstance();
                for(String str : strings) {
                    String[] properties = str.split(":");
                    Field field = Person.class.getDeclaredField(properties[0]);
                    field.setAccessible(true);
                    Class<?> type = field.getType();
                    if(type.equals(Integer.class)) {
                        field.set(person, Integer.parseInt(properties[1]));
                        continue;
                    }
                    field.set(person, properties[1]);
                }
            }
        } catch(InstantiationException | IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
        return person;
    }
}

在 SpringMVC 配置文件中添加如下配置:

<bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="FormattingConversionService">
    <property name="converters">
        <set>
            <bean class="com.nucsoft.springmvc.converter.PersonConverter"/>
        </set>
    </property>
</bean>
<mvc:annotation-driven conversion-service="FormattingConversionService"/>

請求:

<a href="testConverter?person=name:lily|age:23">test converter</a>

目標 handler 方法:

@RequestMapping("/testConverter")
public String testSpring2Person(Person person) {
    System.out.println("persont:" + person);
    return "success";
}

控制台輸出:

persont:Person{name='lily', age=23} 

e2:在介紹參數獲取問題是,對 @RequestParam 的 “如果方法的入參類型是一個 Map,不包含泛型類型,並且請求參數名稱是被指定” 這種情況沒有進行詳細說明,這裡通過一個例子說明。

將 String 轉換為 Map,將 params=a:1|b:2 轉換為 Map 類型。

自定義類型轉換器:

/**
 * @author solverpeng
 * @create 2016-08-15-15:40
 */
public class String2MapConverter implements Converter<String, Map<String, Object>>{
    @Override
    public Map<String, Object> convert(String s) {
        Map<String, Object> map = new HashMap<>();
        if(s != null & s.length() > 0) {
            String[] strings = s.split("\\|");
            for(String string : strings) {
                String[] split = string.split(":");
                map.put(split[0], split[1]);
            }
        }
        return map;
    }
}

在 SpringMVC 配置文件中添加如下配置:

<bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="FormattingConversionService">
    <property name="converters">
        <set>
            <bean class="com.nucsoft.springmvc.converter.String2MapConverter"/>
        </set>
    </property>
</bean>
<mvc:annotation-driven conversion-service="FormattingConversionService"/>

請求:

<a href="testConverter2?params=a:1|b:2">test converter2</a>

目標 handler 方法:

@RequestMapping("/testConverter2")
public String testString2Map(@RequestParam("params") Map map) {
    System.out.println(map);
    return "success";
}

控制台輸出:

{b=2, a=1}

(2)實現 ConverterFactory 

package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R> {
     <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

如果希望將一種類型轉換為另一種類型及其子類對象時,那麼使用這個接口。

e: num=23&num2=33.33 將 num 轉換為對應的 Integer 類型,將 num2 轉換為對應的 Double 類型。

類型轉換器:org.springframework.core.convert.support.StringToNumberConverterFactory

final class StringToNumberConverterFactory implements ConverterFactory<String, Number> {

    @Override
    public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToNumber<T>(targetType);
    }

    private static final class StringToNumber<T extends Number> implements Converter<String, T> {

        private final Class<T> targetType;

        public StringToNumber(Class<T> targetType) {
            this.targetType = targetType;
        }

        @Override
        public T convert(String source) {
            if (source.length() == 0) {
                return null;
            }
            return NumberUtils.parseNumber(source, this.targetType);
        }
    }

}

請求:

<a href="testString2Number?num=23&num2=33.33">test String to Number</a>

目標 handler 方法:

@RequestMapping("/testString2Number")
public String testString2Number(@RequestParam("num") Integer num, @RequestParam("num2") Double num2) {
    System.out.println("num:" + num);
    System.out.println("num2:" + num2);
    return "success";
}

控制台輸出:

num:23
num2:33.33

(3)還有一種 GenericConverter ,這裡不對其進行說明。有興趣的童鞋,可自行研究,用到的情況比較少。

2.格式化

這裡所說的格式化,主要指的是日期和數字的格式化。SpringMVC 支持使用 @DateTimeFormat 和 @NumberFormat 來完成數據類型的格式化。看一個例子。

/**
 * @author solverpeng
 * @create 2016-08-16-11:14
 */
public class Employee {
    private String empName;
    private String email;
    private Date birth;
    private Double salary;

    public String getEmpName() {
        return empName;
    }

    public void setEmpName(String empName) {
        this.empName = empName;
    }

    @Email
    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    @NumberFormat(pattern = "#,###,###.##")
    public Double getSalary() {
        return salary;
    }

    public void setSalary(Double salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "  empName='" + empName + '\'' +
                ", email='" + email + '\'' +
                ", birth=" + birth +
                ", salary=" + salary +
                '}';
    }
}

請求:

<a href="testFormat?empName=lily&[email protected]&birth=1992-12-23&salary=1,234,567.89">test Format</a>

控制台輸出:

employee:Employee{  empName='null', email='[email protected]', birth=Wed Dec 23 00:00:00 CST 1992, salary=1234567.89}

請求:

<a href="testFormat?empName=lily&[email protected]&birth=2992-12-23&salary=1,234,567.89">test Format</a>

控制台輸出:

allError:Field error in object 'employee' on field 'birth': rejected value [Sun Dec 23 00:00:00 CST 2992]; codes [Past.employee.birth,Past.birth,Past.java.util.Date,Past];

arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [employee.birth,birth]; arguments []; default message [birth]]; default message [需要是一個過去的事件]

employee:Employee{ empName='null', email='[email protected]', birth=Sun Dec 23 00:00:00 CST 2992, salary=1234567.89}

二、SpringMVC 使用 DataBinder 進行數據的綁定。在類型轉換和格式化之後,會進行數據的綁定。

三、SpringMVC 的數據校驗:

Spring 4.0 之後,支持  Bean Validation 1.0(JSR-303)和 Bean Validation 1.1(JSR-349) 校驗。同時也支持 Spring Validator 接口校驗。

Spring 提供一個驗證接口,你可以用它來驗證對象。這個 Validator 接口使用一個 Errors 對象來工作,驗證器驗證失敗的時候向 Errors 對象填充驗證失敗信息。

1.使用 Spring Validator 接口校驗

(1)一個簡單對象的驗證

實體類:

/** * @author solverpeng * @create 2016-08-12-10:50 */ public class Person { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } } Person.java

創建驗證器(即創建 Validator 的實現類)

/**
 * @author solverpeng
 * @create 2016-08-12-10:51
 */
public class PersonValidator implements Validator{
    /**
     * This Validator validates *just* for Person
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return Person.class.equals(aClass);
    }

    @Override
    public void validate(Object o, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "name.empty", "人名不能為空.");
        Person person = (Person) o;
        if(person.getAge() < 0) {
            errors.rejectValue("age", "negativevalue", "年齡不能為負數.");
        } else if(person.getAge() > 110) {
            errors.rejectValue("age", "too.darn.old", "年齡不得超過110歲.");
        }
    }
}

使用:

/**
 * @author solverpeng
 * @create 2016-08-12-10:49
 */
@Controller
public class TargetHandler {

    @InitBinder
    public void initBinder(DataBinder binder) {
        binder.setValidator(new PersonValidator());
    }

    @RequestMapping("/testPersonValidator")
    public String testPersonValidator(@Valid Person person, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError allError : allErrors) {
                    System.out.println("error: " + allError.getDefaultMessage());
                }
            }
        }
        System.out.println(person);
        return "success";
    }

}

(2)一個復雜對象的驗證

實體類:

/** * @author solverpeng * @create 2016-08-12-11:14 */ public class Customer { private String firstName; private String surname; private Address address; public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } public String getSurname() { return surname; } public void setSurname(String surname) { this.surname = surname; } @Override public String toString() { return "Customer{" + "firstName='" + firstName + '\'' + ", surname='" + surname + '\'' + ", address=" + address + '}'; } } Customer.java /** * @author solverpeng * @create 2016-08-12-11:15 */ public class Address { private String addressName; public String getAddressName() { return addressName; } public void setAddressName(String addressName) { this.addressName = addressName; } @Override public String toString() { return "Address{" + "addressName='" + addressName + '\'' + '}'; } } Address.java

驗證器類:

/**
 * @author solverpeng
 * @create 2016-08-12-11:16
 */
public class AddressValidator implements Validator{
    @Override
    public boolean supports(Class<?> clazz) {
        return Address.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "addressName", "field.required", "地址名稱必須輸入.");
    }
}
/**
 * @author solverpeng
 * @create 2016-08-12-11:18
 */
public class CustomerValidator implements Validator{
    private final Validator addressValidator;

    public CustomerValidator(Validator addressValidator) {
        if(addressValidator == null) {
            throw new IllegalArgumentException("the validator is required and must be null.");
        }
        if(!addressValidator.supports(Address.class)) {
            throw new IllegalArgumentException("the validator must be [Address] instance.");
        }

        this.addressValidator = addressValidator;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return Customer.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required", "firstName 必須輸入.");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required", "surname 必須輸入.");
        Customer customer = (Customer) target;

        try {
            errors.pushNestedPath("address");
            ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
        } finally {
            errors.popNestedPath();
        }
    }
}

使用

/**
 * @author solverpeng
 * @create 2016-08-12-10:49
 */
@Controller
public class TargetHandler {

    @InitBinder
    public void initBinder(DataBinder binder) {
        binder.setValidator(new CustomerValidator(new AddressValidator()));
    }

    @RequestMapping("/testCustomerValidator")
    public String testCustomerValidator(@Valid Customer customer, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError error : allErrors) {
                    System.out.println("error: " + error.getDefaultMessage());
                }
            }
        }
        return "success";
    }

}

說明:對於復雜對象來說,創建驗證器的時候,可以使用嵌套的方式

通過實現 Validator 接口的方式創建的驗證器,不是通過配置嗎,也不是通過注解來調用,而是通過 @InitBinder 標注的方法,進而去調每個驗證器進行驗證。

 

Spring 沒有對 @Valid 添加某個屬性來指定使用哪個驗證器進行驗證。所以下面這種情況會報異常。

/**
 * @author solverpeng
 * @create 2016-08-12-10:49
 */
@Controller
public class TargetHandler {

    @InitBinder
    public void initBinder(DataBinder binder) {
        binder.setValidator(new PersonValidator());
        binder.setValidator(new CustomerValidator(new AddressValidator()));
    }

    @RequestMapping("/testCustomerValidator")
    public String testCustomerValidator(@Valid Customer customer, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError error : allErrors) {
                    System.out.println("error: " + error.getDefaultMessage());
                }
            }
        }
        return "success";
    }

    @RequestMapping("/testPersonValidator")
    public String testPersonValidator(@Valid Person person, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError allError : allErrors) {
                    System.out.println("error: " + allError.getDefaultMessage());
                }
            }
        }
        System.out.println(person);
        return "success";
    }

}

同時指定了兩個驗證器,不論是請求 testCustomerValidator,還是請求 testPersonValidator 都會報異常,因為在 initBinder() 中設置的兩個 validator 是一個且的關系,只要是驗證,必須同時滿足這兩個 validator 的驗證規則。

 

默認情況下,如果不對 @InitBinder 注解指定 value 屬性值,那麼這個方法對每個入參存在 model 參數的目標 handler 方法起作用,在目標方法調用前,對 入參處的 model 執行校驗。

如果對 @InitBinder 注解指定 value 屬性值,那麼它只會對對應的目標 handler 方法的入參 model 執行校驗.

如:

/**
 * @author solverpeng
 * @create 2016-08-12-10:49
 */
@Controller
public class TargetHandler {

    @InitBinder("customer")
    public void initBinder(DataBinder binder) {
        binder.setValidator(new CustomerValidator(new AddressValidator()));
    }

    @RequestMapping("/testCustomerValidator")
    public String testCustomerValidator(@Valid Customer customer, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError error : allErrors) {
                    System.out.println("error: " + error.getDefaultMessage());
                }
            }
        }
        return "success";
    }

    @RequestMapping("/test")
    public String test(@Valid Person person) {
        System.out.println(person);
        return "success";
    }

}

如此時,只會對 testCustomerValidator() 方法處的 customer 參數執行校驗,而不會對 test() 方法處的 person 參數執行校驗。

 

目標 handler 方法處的 BindingResult 類型的參數,它是 Errors 的子接口,驗證失敗之後的錯誤信息會存放到 Erros 中,在目標方法處,驗證出錯的信息可以通過 BindingResult 來獲取。

需要注意的是,如果在目標方法處存在多個需要校驗的對象,需要在每個對象後添加 BindingResult 來獲取驗證失敗的錯誤信息,而不能只通過一個 BindingResult 來獲取所有的錯誤信息,即 BindignResult 必須緊挨著要驗證的參數後。

 

2.使用 JSR-303 進行校驗

JSR-303 校驗是一個數據校驗規范,Spring 沒有對其進行實現,所以使用的時候,需要添加一個實現,這裡添加 Hibernate Validator 作為 JSR-303 的實現.

需要額外添加的 jar 包:

validation-api-1.1.0.CR1.jar
hibernate-validator-5.0.0.CR2.jar
hibernate-validator-annotation-processor-5.0.0.CR2.jar
jboss-logging-3.1.1.GA.jar
classmate-0.8.0.jar

在 Bean 的 getXxx() 或屬性名上添加驗證規則注解.

(1)一個簡單對象的驗證

實體類:

public class Person {
    private String name;
    private int age;

    @NotBlank(message = "人名不能為空")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Min(value = 10, message = "年齡最小為10.")
    @Max(value = 110, message = "年齡最大不得超過110.")
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

使用:

@RequestMapping("/testPersonValidator")
public String testPersonValidator(@Valid Person person, BindingResult result) {
    if(result.hasErrors()) {
        List<ObjectError> allErrors = result.getAllErrors();
        if(!CollectionUtils.isEmpty(allErrors)) {
            for(ObjectError allError : allErrors) {
                System.out.println("error: " + allError.getDefaultMessage());
            }
        }
    }
    System.out.println(person);
    return "success";
}

不需要通過 @InitBinder 來指定驗證器,只需要在目標 handler 方法處對需要驗證的對象標注 @Valid 注解。可以對一個屬性標有多個驗證規則的注解。

(2)一個復雜對象的驗證

實體:

public class Address {
    private String addressName;

    @NotBlank
    public String getAddressName() {
        return addressName;
    }

    public void setAddressName(String addressName) {
        this.addressName = addressName;
    }

    @Override
    public String toString() {
        return "Address{" +
                "addressName='" + addressName + '\'' +
                '}';
    }
}
public class Customer {
    private String firstName;
    private String surname;
    private Address address;

    @NotBlank(message = "firstName must be not null.")
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    @Valid
    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    @NotBlank(message = "surname must be not null.")
    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "firstName='" + firstName + '\'' +
                ", surname='" + surname + '\'' +
                ", address=" + address +
                '}';
    }
}

說明:對復雜對象的驗證和簡單對象的驗證使用方式相同,對內嵌對象的驗證使用 @Valid 注解就行。

和實現 Validator 接口不同,在 Spring 中使用 JSR-303 可以在一個 handler 中驗證多個不同的的 bean 。如:

@Controller
public class TargetHandler {

    @RequestMapping("/testCustomerValidator")
    public String testCustomerValidator(@Valid Customer customer, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError error : allErrors) {
                    System.out.println("error: " + error.getDefaultMessage());
                }
            }
        }
        return "success";
    }

    @RequestMapping("/testPersonValidator")
    public String testPersonValidator(@Valid Person person, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError allError : allErrors) {
                    System.out.println("error: " + allError.getDefaultMessage());
                }
            }
        }
        System.out.println(person);
        return "success";
    }

}

說明:不論是請求 testCustomerValidator 對 Customer 進行驗證還是請求testPersonValidator 對 Person 進行驗證,不會報異常

3.自定義驗證規則約束注解

(1)參照已經實現的驗證規則。自定義的驗證規則注解需要使用 @Constraint 注解,同時指定其屬性 validatedBy 的值,即使用哪個驗證器類進行校驗。

(2)必須對驗證器注解類添加三個屬性:message、groups和payload。

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NumberValidator.class)
public @interface Number {
    String message() default "must be a number.";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

}

定義驗證器類,必須實現  public interface ConstraintValidator<A extends java.lang.annotation.Annotation, T> 這個接口,如:

public class NumberValidator implements ConstraintValidator<Number, String> {
    private Number number;

    @Override
    public void initialize(Number number) {
        this.number = number;
    }

    @Override
    public boolean isValid(String str, ConstraintValidatorContext constraintValidatorContext) {
        try {
            Integer.parseInt(str);
            return true;
        } catch(NumberFormatException e) {
            return false;
        }
    }
}

可以看出,需要實現兩個方法,initialize() 方法進行初始化,isValid() 方法進行驗證,其中 A 為驗證器注解類名,也是initialize() 方法的入參,T 為要驗證的對象,也是 isValid() 方法的的第一個參數。

(3)對驗證規則注解進行分組

問題:通過驗證接口的方式,可以為一個 bean 定義多個驗證器,可以適用於不同的情況。那麼使用 JSR-303 該如何達到這種效果?

<1>定義分組接口(普通接口就行),用於標識分組

<2>對要驗證的 bean 標注的注解進行分組。

public class Person {
    private String name;
    private int age;

    @NotBlank(message = "人名不能為空")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Min(value = 10, message = "年齡最小為10.", groups = First.class)
    @Max(value = 110, message = "年齡最大不得超過110.", groups = Second.class)
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

驗證時指定分組

@RequestMapping("/testPersonValidator")
public String testPersonValidator(@Validated({First.class}) Person person, BindingResult result) {
    if(result.hasErrors()) {
        List<ObjectError> allErrors = result.getAllErrors();
        if(!CollectionUtils.isEmpty(allErrors)) {
            for(ObjectError allError : allErrors) {
                System.out.println("error: " + allError.getDefaultMessage());
            }
        }
    }
    System.out.println(person);
    return "success";
}

說明:

在使用時,可以指定同時使用多個分組,按照先後順序進行驗證,若第一個不通過,則不驗證第二個。

為同一個 Bean 定義的多個驗證器注解名稱不能重復,若想添加相同的規則,需要自定義注解。

 

四、客戶端錯誤消息的顯示:

上面已經介紹過,不論是類型轉換和格式化失敗,還是數據校驗失敗,都會將錯誤信息填充到 BindingResult 中,那麼在客戶端如何獲取呢?

使用 SpringMVC 標簽顯示錯誤消息。當使用 springmvc 標簽顯示錯誤消息時,SpringMVC 會查看 Web 上下文是否裝配了對應的國際化資源文件,在國際化資源文件中查找對應的國際化消息。如果沒有找到,則顯示默認的錯誤消息。

每個屬性在數據綁定或數據校驗發生錯誤的時候,都會生成一個 fieldError 對象。當一個屬性校驗失敗後,校驗框架會為該屬性生成4個消息代碼。

如 User 的 password 屬性標注了一個 @Pattern 注解,當不滿足 @Pattern 定義的規則時,就會產生以下4個錯誤代碼。

Pattern.user.password
Pattern.password
Pattern.java.lang.String
Pattern

若類型轉換或數據格式化出錯時,或該有的參數不存在時,或調用處理方法發生錯誤時,都會在隱含模型中創建錯誤消息。

錯誤代碼前綴:

required:必要的參數不存在。如 @RequestParam("param1") 標注了一個入參,但是該參數不存在。

typeMismatch: 在數據綁定時,發生數據類型不匹配的問題。

methodInvocation: 調用處理方法時發生了錯誤。

 

注冊國際化資源文件:

在SpringMVC配置文件中添加 ResourceBundleMessageSource 類型的 Bean ,同時指定其 id 為 messageSource。設置其國際化基名。如:

<bean class="org.springframework.context.support.ResourceBundleMessageSource" id="messageSource">
  <property name="basename" value="i18n"/>
</bean>

 

五、總結

本篇文章從 <mvc:annotation-driven /> 說起,主要說明了 SpringMVC 的類型轉換、格式化以及數據校驗。只是對用法進行了說明,沒有更深層次的深入。以後深入的時候再寫文章來介紹吧。

 

六、外傳

(1)

在 idea 下,無法對國際化資源文件中的中文轉為 Unicode,這裡提供一個 中文漢字 | ASCII | Unicode互相轉換工具 在線工具:

http://www.atool.org/chinese2unicode.php

(2)

@InitBinder 詳解

標注有 @InitBinder 注解的方法。可以對 WebDataBinder 對象進行初始化,WebDataBinder 是 DataBinder 的子類,用於完成表單屬性對 Bean 屬性的綁定。

@InitBinder 標注的方法不能有返回值,參數一般為 WebDataBinder 。

如不自動綁定對象中的 name 值。

@InitBinder
public void initBinder(WebDataBinder dataBinder) {
    dataBinder.setDisAllowedField("name");
}

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved