SpringMVC——类型转换和格式化数据校验客户端显示错误消息
Posted solverpeng
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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=lily@bb.com&birth=1992-12-23&salary=1,234,567.89">test Format</a>
控制台输出:
employee:Employee{ empName=\'null\', email=\'lily@bb.com\', birth=Wed Dec 23 00:00:00 CST 1992, salary=1234567.89}
请求:
<a href="testFormat?empName=lily&email=lily@bb.com&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=\'lily@bb.com\', 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 + \'}\'; } }
创建验证器(即创建 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 + \'}\'; } }
/** * @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 + \'\\\'\' + \'}\'; } }
验证器类:
/** * @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) { 以上是关于SpringMVC——类型转换和格式化数据校验客户端显示错误消息的主要内容,如果未能解决你的问题,请参考以下文章