概述
public String method(Integer num, Date birth) {
// ...
}
HTTP请求传递的数据都是字符串String类型的,如果请求中含有num和birth参数,那么num会被自动转换成Intger数据类型,birth会被自动转化为Date对象
数据绑定
- SpringMVC主框架将ServletRequest对象及目标方法的入参实例传递给WebDataBinderFactory实例,以创建DataBinder实例对象
- DataBinder调用装配在SpringMVC上下文中ConversionService组件进行数据类型转换,数据格式化工作。将Servlet中的请求信息填充到入参对象中
- 调用Validator组件对已经绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果BindingData对象
- SpringMVC抽取BindingResult中的入参对象和校验错误对象,将它们赋给处理方法的响应入参
SpringMVC通过反射机制对目标处理方法进行解析,将请求消息绑定到处理方法的入参中。数据绑定的核心组件是DataBinder,运行机制如下:
简单总结就是先将控制器处理方法的入参对象和ServletRequest对象传入数据类型转换器,进行数据类型转换,之后通过Validator组件进行数据校验,将生成结果绑定到BindingData对象,最终SpringMVC抽取BindingResult中的入参对象和校验错误对象进行控制器处理方法的入参
自定义类型转换器
ConversionService是Spring类型转换体系的核心接口,可以利用ConversionServiceFactoryBean在Spring的IOC容器中定义一个ConversionService,并在Bean属性配置及Spring MVC处理方法入参绑定等场合使用它进行数据的转换。可通过ConversionServiceFactoryBean的converters属性注册自定义的类型转换器
<!-- SpringMVC中有默认配置的ConversionService,自定义时需要显氏配置将其覆盖-->
<mvc:annotation-driven conversion-service="conversionService"></mvc:annotation-driven>
<!-- 配置 ConversionService -->
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<property name="converters">
<set>
<!-- 注解注入-->
<ref bean="employeeConverter"/>
</set>
</property>
</bean>
Spring支持的转换器
Spring定义了3种类型的转换器,实现任意一个转换接口都可以作为自定义转换器注册到:
CconversionServiceFactory:
- Converter<S,T>:将 S 类型对象转为 T 类型对象
- GenericConverter:会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换
- ConverterFactory:将相同系列多个 “同质” Converter 封装在一起。如果希望将一种类型的对象转换为另一种类型及其子类的对象(例如将 String 转换为 Number 及 Number 子类(Integer、Long、Double 等)对象)可使用该转换器工厂类
@Component
public class EmployeeConverter implements Converter<String, Employee> {
@Override
public Employee convert(String source) {
if(source != null){
String [] vals = source.split("-");
//GG-gg@atguigu.com-0-105
if(vals != null && vals.length == 4){
String lastName = vals[0];
String email = vals[1];
Integer gender = Integer.parseInt(vals[2]);
Department department = new Department();
department.setId(Integer.parseInt(vals[3]));
Employee employee = new Employee(null, lastName, email, gender, department);
System.out.println(source + "--convert--" + employee);
return employee;
}
}
return null;
}
}
@InitBinder
由InitBinder标注的方法,可以对WebDataBinder对象进行初始化。WebDataBinder是DataBinder的子类,用于完成由表单字段到JavaBean属性的绑定
- @InitBinder方法不能有返回值,它必须声明为void
- @InitBinder方法的参数通常是WebDataBinder
@InitBinder
public void initBinder(WebDataBinder binder) {
//禁止表单字段中lastName映射到对应JavaBean属性
binder.setDisallowedFields("lastName");
}
数据格式化
SpringMVC数据类型格式化主要是数值格式化和日期格式化,当输入的格式按照注解所定义的格式,SpringMVC就能将其解析为相对应的形式。
对属性对象的输入/输出进行格式化,从其本质上讲依然属于类型转化。Spring在格式化模块中定义了一个实现了ConversionService接口的FormattingConversionService实现类,该实现类扩展了GenericConversionService,既具有类型转化的功能又有格式化的功能
FormattingConversionService拥有一个FormattingConversionServiceFactoryBean工厂类,后者用于在Spring上下文中构造前者
FormattingConversionServiceFactroyBean 内部已经注册了:
- NumberFormatAnnotationFormatterFactroy:支持对数字类型的属性使用 @NumberFormat 注解
- pattern:类型为 String,自定义样式,如patter="#,###"
- JodaDateTimeFormatAnnotationFormatterFactroy:支持对日期类型的属性使用@DateTimeFormat 注解
- pattern 属性:类型为字符串。指定解析/格式化字段数据的模式,如:”yyyy-MM-dd hh:mm:ss”
装配了 FormattingConversionServiceFactroyBean 后,就可以在 Spring MVC 入参绑定及模型数据输出时使用注解驱动—mvc:annotation-driven/ 默认创建的ConversionService 实例即为
FormattingConversionServiceFactroyBean
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date birth;
@NumberFormat(pattern="#,###,###.#")
private Float salary;
数据校验
JSR303是Java为Bean数据合法性校验提供的标准框架,JSR303通过在Bean属性上标注类似@NotNull,@Max等标准的注解指定校验规则,并通过标准的验证接口对Bean进行验证
注解 | 功能说明 |
---|---|
@Null | 被标注的元素必须为null |
@NotNull | 被标注的元素不能为null |
@AssertTrue | 被标注的元素必须为true |
@AssertFalse | 被标注的元素必须为false |
@Max(value) | 被标注的元素必须为数字且值不能大于value |
@Min(value) | 被标注的元素必须为数字且值不能小于value |
@Size(max,min) | 被标注的元素必须为数字且在min-max范围内 |
@Past | 被标注的元素必须为一个过去的日期 |
@Future | 被标注的元素必须为一个将来的日期 |
@Pattern(value) | 被标注的元素必须符合指定的正则表达式 |
Hibernate Validator
Hibernate Validator是JSR 303的一个参考实现,扩展以下注解
注解 | 功能说明 |
---|---|
被标注的元素必须为电子邮箱地址 | |
@Length | 被标注的字符串大小必须在指定范围内 |
@NotEmpty | 被标注的字符串必须非空 |
@Range | 被标注的元素必须在指定范围内 |
SpringMVC数据校验
- Spring 4.0 拥有自己独立的数据校验框架,同时支持 JSR 303 标准的校验框架
- 在 Spring MVC 中,可直接通过注解驱动的方式进行数据校验
- Spring 的 LocalValidatorFactroyBean 既实现了 Spring 的Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在 Spring 容器中定义了一个LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean 中
- Spring 本身并没有提供 JSR303 的实现,所以必须将JSR303 的实现者的 jar 包放到类路径下
数据校验流程:
- 加入hibernate validator验证框架
- 在SpringMVC配置文件中加上< mvc:annotation-driven>
- 在需要校验的bean属性上添加对应的属性
- 在控制器处理方法上参数bean前面添加@Valid
ps:< mvc:annoation-driven>默认装配好一个LocalValidatorFactoryBean,通过在处理方法的入参标注@valid注解即可让SpringMVC在完成数据绑定后执行数据校验的工作
//bean属性上注解校验类型
public class Employee {
private Integer id;
@NotEmpty
private String lastName;
@Email
@NotEmpty
private String email;
@Past
private Date birth;
}
@RequestMapping(value="/emp" , method=RequestMethod.POST)
//表单自动传入pojo类
//bean参数前注解@Valid
public String save(@Valid Employee employee,BindingResult result,Map<String,Object> map) {
employeeDao.save(employee);
//获取校验结果
if(result.getErrorCount()>0) {
System.out.println("出错");
for(FieldError error : result.getFieldErrors()) {
System.out.println(error.getField()+":"+error.getDefaultMessage());
}
//若验证出错,则转向定制页面
map.put("departments",departmentDao.getDepartments());
return "input";
}
return "redirect:/emps";
}
需校验的Bean对象和其绑定结果对象或错误对象需要成对出现,它们之间不允许声明其他入参
目标方法中获取校验结果
在bean类属性标注校验注解,在处理方法对应的入参前添加@Valid,SpringMVC就会实施校验并将校验结果保存在被校验入参对象之后的BindingResult或Errors入参(Errors接口提供了获取错误信息的方法,BindingResult扩展了Erros接口)
常用方法:
- – FieldError getFieldError(String field)
- – List getFieldErrors()
- -Object getFieldValue(String field)
- -Int getErrorCount()
在页面上显示错误
SpringMVC除了会将校验结果保存到对应的BindingResult或Errors对象外,还会将所有校验结果保存到"隐含模型"。即使处理方法的签名中没有对应于表单/命令对象的结果入参,校验结果也会保存在"隐含对象"中。隐含模型中的所有数据最终将通过HttpServletRequest的属性列表暴露给JSP视图对象,因此在JSP中可以获取错误信息,在JSP页面上可通过< form:errors path=“xxx-属性名”>显示错误消息
<!-- *代表显示全部错误消息-->
<form:errors path="*"></form:errors>
<c:if test="${employee.id == null}">
LastName:<form:input path="lastName"/>
<!-- 显示"lastName"的错误消息-->
<form:errors path="lastName"></form:errors>
</c:if>
<c:if test="${employee.id != null}">
<form:hidden path="id"/>
<input type="hidden" name="_method" value="PUT">
</c:if>
<br>
Email:<form:input path="email"/>
<!-- 显示“email”的错误消息-->
<form:errors path="email"></form:errors>
提示消息的国际化
- 每个属性在数据绑定和数据校验发生错误时,都会生成一个对应的FieldError对象
- 当一个属性校验失败后,校验框架会为该属性生成4个消息代码,这些代码以校验注解名为前缀,结合ModelAttribute,属性名及属性类型名生成多个对应的消息代码
例如 User 类中的 password 属性标准了一个 @Pattern 注解,当该属性值不满足 @Pattern 所定义的规则时, 就会产生以下 4 个错误代码:
- Pattern.user.password
- Pattern.password
- Pattern.java.lang.String
- Pattern
当使用 Spring MVC 标签显示错误消息时, Spring MVC 会查看WEB 上下文是否装配了对应的国际化消息,如果没有,则显示默认的错误消息,否则使用国际化消息
若数据类型转换或数据格式转换时发生错误,或该有的参数不存在,或调用处理方法时发生错误,都会在隐含模型中创建错误消息。其错误代码前缀说明如下:
- required:必要的参数不存在。如 @RequiredParam(“param1”) 标注了一个入参,但是该参数不存在
- typeMismatch:在数据绑定时,发生数据类型不匹配的问题
- methodInvocation:Spring MVC 在调用处理方法时发生了错误
SpringMVC配置文件中注册国际化资源
<!-- 配置国际化资源文件 -->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="i18n"></property>
</bean>
配置对应属性文件
NotEmpty.employee.lastName=\u540D\u5B57\u4E0D\u80FD\u4E3A\u7A7A
Email.employee.email=Email\u683C\u5F0F\u4E0D\u89C4\u8303
Past.employee.birth=Birth\u5FC5\u987B\u4E3A\u4E4B\u524D\u7684\u65F6\u95F4
typeMismatch.employee.birth=Birth\u6570\u636E\u7C7B\u578B\u8F6C\u6362\u5931\u8D25
HttpMessageConvert< T >
HttpMessageConvert< T >负责将请求参数转换为一个对象(类型为T),将对象(类型为T)输出为响应信息
HTTP请求和响应报文的本质上都是字符串,当请求报文来到ava内部,会被封装成ServletInputStream的输入流,供我们读取报文;响应报文则是通过ServletOutputStream输出流,来输出响应报文
public ServletInputStream getInputStream() throws IOException;
public ServletOutputStream getOutputStream() throws IOException;
流中都是原始的字符串,而java中都是有意义的对象,那么在报文到达SpringMVC和从SpringMVC出去,都存在字符串到java对象的阻抗问题,在SpringMVC中这一转换通过HttpMessageConvert机制实现
HttpInputMessage 这个类是SpringMVC内部对一次Http请求报文的抽象,在HttpMessageConverter的read()方法中,有一个HttpInputMessage的形参,它正是SpringMVC的消息转换器所作用的受体“请求消息”的内部抽象,消息转换器从“请求消息”中按照规则提取消息,转换为方法形参中声明的对象
package org.springframework.http;
import java.io.IOException;
import java.io.InputStream;
public interface HttpInputMessage extends HttpMessage {
InputStream getBody() throws IOException;
}
HttpOutputMessage 这个类是SpringMVC内部对一次Http响应报文的抽象,在HttpMessageConverter的write()方法中,有一个HttpOutputMessage的形参,它正是SpringMVC的消息转换器所作用的受体“响应消息”的内部抽象,消息转换器将“响应消息”按照一定的规则写到响应报文中
package org.springframework.http;
import java.io.IOException;
import java.io.OutputStream;
public interface HttpOutputMessage extends HttpMessage {
OutputStream getBody() throws IOException;
}
HttpMessageConverter是对消息转换器最高层次的接口抽象,描述了一个消息转换器的一般特征
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, MediaType mediaType);
boolean canWrite(Class<?> clazz, MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
举个例子,当我们声明了下面这个处理方法:
@RequestMapping(value="/string", method=RequestMethod.POST)
public @ResponseBody String readString(@RequestBody String string) {
return "Read string '" + string + "'";
}
当SpringMVC进入readString()方法前,会根据@RequestBody注解选择适当的HttpMessageConverter实现类来将请求参数解析到String变量,具体来说是使用了StringHttpMessageConverter类,它的canRead()方法返回true,然后read()方法从请求中读取请求参数,绑定到readString()方法的String变量中
当SpringMVC执行完readString()方法后,由于返回值标注了@ResponseBody,SpringMVC将使用StringHttpMessageConverter的write()方法,将结果作为String值写入响应报文,之后canWrite()方法返回true
HttpMessageConvert< T >实现类
实现类 | 功能说明 |
---|---|
StringHttpMessageConvert | 将请求信息转化为字符串 |
ByteArrayHttpMessageConvert | 读写二进制数据 |
… | … |
DispatcherServlet默认装配RequestMappingHandlerAdapter,而RequestMappingHandlerAdapter默认装配如下HttpMessageConvert:
- ByteArrayHttpMessageConverter
- StringHttpMessageConverter
- ResourceHttpMessageConverter
- SourceHttpMessageConverter< T >
- AllEncompassingFormHttpMessageConverter
- …
使用HttpMessageConvert< T >
- 使用@RequestBody修饰方法入参,将请求参数转化为对应的Java对象
- 使用@Response修饰方法,将方法返回值转化为对应的响应信息
当控制器处理方法使用到@ResponseBody或者@RequestBody时,Spring首先根据请求头或响应头的Accept属性选择匹配的HttpMessageConverter,进而根据参数类型或泛型类型过滤得到匹配的HttpMessageConverter,若找不到可用的HttpMessageConverter将报错
简单地说,HttpMessageConvert作用于将请求参数转化为对应的java对象,将控制器处理方法返回对象转化为响应报文,是HTTP中报文和Java对象连接的桥梁
@RequestMapping(value="test/HttpMessageConvert" ,method=RequestMethod.POST)
@ResponseBody
public String testHttpMessageConvert(@RequestBody String body) {
return "helloworld"+ new Date();
}
<form action="test/HttpMessageConvert" method="POST" enctype="multipart/form-data">
File:<input type="file" name="file">
Desc:<input type="text" name="desc">
<input type="submit" value="submit">
</form>
将请求参数绑定到String变量body,将返回值输出为响应报文
疑惑
HttpMessageConvert用于将HTTP报文中的请求参数绑定到java对象,那其作用不就和ConvertService重复了吗?同时当有多个请求参数都需要绑定java对象时,又如何解决的? 百思不得其解,希望各位看官留言解惑。这篇文字的内容我并不能很好地了解其原理,只能浅尝辄止,等到日后技术长进再回头细看。