在Spring MVC -- 數據綁定和表單標籤庫中咱們已經見證了數據綁定的威力,並學習瞭如何使用表單標籤庫中的標籤。可是,Spring的數據綁定並不是沒有任何限制。有案例代表,Spring在如何正確綁定數據方面是雜亂無章的。下面舉兩個例子:css
1)在Spring MVC -- 數據綁定和表單標籤庫中的tags-demo應用中,若是在/input-book頁面輸入一個非數字的價格,而後點擊」Add book「,將會跳轉到/save-book頁面:html
然而事實上/save-book頁面並不會加載成功:
java
這主要是由於沒法將表單輸入的價格從String類型綁定到Model屬性"book"所對應的Book對象的price屬性上(表單輸入價格是445edfg,price是BigDecimal類型,類型轉換失敗)。web
五月 09, 2019 8:47:23 上午 org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver logException
警告: Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'book' on field 'price': rejected value [445edfg];
codes [typeMismatch.book.price,typeMismatch.price,typeMismatch.java.math.BigDecimal,typeMismatch];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [book.price,price];
arguments []; default message [price]];
default message [Failed to convert property value of type 'java.lang.String' to required type 'java.math.BigDecimal' for property 'price';
nested exception is java.lang.NumberFormatException]]
2)Spring老是試圖用默認的語言區域將日期綁定到java.util.Data。假設想讓Spring使用不一樣的日期樣式,就須要使用一個Converter(轉換器)或者Formatter(格式化)來協助Spring完成。spring
本篇博客將會討論Converter和Formatter的內容。這二者都可用於將一個對象的類型轉換成另外一種類型。Converter是通用元件,能夠在應用程序的任意層中使用,而Formatter則是專門爲Web層設計的。express
本篇博客有兩個示例程序:converter-demo和formatter-demo。二者都使用一個messageSource bean來幫助顯示受控的錯誤消息,這個bean的功能在本篇博客只會簡單的提到,後面的博客會詳細介紹。apache
Spring的converter是能夠將一種類型轉換成另外一種類型的對象。例如,用戶輸入的日期可能有許多種形式,如」December 25, 2014「 」12/25/2014「和"2014-12-25",這些都表示同一個日期。默認狀況下,Spring會期待用戶輸入的日期樣式與當前語言區域的日期樣式相同。例如:對於美國的用戶而言,就是月/日/年格式。若是但願Spring在將輸入的日期字符串綁定到LocalDate時,使用不一樣的日期樣式,則須要編寫一個Converter,才能將字符串轉換成日期。java.time.LocalDate類是Java 8的一個新類型,用來替代java.util.Date。還需使用新的Date/Time API來替換舊的Date和Calendar類。數組
爲了建立Converter,必須編寫org.springframework.core.convert.converter.Converter接口的一個實現類,這個接口的源代碼以下:瀏覽器
/* * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.core.convert.converter; import org.springframework.lang.Nullable; /** * A converter converts a source object of type {@code S} to a target of type {@code T}. * * <p>Implementations of this interface are thread-safe and can be shared. * * <p>Implementations may additionally implement {@link ConditionalConverter}. * * @author Keith Donald * @since 3.0 * @param <S> the source type * @param <T> the target type */ @FunctionalInterface public interface Converter<S, T> { /** * Convert the source object of type {@code S} to target type {@code T}. * @param source the source object to convert, which must be an instance of {@code S} (never {@code null}) * @return the converted object, which must be an instance of {@code T} (potentially {@code null}) * @throws IllegalArgumentException if the source cannot be converted to the desired target type */ @Nullable T convert(S source); }
這裏的S表示源類型,T表示目標類型。例如,爲了建立一個能夠將Long轉換成Date的Converter,要像下面這樣聲明Converter類:spring-mvc
public class LongToLocalDateConverter implements Converter<Long, LocalDate> { }
在類實體中,須要編寫一個來自Converter接口的convert方法實現,這個方法的簽名以下:
T convert(S source);
本小節將會建立一個converter-demo的web應用。用來演示Converter的使用。
converter-demo應用提供了一個控制器:EmployeeController類。它容許用戶添加員工信息、並保存顯示員工信息:
package controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import domain.Employee; @Controller public class EmployeeController { //訪問URL:/add-employee 添加員工信息 @RequestMapping(value="/add-employee") public String inputEmployee(Model model) { model.addAttribute("employee",new Employee()); return "EmployeeForm"; } //訪問URL:/save-employee 保存並顯示信息 將表單提交的數據綁定到employee對象的字段上 //@ModelAttribute 會將employee對象添加到Model對象上,用於視圖顯示 @RequestMapping(value="/save-employee") public String saveEmployee(@ModelAttribute Employee employee, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); return "EmployeeForm"; } // save employee here model.addAttribute("employee", employee); return "EmployeeDetails"; } }
能夠看到EmployeeController控制器包含兩個請求訪問方法:
注意:saveEmployee()方法的BindingResult參數中放置了Spring的全部綁定錯誤。該方法利用BindingResult記錄全部綁定錯誤。綁定錯誤也能夠利用errors標籤顯示在一個表單中,如EmployeeForm.jsp頁面所示。
EmployeeForm.jsp:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE HTML> <html> <head> <title>Add Employee Form</title> <style type="text/css">@import url("<c:url value="/css/main.css"/>");</style> </head> <body> <div id="global"> <form:form modelAttribute="employee" action="save-employee" method="post"> <fieldset> <legend>Add an employee</legend> <p> <label for="firstName">First Name: </label> <form:input path="firstName" tabindex="1"/> </p> <p> <label for="lastName">Last Name: </label> <form:input path="lastName" tabindex="2"/> </p> <p> <form:errors path="birthDate" cssClass="error"/> </p> <p> <label for="birthDate">Date Of Birth (MM-dd-yyyy): </label> <form:input path="birthDate" tabindex="3" /> </p> <p id="buttons"> <input id="reset" type="reset" tabindex="4"> <input id="submit" type="submit" tabindex="5" value="Add Employee"> </p> </fieldset> </form:form> </div> </body> </html>
能夠看到表單數據被綁定到了模型屬性"employee"上,"employee"屬性保存着一個Employee對象,其中輸入出生日期信息的input標籤被綁定到了Employee對象的birthDate屬性上。
此外,咱們使用了表單標籤errors:
<form:errors path="birthDate" cssClass="error"/>
errors標籤用於渲染一個或多個HTML的<span></span>元素。代碼中errors標籤顯示了一個與表單支持對象的birthDate屬性相關的字段錯誤,而且設置span的class屬性爲"error",從而能夠經過class選擇器設置該元素的樣式
EmployeeDetails.jsp:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE HTML> <html> <head> <title>Save Employee</title> <style type="text/css">@import url("<c:url value="/css/main.css"/>");</style> </head> <body> <div id="global"> <h4>The employee details have been saved.</h4> <p> <h5>Details:</h5> First Name: ${employee.firstName}<br/> Last Name: ${employee.lastName}<br/> Date of Birth: ${employee.birthDate} </p> </div> </body> </html>
main.css:
#global { text-align: left; border: 1px solid #dedede; background: #efefef; width: 560px; padding: 20px; margin: 30px auto; } form { font:100% verdana; min-width: 500px; max-width: 600px; width: 560px; } form fieldset { border-color: #bdbebf; border-width: 3px; margin: 0; } legend { font-size: 1.3em; } form label { width: 250px; display: block; float: left; text-align: right; padding: 2px; } #buttons { text-align: right; } #errors, li { color: red; } .error { color: red; font-size: 9pt; }
Employee類:
package domain; import java.io.Serializable; import java.time.LocalDate; public class Employee implements Serializable { private static final long serialVersionUID = -908L; private long id; private String firstName; private String lastName; private LocalDate birthDate; private int salaryLevel; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public LocalDate getBirthDate() { return birthDate; } public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; } public int getSalaryLevel() { return salaryLevel; } public void setSalaryLevel(int salaryLevel) { this.salaryLevel = salaryLevel; } }
爲了使EmployeeForm.jsp頁面中表單輸入的日期可使用不一樣於當前語言區域的日期樣式,,咱們建立了一個StringToLocalDateConverter類:
package converter; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import org.springframework.core.convert.converter.Converter; public class StringToLocalDateConverter implements Converter<String, LocalDate> { private String datePattern; //設定日期樣式 public StringToLocalDateConverter(String datePattern) { this.datePattern = datePattern; } @Override public LocalDate convert(String s) { try { //使用指定的formatter從字符串中獲取一個LocalDate對象 若是字符串不符合formatter指定的樣式要求,轉換會失敗 return LocalDate.parse(s, DateTimeFormatter.ofPattern(datePattern)); } catch (DateTimeParseException e) { // the error message will be displayed when using <form:errors> throw new IllegalArgumentException( "invalid date format. Please use this pattern\"" + datePattern + "\""); } } }
StringToLocalDateConverter類的convert()方法,它利用傳給構造器的日期樣式,將一個String轉換成LocalDate。
若是輸入的日期格式有問題,將會拋出IllegalArgumentException異常,這代表如下代碼中input標籤綁定到表單支持對象的birthDate屬性出現錯誤:
<p> <label for="birthDate">Date Of Birth (MM-dd-yyyy): </label> <form:input path="birthDate" tabindex="3" /> </p>
在/save-product頁面對應的請求處理方法saveEmployee()中,bindingResult參數將會記錄到表單支持對象birthDate屬性的類型轉換錯誤。
//訪問URL:/save-employee 保存並顯示信息 將表單提交的數據綁定到employee對象的字段上 //@ModelAttribute 會將employee對象添加到Model對象上,用於視圖顯示 @RequestMapping(value="/save-employee") public String saveEmployee(@ModelAttribute Employee employee, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); return "EmployeeForm"; } // save employee here model.addAttribute("employee", employee); return "EmployeeDetails"; }
請求處理方法saveEmployee()將會返回EmployeeForm.jsp頁面。EmployeeForm.jsp頁面中的errros標籤(咱們能夠將其看作BindingResult)將會顯示出與表單支持對象birthDate屬性相關的錯誤信息。
此外,咱們也能夠本身指定birthDate屬性錯誤消息,即在Spring MVC配置文件中指定錯誤信息源:
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basename" value="/WEB-INF/resource/messages" /> </bean>
錯誤信息保存在在/WEB-INF/resource/messages.properties文件中:
typeMismatch.employee.birthDate=Invalid date. Please use the specified date pattern
注意:表單支持對象employee能夠省略,便可以寫成:
typeMismatch.birthDate=Invalid date. Please use the specified date pattern
爲了使用Spring MVC應用中定製的Converter,須要在Spring MVC配置文件中編寫一個類名爲org.springframework.context.support.ConversionServiceFactoryBean的bean,bean的id爲conversionService(名字能夠修改,可是須要一致)。有興趣的能夠仔細閱讀這個類的源碼:
/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.context.support; import java.util.Set; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConversionServiceFactory; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.lang.Nullable; /** * A factory providing convenient access to a ConversionService configured with * converters appropriate for most environments. Set the * {@link #setConverters "converters"} property to supplement the default converters. * * <p>This implementation creates a {@link DefaultConversionService}. * Subclasses may override {@link #createConversionService()} in order to return * a {@link GenericConversionService} instance of their choosing. * * <p>Like all {@code FactoryBean} implementations, this class is suitable for * use when configuring a Spring application context using Spring {@code <beans>} * XML. When configuring the container with * {@link org.springframework.context.annotation.Configuration @Configuration} * classes, simply instantiate, configure and return the appropriate * {@code ConversionService} object from a {@link * org.springframework.context.annotation.Bean @Bean} method. * * @author Keith Donald * @author Juergen Hoeller * @author Chris Beams * @since 3.0 */ public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean { @Nullable private Set<?> converters; @Nullable private GenericConversionService conversionService; /** * Configure the set of custom converter objects that should be added: * implementing {@link org.springframework.core.convert.converter.Converter}, * {@link org.springframework.core.convert.converter.ConverterFactory}, * or {@link org.springframework.core.convert.converter.GenericConverter}. */ public void setConverters(Set<?> converters) { this.converters = converters; } @Override public void afterPropertiesSet() { this.conversionService = createConversionService(); ConversionServiceFactory.registerConverters(this.converters, this.conversionService); } /** * Create the ConversionService instance returned by this factory bean. * <p>Creates a simple {@link GenericConversionService} instance by default. * Subclasses may override to customize the ConversionService instance that * gets created. */ protected GenericConversionService createConversionService() { return new DefaultConversionService(); } // implementing FactoryBean @Override @Nullable public ConversionService getObject() { return this.conversionService; } @Override public Class<? extends ConversionService> getObjectType() { return GenericConversionService.class; } @Override public boolean isSingleton() { return true; } }
能夠看到這個類主要包含下面2個屬性:
@Nullable private Set<?> converters; @Nullable private GenericConversionService conversionService;
而converters這個屬性,它被用來列出要在應用中使用的全部定製的Converter。conversionService對象則經過配置property元素來調用setter方法以設置converters屬性值。例如:下面的bean聲明註冊了StringToDateConverter:
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <list> <bean class="converter.StringToLocalDateConverter"> <constructor-arg type="java.lang.String" value="MM-dd-yyyy"/> </bean> </list> </property> </bean>
注意:在Spring容器建立StringToLocalDateConverter實例時,將會採用構造器依賴注入方式,調用StringToLocalDateConverter()構造函數並傳入日期樣式MM-dd-yyyy。
隨後,要給<annotation-driven/>元素的conversion-service屬性賦值bean名稱(本例中是conversionService),以下所示:
<mvc:annotation-driven conversion-service="conversionService"/>
下面給出springmvc-config.xml文件的全部內容:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="controller"/> <mvc:annotation-driven conversion-service="conversionService"/> <mvc:resources mapping="/css/*" location="/css/"/> <mvc:resources mapping="/*.html" location="/"/> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <list> <bean class="converter.StringToLocalDateConverter"> <constructor-arg type="java.lang.String" value="MM-dd-yyyy"/> </bean> </list> </property> </bean> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basename" value="/WEB-INF/resource/messages" /> </bean> </beans>
部署描述符(web.xml文件):
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/config/springmvc-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
部署項目,並在瀏覽器輸入:
http://localhost:8008/converter-demo/add-employee
試着輸入一個無效的日期,將會跳轉到/save-employee,可是表單內容不會丟失,而且會在表單中看到錯誤的消息(在前文中已經介紹過了):
這裏爲何/add-employee提交的表單信息會轉到/save-employee頁面呢?
這主要是因爲在請求/save-employee頁面時,會建立一個Employee對象,用來接收/add-employee頁面表單提交的數據。所以表單數據保存在了employee中,而employee會被添加到Model對象的"employee"屬性中。當提交一個無效日期時,會請求轉發到EmployeeForm.jsp頁面,因爲請求轉發數據不會丟失,所以EmployeeForm.jsp頁面中的表單會加載表單支持對象(也就是employee)各個屬性的值。
//訪問URL:/save-employee 保存並顯示信息 將表單提交的數據綁定到employee對象的字段上 //@ModelAttribute 會將employee對象添加到Model對象上,用於視圖顯示 @RequestMapping(value="/save-employee") public String saveEmployee(@ModelAttribute Employee employee, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); return "EmployeeForm"; } // save employee here model.addAttribute("employee", employee); return "EmployeeDetails"; }
Formatter就像Converter同樣,也是將一種類型轉換成另外一種類型。可是Formatter的源類型必須是一個String,而Converter則適用於任意的源類型。Formatter更適合Web層,而Converter則能夠用在任意層。爲了轉換Spring MVC應用表單中的用戶輸入,始終應該選擇Formatter,而不是Converter。
爲了建立Formatter,要編寫一個實現org.springframework.format.Formatter接口的Jave類,org.springframework.format.Formatter接口的源代碼以下:
/* * Copyright 2002-2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.format; /** * Formats objects of type T. * A Formatter is both a Printer <i>and</i> a Parser for an object type. * * @author Keith Donald * @since 3.0 * @param <T> the type of object this Formatter formats */ public interface Formatter<T> extends Printer<T>, Parser<T> { }
這裏的T表示輸入字符串要轉換的目標類型。該接口有parse()和print()兩個方法,因此實現類必須實驗它們:
String print(T object, Locale locale);
T parse(String text, Locale locale) throws ParseException;
parse()方法利用指定的Locale將一個String解析成目標類型。print()方法與之相反,它返回目標對象的字符串表示法。
例如:formatter-demo應用中用一個LocalDateFormatter將String轉換成Date。其做用與converter-demo中的StringToLocalDateConverter同樣。
本小節將會建立一個formatter-demo的web應用。用來演示Formatter的使用。該應用大體和converter-demo應用相同,主要就是類型轉換以及配置文件的不一樣,相同部分不作解釋,具體能夠參考converter-demo應用。
formatter-demo應用提供了一個控制器:EmployeeController類。它容許用戶添加員工信息、並保存顯示員工信息:
package controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import domain.Employee; @org.springframework.stereotype.Controller public class EmployeeController { //訪問URL:/add-employee 添加員工信息 @RequestMapping(value="add-employee") public String inputEmployee(Model model) { model.addAttribute(new Employee()); //屬性名默認爲類名(小寫) return "EmployeeForm"; } //訪問URL:/save-employee 保存並顯示信息 將表單提交的數據綁定到employee對象的字段上 //@ModelAttribute 會將employee對象添加到Model對象上,用於視圖顯示 @RequestMapping(value="save-employee") public String saveEmployee(@ModelAttribute Employee employee, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); System.out.println("Code:" + fieldError.getCode() + ", field:" + fieldError.getField()); return "EmployeeForm"; } // save product here model.addAttribute("employee", employee); return "EmployeeDetails"; } }
能夠看到EmployeeController控制器包含兩個請求訪問方法:
EmployeeForm.jsp:
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE HTML> <html> <head> <title>Add Product Form</title> <style type="text/css">@import url("<c:url value="/css/main.css"/>");</style> </head> <body> <div id="global"> <form:form modelAttribute="employee" action="save-employee" method="post"> <fieldset> <legend>Add an employee</legend> <p> <label for="firstName">First Name: </label> <input type="text" id="firstName" name="firstName" tabindex="1"> </p> <p> <label for="lastName">First Name: </label> <input type="text" id="lastName" name="lastName" tabindex="2"> </p> <p> <form:errors path="birthDate" cssClass="error"/> </p> <p> <label for="birthDate">Date Of Birth: </label> <form:input path="birthDate" id="birthDate" /> </p> <p id="buttons"> <input id="reset" type="reset" tabindex="4"> <input id="submit" type="submit" tabindex="5" value="Add Employee"> </p> </fieldset> </form:form> </div> </body> </html>
EmployeeDetails.jsp:
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> <!DOCTYPE HTML> <html> <head> <title>Save Employee</title> <style type="text/css">@import url("<c:url value="/css/main.css"/>");</style> </head> <body> <div id="global"> <h4>The employee details have been saved.</h4> <p> <h5>Details:</h5> First Name: ${employee.firstName}<br/> Last Name: ${employee.lastName}<br/> Date of Birth: ${employee.birthDate} </p> </div> </body> </html>
main.css:
#global { text-align: left; border: 1px solid #dedede; background: #efefef; width: 560px; padding: 20px; margin: 100px auto; } form { font:100% verdana; min-width: 500px; max-width: 600px; width: 560px; } form fieldset { border-color: #bdbebf; border-width: 3px; margin: 0; } legend { font-size: 1.3em; } form label { width: 250px; display: block; float: left; text-align: right; padding: 2px; } #buttons { text-align: right; } #errors, li { color: red; } .error { color: red; font-size: 9pt; }
Employee類:
package domain; import java.io.Serializable; import java.time.LocalDate; public class Employee implements Serializable { private static final long serialVersionUID = -908L; private long id; private String firstName; private String lastName; private LocalDate birthDate; private int salaryLevel; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public LocalDate getBirthDate() { return birthDate; } public void setBirthDate(LocalDate birthDate) { this.birthDate = birthDate; } public int getSalaryLevel() { return salaryLevel; } public void setSalaryLevel(int salaryLevel) { this.salaryLevel = salaryLevel; } }
爲了使EmployeeForm.jsp頁面中表單輸入的日期可使用不一樣於當前語言區域的日期樣式,咱們建立了一個LocalDateFormatter.java類:
package formatter; import java.text.ParseException; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.Locale; import org.springframework.format.Formatter; public class LocalDateFormatter implements Formatter<LocalDate> { private DateTimeFormatter formatter; private String datePattern; //設定日期樣式 public LocalDateFormatter(String datePattern) { this.datePattern = datePattern; formatter = DateTimeFormatter.ofPattern(datePattern); } //利用指定的Locale將一個LocalDate解析成String類型 @Override public String print(LocalDate date, Locale locale) { System.out.println(date.format(formatter)); return date.format(formatter); } //利用指定的Locale將一個String解析成LocalDate類型 @Override public LocalDate parse(String s, Locale locale) throws ParseException { System.out.println("formatter.parse. s:" + s + ", pattern:" + datePattern); try { //使用指定的formatter從字符串中獲取一個LocalDate對象 若是字符串不符合formatter指定的樣式要求,轉換會失敗 return LocalDate.parse(s, DateTimeFormatter.ofPattern(datePattern)); } catch (DateTimeParseException e) { // the error message will be displayed in <form:errors> throw new IllegalArgumentException( "invalid date format. Please use this pattern\"" + datePattern + "\""); } } }
爲了使用Spring MVC應用中定製的Formatter,須要在Spring MVC配置文件中編寫一個類名爲org.springframework.format.support.FormattingConversionServiceFactoryBean的bean,bean的id爲conversionService(名字能夠修改,可是須要一致)。有興趣的能夠仔細閱讀這個類的源碼:
/* * Copyright 2002-2017 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.format.support; import java.util.Set; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.core.convert.support.ConversionServiceFactory; import org.springframework.format.AnnotationFormatterFactory; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistrar; import org.springframework.format.FormatterRegistry; import org.springframework.format.Parser; import org.springframework.format.Printer; import org.springframework.lang.Nullable; import org.springframework.util.StringValueResolver; /** * A factory providing convenient access to a {@code FormattingConversionService} * configured with converters and formatters for common types such as numbers and * datetimes. * * <p>Additional converters and formatters can be registered declaratively through * {@link #setConverters(Set)} and {@link #setFormatters(Set)}. Another option * is to register converters and formatters in code by implementing the * {@link FormatterRegistrar} interface. You can then configure provide the set * of registrars to use through {@link #setFormatterRegistrars(Set)}. * * <p>A good example for registering converters and formatters in code is * {@code JodaTimeFormatterRegistrar}, which registers a number of * date-related formatters and converters. For a more detailed list of cases * see {@link #setFormatterRegistrars(Set)} * * <p>Like all {@code FactoryBean} implementations, this class is suitable for * use when configuring a Spring application context using Spring {@code <beans>} * XML. When configuring the container with * {@link org.springframework.context.annotation.Configuration @Configuration} * classes, simply instantiate, configure and return the appropriate * {@code FormattingConversionService} object from a * {@link org.springframework.context.annotation.Bean @Bean} method. * * @author Keith Donald * @author Juergen Hoeller * @author Rossen Stoyanchev * @author Chris Beams * @since 3.0 */ public class FormattingConversionServiceFactoryBean implements FactoryBean<FormattingConversionService>, EmbeddedValueResolverAware, InitializingBean { @Nullable private Set<?> converters; @Nullable private Set<?> formatters; @Nullable private Set<FormatterRegistrar> formatterRegistrars; private boolean registerDefaultFormatters = true; @Nullable private StringValueResolver embeddedValueResolver; @Nullable private FormattingConversionService conversionService; /** * Configure the set of custom converter objects that should be added. * @param converters instances of any of the following: * {@link org.springframework.core.convert.converter.Converter}, * {@link org.springframework.core.convert.converter.ConverterFactory}, * {@link org.springframework.core.convert.converter.GenericConverter} */ public void setConverters(Set<?> converters) { this.converters = converters; } /** * Configure the set of custom formatter objects that should be added. * @param formatters instances of {@link Formatter} or {@link AnnotationFormatterFactory} */ public void setFormatters(Set<?> formatters) { this.formatters = formatters; } /** * <p>Configure the set of FormatterRegistrars to invoke to register * Converters and Formatters in addition to those added declaratively * via {@link #setConverters(Set)} and {@link #setFormatters(Set)}. * <p>FormatterRegistrars are useful when registering multiple related * converters and formatters for a formatting category, such as Date * formatting. All types related needed to support the formatting * category can be registered from one place. * <p>FormatterRegistrars can also be used to register Formatters * indexed under a specific field type different from its own <T>, * or when registering a Formatter from a Printer/Parser pair. * @see FormatterRegistry#addFormatterForFieldType(Class, Formatter) * @see FormatterRegistry#addFormatterForFieldType(Class, Printer, Parser) */ public void setFormatterRegistrars(Set<FormatterRegistrar> formatterRegistrars) { this.formatterRegistrars = formatterRegistrars; } /** * Indicate whether default formatters should be registered or not. * <p>By default, built-in formatters are registered. This flag can be used * to turn that off and rely on explicitly registered formatters only. * @see #setFormatters(Set) * @see #setFormatterRegistrars(Set) */ public void setRegisterDefaultFormatters(boolean registerDefaultFormatters) { this.registerDefaultFormatters = registerDefaultFormatters; } @Override public void setEmbeddedValueResolver(StringValueResolver embeddedValueResolver) { this.embeddedValueResolver = embeddedValueResolver; } @Override public void afterPropertiesSet() { this.conversionService = new DefaultFormattingConversionService(this.embeddedValueResolver, this.registerDefaultFormatters); ConversionServiceFactory.registerConverters(this.converters, this.conversionService); registerFormatters(this.conversionService); } private void registerFormatters(FormattingConversionService conversionService) { if (this.formatters != null) { for (Object formatter : this.formatters) { if (formatter instanceof Formatter<?>) { conversionService.addFormatter((Formatter<?>) formatter); } else if (formatter instanceof AnnotationFormatterFactory<?>) { conversionService.addFormatterForFieldAnnotation((AnnotationFormatterFactory<?>) formatter); } else { throw new IllegalArgumentException( "Custom formatters must be implementations of Formatter or AnnotationFormatterFactory"); } } } if (this.formatterRegistrars != null) { for (FormatterRegistrar registrar : this.formatterRegistrars) { registrar.registerFormatters(conversionService); } } } @Override @Nullable public FormattingConversionService getObject() { return this.conversionService; } @Override public Class<? extends FormattingConversionService> getObjectType() { return FormattingConversionService.class; } @Override public boolean isSingleton() { return true; } }
能夠看到這個類主要包含下面6個屬性:
@Nullable private Set<?> converters; @Nullable private Set<?> formatters; @Nullable private Set<FormatterRegistrar> formatterRegistrars; private boolean registerDefaultFormatters = true; @Nullable private StringValueResolver embeddedValueResolver; @Nullable private FormattingConversionService conversionService;
而formatters這個屬性,它被用來列出要在應用中使用的全部定製的Formatter,這與converter-demo中用於註冊Converter類不一樣。例如:下面的bean聲明註冊了LocalDateFormatter:
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="formatters"> <set> <bean class="formatter.LocalDateFormatter"> <constructor-arg type="java.lang.String" value="MM-dd-yyyy" /> </bean> </set> </property> </bean>
注意:在Spring容器建立LocalDateFormatter實例時,將會採用構造器依賴注入方式,調用LocalDateFormatter()構造函數並傳入日期樣式MM-dd-yyyy。
隨後,要給<annotation-driven/>元素的conversion-service屬性賦值bean名稱(本例中是conversionService),以下所示:
<mvc:annotation-driven conversion-service="conversionService" />
下面給出springmvc-config.xml文件的全部內容:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="controller" /> <context:component-scan base-package="formatter" /> <mvc:annotation-driven conversion-service="conversionService" /> <mvc:resources mapping="/css/*" location="/css/" /> <mvc:resources mapping="/*.html" location="/" /> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="formatters"> <set> <bean class="formatter.LocalDateFormatter"> <constructor-arg type="java.lang.String" value="MM-dd-yyyy" /> </bean> </set> </property> </bean> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basename" value="/WEB-INF/resource/messages" /> </bean> </beans>
注意:還須要給這個這個Formatter添加一個<component-scan/>元素,用於指定Formatter的基本包。
部署描述符(web.xml文件):
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class> org.springframework.web.servlet.DispatcherServlet </servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/config/springmvc-config.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
部署項目,並在瀏覽器輸入:
http://localhost:8008/formatter-demo/add-employee
試着輸入一個無效的日期,將會跳轉到/save-employee,可是表單內容不會丟失,而且會在表單中看到錯誤的消息:
能夠看到在將表單提交的Date Of Bitrh從String類型轉換到LocalDate類型,調用了LocalDateFormatter類中的parse()方法:
//利用指定的Locale將一個String解析成LocalDate類型 @Override public LocalDate parse(String s, Locale locale) throws ParseException { System.out.println("formatter.parse. s:" + s + ", pattern:" + datePattern); try { //使用指定的formatter從字符串中獲取一個LocalDate對象 若是字符串不符合formatter指定的樣式要求,轉換會失敗 return LocalDate.parse(s, DateTimeFormatter.ofPattern(datePattern)); } catch (DateTimeParseException e) { // the error message will be displayed in <form:errors> throw new IllegalArgumentException( "invalid date format. Please use this pattern\"" + datePattern + "\""); } }
因爲類型轉換失敗,拋出異常,/save-product頁面對應的請求處理方法saveEmployee()的BindingResult參數將會記錄到這個綁定錯誤,並輸出錯誤字段的信息:
//訪問URL:/save-employee 保存並顯示信息 將表單提交的數據綁定到employee對象的字段上 //@ModelAttribute 會將employee對象添加到Model對象上,用於視圖顯示 @RequestMapping(value="save-employee") public String saveEmployee(@ModelAttribute Employee employee, BindingResult bindingResult, Model model) { if (bindingResult.hasErrors()) { FieldError fieldError = bindingResult.getFieldError(); System.out.println("Code:" + fieldError.getCode() + ", field:" + fieldError.getField()); return "EmployeeForm"; }
若是提交有效的表單信息:
註冊Formatter,除了像formatter-demo應用中那樣:在Spring MVC配置文件中配置一個名爲conversionService的bean,使用這個bean的formatters屬性註冊formatter。咱們還可使用Registrar,下面是一個註冊MyFormatterRegistrar的例子:
package formatter; import org.springframework.format.FormatterRegistrar; import org.springframework.format.FormatterRegistry; public class MyFormatterRegistrar implements FormatterRegistrar { private String datePattern; public MyFormatterRegistrar(String datePattern) { this.datePattern = datePattern; } @Override public void registerFormatters(FormatterRegistry registry) { // TODO Auto-generated method stub registry.addFormatter(new LocalDateFormatter(datePattern)); //registry more formatters here } }
有了Registrar,就不須要在Spring MVC配置文件中註冊任何formatter了,只在Spring MVC配置文件中註冊Registrar就能夠了,這裏咱們使用conversionService這個bean的formatterRegistrars屬性註冊MyFormatterRegistrar。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="controller" /> <context:component-scan base-package="formatter" /> <mvc:annotation-driven conversion-service="conversionService" /> <mvc:resources mapping="/css/**" location="/css/" /> <mvc:resources mapping="/*.html" location="/" /> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="formatterRegistrars"> <set> <bean class="formatter.MyFormatterRegistrar"> <constructor-arg type="java.lang.String" value="MM-dd-yyyy" /> </bean> </set> </property> </bean> <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource"> <property name="basename" value="/WEB-INF/resource/messages" /> </bean> </beans>
Converter是通常工具,能夠將一種類型轉換成另外一種類型。例如,將String轉換成LocalDate,或者將Long類型轉換成LocalDate。Converter既能夠用在Web層,又能夠用在其它層。
Formatter只能將String轉換成另外一種類型。例如,將String轉換成LocalDate,可是不能將Long類型轉換成LocalDate。所以,Formatter適用於Web層,爲此,在Spring MVC應用程序中,選擇Formatter更合適。
參考文章
[2]LocalDate/LocalDateTime與String的互相轉換示例(附DateTimeFormatter詳解)
[3]Spring MVC學習指南