在Spring MVC -- 数据绑定和表单标签库中我们已经见证了数据绑定的威力,并学习了如何使用表单标签库中的标签。但是,Spring的数据绑定并非没有任何限制。有案例表明,Spring在如何正确绑定数据方面是杂乱无章的。下面举两个例子:
1)在Spring MVC -- 数据绑定和表单标签库中的tags-demo应用中,如果在/input-book页面输入一个非数字的价格,然后点击”Add book“,将会跳转到/save-book页面:
然而事实上/save-book页面并不会加载成功:
这主要是因为无法将表单输入的价格从String类型绑定到Model属性"book"所对应的Book对象的price属性上(表单输入价格是445edfg,price是BigDecimal类型,类型转换失败)。
五月 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完成。
本篇博客将会讨论Converter和Formatter的内容。这两者均可用于将一个对象的类型转换成另一种类型。Converter是通用元件,可以在应用程序的任意层中使用,而Formatter则是专门为Web层设计的。
本篇博客有两个示例程序:converter-demo和formatter-demo。两者都使用一个messageSource bean来帮助显示受控的错误消息,这个bean的功能在本篇博客只会简单的提到,后面的博客会详细介绍。
一 Converter接口
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类:
public class LongToLocalDateConverter implements Converter<Long, LocalDate> { }
在类实体中,需要编写一个来自Converter接口的convert方法实现,这个方法的签名如下:
T convert(S source);
二 converter-demo范例
本小节将会创建一个converter-demo的web应用。用来演示Converter的使用。
1、目录结构
2、Controller类
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控制器包含两个请求访问方法:
- inputEmployee():对应着动作/add-employee,该函数执行完毕,加载EmployeeForm.jsp页面;
- saveEmployee():对应着动作/save-employee,该函数执行完毕,加载EmployeeDetails.jsp页面;
注意:saveEmployee()方法的BindingResult参数中放置了Spring的所有绑定错误。该方法利用BindingResult记录所有绑定错误。绑定错误也可以利用errors标签显示在一个表单中,如EmployeeForm.jsp页面所示。
3、视图、Employee类
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; } }
4、Converter
为了使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
5、配置文件
为了使用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>
6、应用测试
部署项目,并在浏览器输入:
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接口
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范例
本小节将会创建一个formatter-demo的web应用。用来演示Formatter的使用。该应用大致和converter-demo应用相同,主要就是类型转换以及配置文件的不同,相同部分不做解释,具体可以参考converter-demo应用。
1、目录结构
2、Controller类
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控制器包含两个请求访问方法:
- inputEmployee():对应着动作/add-employee,该函数执行完毕,加载EmployeeForm.jsp页面;
- saveEmployee():对应着动作/save-employee,该函数执行完毕,加载EmployeeDetails.jsp页面;
3、视图、Employee类
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; } }
4、Formatter
为了使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 + """); } } }
5、配置文件
为了使用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>
6、应用测试
部署项目,并在浏览器输入:
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"; }
如果提交有效的表单信息:
7、用Registrar注册Formatter
注册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,还是Formatter
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学习指南