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