Spring MVC -- 转换器和格式化

时间:2021-07-28 01:36:51

Spring MVC -- 数据绑定和表单标签库中我们已经见证了数据绑定的威力,并学习了如何使用表单标签库中的标签。但是,Spring的数据绑定并非没有任何限制。有案例表明,Spring在如何正确绑定数据方面是杂乱无章的。下面举两个例子:

1)Spring MVC -- 数据绑定和表单标签库中的tags-demo应用中,如果在/input-book页面输入一个非数字的价格,然后点击”Add book“,将会跳转到/save-book页面:

Spring MVC -- 转换器和格式化

然而事实上/save-book页面并不会加载成功:
Spring MVC -- 转换器和格式化

这主要是因为无法将表单输入的价格从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、目录结构

Spring MVC -- 转换器和格式化

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:;
} 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

Spring MVC -- 转换器和格式化

Spring MVC -- 转换器和格式化

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

Spring MVC -- 转换器和格式化

试着输入一个无效的日期,将会跳转到/save-employee,但是表单内容不会丢失,并且会在表单中看到错误的消息(在前文中已经介绍过了):

Spring MVC -- 转换器和格式化

这里为什么/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、目录结构

Spring MVC -- 转换器和格式化

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:;
} 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 &lt;T&gt;,
* 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

Spring MVC -- 转换器和格式化

试着输入一个无效的日期,将会跳转到/save-employee,但是表单内容不会丢失,并且会在表单中看到错误的消息:

Spring MVC -- 转换器和格式化

Spring MVC -- 转换器和格式化

可以看到在将表单提交的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";
}

如果提交有效的表单信息:

Spring MVC -- 转换器和格式化

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更合适。

参考文章

[1]愉快且方便的处理时间-- LocalDate

[2]LocalDate/LocalDateTime与String的互相转换示例(附DateTimeFormatter详解)

[3]Spring MVC学习指南

[4]Spring MVC -- Spring框架入门(IoC、DI以及XML配置文件)

[5]Spring依赖注入之数组,集合(List,Set,Map),Properties的注入