在介紹類型轉換和格式化之前,我首先來介紹 <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"); }