1. 數據綁定流程
- SpringMVC 主框架將 ServletRequest 對象及目標方法的入參實例傳遞給 WebDataBinderFactory
實例,以創建 DataBinder 實例對象; - DataBinder 調用裝配在 SpringMVC 上下文中的 ConversionService 組件進行數據類型轉換,
數據格式化工作; 將 Servlet 中的請求信息填充到入參對象中; - 調用 Validator 組件對已經綁定了請求消息的入參對象進行數據合法性校驗,並最終生成數據綁定結果
BindingData 對象; - Spring MVC 抽取 BindingResult 中的入參對象和校驗錯誤對象,將它們賦給處理方法的響應入參;
- 總結: Spring MVC 通過反射機制對目標處理方法進行解析,將請求消息綁定到處理方法的入參中.
數據綁定的核心部件是 DataBinder.
2. Spring 支持的轉換器
- Spring 定義了三種類型的轉換器接口,實現任意一個轉換接口都可以作為自定義轉換器注冊到
ConversionServiceFacotoryBean 中:Converter<S,T>
: 將 S 類型的對象轉換為 T 類型對象;ConverterFactory
: 將相同系列多個"同質"Converter封裝在一起; 將一種類型的對象轉換為
另一種類型及其子類的對象.例如,將String轉換為 Number 及 Number 子類Integer,Long,
Double等對象;GenericConverter
: 會根據源類對象及目標類對象所在的宿主類中的上下文信息進行類型轉換;
2.1 自定義類型轉換器
- ConversionService 是 Spring 類型轉換體系的核心接口;
- 可以利用 ConversionServiceFatoryBean 在 Spring 的 IOC 容器中定義一個 ConversionService;
Spring 將自動識別出 IOC 容器中的 ConversionService, 並在 Bean 屬性配置及 SpringMVC
處理方法入參綁定等場合使用它進行數據的轉換; - 可通過 ConversionServiceFactoryBean 的 converters 屬性注冊自定義的類型轉換器;
// 從 java.lang.String 轉換為 POJO 類
// index.jsp
<form action="${pageContext.request.contextPath}/empConvert" method="post">
<!-- 姓名;郵箱;性別;部門 -->
<input type="text" name="employee" value="lisi;lisi@163.com;1;105"/>
<input type="submit" value="類型轉換"/>
</form>
// EmployeeHanlder.java
@RequestMapping(value="/empConvert",method=RequestMethod.POST)
public String empConvert(@RequestParam("employee") Employee employee){
employeeService.save(employee);
return "redirect:/emps";
}
// EmployeeConverter.java
// 類型轉換,將String類型轉換為 Employee, 即自定義類型轉換器
@Component
public class EmployeeConverter implements Converter<String,Employee>{
public Employee convert(String source){
Employee result = null;
if(null != resource){
// 將字符串分割
String[] empInfos = source.split(";");
if(null != empInfos && empInfos.length == 4){
result = new Employee();
result.setLastName(empInfos[0]);
result.setEmail(empInfos[1]);
// 將String 類型轉換為 Integer 類型
result.setGender(Integer.parseInt(empInfos[2]));
Department department = new Department();
department.setId(Integer.parseInt(empInfos[3]));
result.setDepartment(department);
}
}
return result;
}
}
// SpringDispatcherServlet-servlet.xml
// 在SpringMVC配置文件中,注冊自定義類型轉換器
<!-- 引用轉換器 -->
<mvc:annotation-driven conversion-service="conversionServiceFactoryBean">
</mvc:annotation-driven>
<!-- 注冊 -->
<bean id="conversionServiceFactoryBean"
class="org.springframework.context.support.ConversionServiceFactoryBean">
<property name="converters">
<!-- 第一種寫法: 首字母小寫Bean -->
<list>
<ref bean="employeeConverter"/>
</list>
<!-- 第二種寫法: 全類名的寫法
<list>
<bean class="cn.itcast.springmvc.converts.EmployeeConvert"></bean>
</list>
-->
</perperty>
</bean>
2.2 mvc:annotation-driven
<mvc:annotation-driven/>
會自動注冊RequestMappingHandlerMapping
,
RequestMappingHandlerAdapter
與ExceptionHandlerExceptionResolver
三個Bean;- 還提供以下支持:
- 支持使用
ConversionService
實例對表單參數進行類型轉換; - 支持使用
@NumberFormatannotation
,@DataTimeFormat
注解完成數據類型的格式化; - 支持使用
@Valie
注解對 JavaBean 實例進行 JSR 303 驗證; - 支持使用
@RequestBody
和@ResponseBody
注解;
- 支持使用
2.3 @InitBinder
- 由
@InitBinder
標識的方法,可以對 WebDataBinder 對象進行初始化,WebDataBinder 是 DataBinder
的子類,用於完成由表單字段到 JavaBean 屬性的綁定; @InitBinder
方法不能有返回值,它必須聲明為 void;@InitBinder
方法的參數通常是 WebDataBinder
@InitBinder
public void initBinder(WebDataBinder webDataBinder){
// 不自動綁定對象中的 email 屬性
webDataBinder.setDisallowedFields("email");
}
3. 數據格式化
- 對屬性對象的輸入/輸出進行格式化,從其本質上講依然屬於"類型轉換"的范疇;
- Spring 在格式化模塊中定義了一個實現了
ConversionService
接口的FormattingConversionService
實現類,該實現類擴展了GenericConversionService
,因此它既具有類型轉換的功能,也具有格式化
的功能; FormattingConversionService
擁有一個FormattingConversionServiceFactoryBean
工
廠類,后者用於在 Spring上下文中構造前者;FormattingConversionServiceFactoryBean
與ConversionServiceFactoryBean
比較ConversionService
只有數據轉換功能,與ConversionServiceFactoryBean
對應;FormattingConversionService
具有數據轉換和數據格式化功能,FormattingConversionServiceFactoryBean
既可以注冊自定義的轉換器,也可以注冊自定義
的注解驅動器邏輯;
<mvc:annotation-driven>
標簽內部默認創建的一個 conversionService 實例就是一個
FormattingConversionServiceFactoryBean;
// 日期類: @DateTimeFormat
// 數字類: @NumberFormat
public class Employee{
private Integer id;
private String lastName;
private String email;
// 更改輸入的日期格式, 默認格式為 yyyy/MM/dd
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date birth;
@NumberFormat(patter="#,###.##")
private Double salary;
get和set 方法
}
// SpringDispatcherServlet-servlet.xml
// 配置 FormattingConversionServiceFacotryBean
<mvc:annotation-driven conversion-service="conversionService"/>
<bean id="conversionService"
class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
<!-- 自定義轉換器 -->
<property name="converters">
<list>
<ref bean='employeeConverter'/>
</list>
</property>
</bean>
3.1 BindingResult
- BindingResult 是接口,繼承Errors;
// 保存客戶
@RequestMapping(value="/emp",method=RequestMethod.POST)
public String save(Employee employee,BindingResult bindingResult){
if(bindingResult.getErrorCount()>0){
List<FieldError> list = bindingResult.getFieldErrors();
for(FieldError fieldError : list){
System.out.println(fieldError.getField()+"\t"+fieldError.getCode());
}
throw new RuntimeException("錄入信息出錯!");
}
employeeService.save(employee);
return "redirect:/emps";
}
3.2 JSR 303 數據驗證
- JSR303 是Java為Bean數據合法性校驗提供的標准框架,它已經包含在 JavaEE6.0 中;
- JSR303 通過在 Bean 屬性上標注類似於
@NotNull
,@Max
等標准的注解指定校驗規則,並通過
標准的驗證接口對 Bean 進行驗證; - Hibernate Validator 是 JSR303 的一個具體實現,除支持所有標准的校驗注解外,它還支持以下的
擴展注解@Email
: 被注釋的元素必須是郵箱地址;@Length
: 被注釋的字符串大小必須在指定的范圍內;@NotEmpty
: 被注釋的字符串必須非空;@Range
: 被注釋的元素必須在合適的范圍內;
- 在 SpringMVC 中,可直接通過注解驅動的方式進行數據校驗;
- Spring 的 LocalValidatorFactoryBean, 既實現了 Spring 的 Validator 接口,也實現了
JSR303 的 Validator 接口.只要在 Spring 容器中定義了一個 LocalValidatorFactoryBean,
即可將其注入到需要數據校驗的Bean中; - Spring 本身並沒有提供JSR303的實現,所以必須將JSR的實現者的jar包放到類路徑下;
// 步驟:
/*
* 1. 導入 HibetnateValidator 驗證框架的 jar 包(5個):
* classmate-0.8.0.jar, hibernate-validator-5.0.0.CR2.jar,
* hibernate-validator-annotation-processor-5.0.0.CR2.jar,
* jboss-logging-3.1.1.GA.jar, validation-api-1.1.0.CR1.jar
*
* 2. 添加注解 <mvc:annotation-driven></mvc:annotation-driven>, 會默認裝配好一個
* LocalValicatorFactoryBean
* 3. Employee.java 中添加 Field 驗證要求的相關注解;比如名字不為空, Email格式要合法等;
* 4. Save 方法對應的 POJO 參數前面添加 @Valid;
* 5. 發生異常后,重新回到錄入頁面;
*/
// Employee.java
pubic class Employee{
private Integer id;
@NotEmpty
private String lastName; // 名字不能為空
@Email
private String email; // 校驗Email格式
.....(略)
}
// EmployeeHandler.java
public String save(@Valid Employee employee,BindingResult bindingResult,
Map<String,Object> map){
if(bindingResult.getErrorCount()>0){
List<FieldError> list = bindingResult.getFieldErrors();
for(FieldError fieldError : list){
System.out.println(fieldError.getField()+"\t"
+fieldError.getDefaultMessage());
}
// 返回到登錄頁面
// 1. 查詢出全部部門
map.put("departments",departmentService.getDepartments());
// 2. 查詢出性別
map.put("genders",getGenderUtils());
return "input";
}
employeeService.save(employee);
return "redirect:/emps";
}
// 頁面顯示錯誤信息
// index.jsp
<form:form action="${pageContext.request.contextPath}/emp" method="post"
modelAttribute="employee">
lastName:<form:input path="lastName"/>
<form:errors path="lastName"></form:errors><br/>
email:<form:input path="email"/>
<form:errors path="email"></form:errors><br/>
<input type="submit" value="添加"/>
</form:form>
// 自定義錯誤信息
// SpringDispatcherServlet-servlet.xml 配置 國際化信息
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="i18n"></property>
</bean>
// 在src目錄下,新建 i18n.properties
// 命名規則: 注解名+類名+需要限制的field
NotEmpty.employee.lastName=用戶名不能為空
Email.employee.email=郵件地址格式不正確
3.3 SpringMVC 數據校驗
- 需校驗的 Bean 對象和其綁定結果對象或錯誤對象是成對出現的,它們之間不允許聲明其他的參數;
- Errors 接口提供了獲取錯誤信息的方法, 如
getErrorCount()
和getFieldErrors(String field)
- BindingResult 擴展了 Errors 接口;
4. 處理JSON
- 導入 jar 包:
jackson-annotations-2.7.0.jar
jackson-core-2.7.0.jar
jackson-databind-2.7.0.jar
- 編寫目標方法,使其返回 JSON 對應的對象(POJO)或集合(Map);
- 在方法上添加
@ResponseBody
注解;
// 需求: 查詢所有員工
// Demo.java
public class Demo{
@Autowired
private EmployeeService employeeService;
// 導入 jackson 的jar包,並且標注 @ResponseBody
// HttpConverter 會自動識別出以 JSON 格式返回給前台
@ResponseBody
@RequestMapping(value="/testJson",method=RequestMethod.GET)
public Collection<Employee> qryAllEmployee(){
// 直接返回 Collection 集合
return employeeService.getAll();
}
}
// index.jsp
<head>
<script type="text/javascript">
$(function(){
$("#testJson").click(function(){
var url=$(this).attr("href");
var args = {name:"zhangsan"};
$.post(url,args,function(data){
for(var i=0; i<data.length; i++){
var id = data[i].id;
var lastName = data[i].lastName;
alert(id+" "+lastName);
}
});
});
})
</script>
</head>
<body>
JSON 示例: <a id="testJson" href="${pageContext.request.contextPath}/testJson">
點擊這里
</a>
</body>
4.1 HttpMessageConverter
HttpMessageConverter<T>
接口負責將請求信息轉換為一個對象(類型為 T),或者將對象(類型為 T)
輸出為響應信息;HttpMessageConverter<T>
接口定義的方法:- Boolean canRead(Class<?>clazz, MediaType mediaType): 表示轉換器是否可以將請求信息
轉換為 clazz 類型的對象,同時,指定支持 MIME 類型(text/html,application/json等); - Boolean canWrite(Class<?>clazz, MediaType mediaType): 表示轉換器是否可以將 clazz
類型的對象寫入到響應流中; - T read(Class<? extends T>clazz,HttpInputMessage msg): 將請求信息流轉換為 T 類型的
對象; - void write(T t, MediaType contentType, HttpOutputMessage msg): 將 T 類型的對象
寫到響應流中,同時,指定響應的媒體類型為 contentType;
- Boolean canRead(Class<?>clazz, MediaType mediaType): 表示轉換器是否可以將請求信息
- 常見
HttpMessageConverter<T>
的實現類StringHttpMessageConverter
: 將請求信息轉換為字符串;XmlAwareFormHttpMessageConverter
ByteArrayHttpMessageConverter
: 讀寫二進制數據;SourceHttpMessageConverter
: 讀寫javax.xml.transform.Source
類型的數據;
- Spring 中使用
HttpMessageConverter<T>
:- 使用
@RequestBody/@ResponseBody
對處理方法進行標注; - 使用
HttpEntity<T>/ResponseEntity<T>
作為處理方法的入參或返回值;
- 使用
- 當控制處理方法使用到
@RequestBody/@ResponseBody
或HttpEntity<T>/ResponseEntity<T>
時, Spring 首先根據請求頭或響應頭的 Accept 屬性選擇匹配的 HttpMessageConverter, 進而
根據參數類型或泛型類型的過濾得到匹配的 HttpMessageConverter, 若找不到可用的
HttpMessageConverter, 將報錯; @RequestBody
和@ResponseBody
不需要成對出現;
// 示例一: @RequestBody
// index.jsp
<h2>注冊</h2>
<form action="${pageContext.request.contextPath}/testRequestBody" method="post"
enctype="multipart/form-data">
用戶名:<input type="text" name="username"/><br/>
密碼: <input type="password" name="password"/><br/>
上傳文件: <input type="file" name="upload"/><br/>
<input type="submit" value="上傳"/><br/>
</form>
// Demo.java
@RequestMapping(value="/testRequestBody",method=RequestMethod.POST)
public String testRequestBody(@RequestBody String content){
System.out.println("請求內容為:"+content);
return "ok";
}
// 示例二: 下載 @ResponseEntity<byte[]>
// index.jsp
下載示例: <a href="${pageContext.request.contextPath}/testDownload">下載</a>
// Demo.java
@RequestMapping(value="/testDownload", method=RequestMethod.GET)
public ReponseEntity<byte[]> testDownload() throws IOException{
byte[] body = null;
FileInputStream input = new FileInputStream(new File("/Users/用戶名/Documents/a.js"));
body = new byte[input.available()];
input.read(body);
input.close();
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Disposition","attachment;filename=a.js");
HttpStatus statusCode = HttpStatus.OK;
ResponseEntity<byte[]> result = new ResponseEntity<byte[]>(body,headers,statusCode);
return result;
}
5. 國際化
AcceptHeaderLocaleResolver
: 根據 HTTP 請求頭的Accept-Language
參數確定本地化類型,
如果沒有顯示定義本地化解析器,SpringMVC默認使用該解析器;CookieLocaleResolver
: 根據指定的 Cookie 值確定本地化類型;SessionLocaleResolver
: 根據 Session 中特定的屬性確定本地化類型;LocaleChangeInterceptor
: 從請求參數中獲取本次請求對應的本地化類型;
// 第一種方式
// src 目錄下,新建 i18n.properties, i18n_en_US.properties, i18n_zh_CN.properties
// i18n.properties 和 i18n_en_US.properties
i18n.username=username
i18n.password=password
// i18n_zh_CN.properties
i18n.username=用戶名
i18n.password=密碼
// index.jsp
國際化:<a href="${pageContext.request.contextPath}/i18n"></a>
// ok.jsp
用戶名: <fmt:message key="i18n.username"></fmt:message><br/>
密 碼: <fmt:message key="i18n.password"></fmt:message><br/>
// SpringDispatcherServlet-servlet.xml 配置
<!-- 國際化配置 -->
<bean id="messageSource"
class="org.springframework.context.support.ResourceBundleMessageSource">
<property name="basename" value="i18n"></property>
</bean>
// Demo.java
@RequestMapping(value="/i18n",method=RequestMethod.GET)
public String test(){
return "ok";
}
// 第二種方式: 注入ResourceBundleMessageSource
// Demo.java
public class Demo{
@Autowired
private ResourceBundleMessageSource messageSource;
@RequestMapping(value="/i18n2",method=RequestMethod.GET)
public String test2(Locale locale){
String v1 = messageSource.getMessage("i18n.username",null,locale);
String v2 = messageSource.getMessage("i18n.password",null,locale);
System.out.println(v1+"...."+v2);
return "ok";
}
}
// 第三種方式, 配置 SessionLocalResolver + LocaleChangeInterceptor
// springDispatcherServlet-servlet.xml
<!-- 配置 SessionLocaleResolver -->
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
</bean>
<mvc:interceptors>
<bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"/>
</mvc:interceptors>
// index.jsp
<h2>示例</h2>
<a href="${pageContext.request.contextPath}/test3?locale=zh_CN">中文</a><br/>
<a href="${pageContext.request.contextPath}/test3?locale=en_US">英文</a><br/>
// Demo.java
public class Demo{
@RequestMapping(value="/test3",method=RequestMethod.GET)
public String test3(){
return "ok";
}
}
5.1 SessionLocaleResolver & LocaleChangeInterceptor 的工作原理
參考資料