Spring MVC -- 转换器和格式化 Spring MVC -- 数据绑定和表单标签库 Spring MVC -- 数据绑定和表单标签库 愉快且方便的处理时间-- LocalDate Spring MVC -- Spring框架入门(IoC、DI以及XML配置文件)

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;    
}
View Code

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

}
View Code

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将会记录到这个绑定错误。

    //访问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标签将会显示出与表单支持对象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

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

}
View Code

可以看到这个类主要包含下面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>
View Code

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>
View Code

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>
View Code

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;    
}
View Code

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

}
View Code

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

}
View Code

可以看到这个类主要包含下面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>
View Code

6、应用测试

部署项目,并在浏览器输入:

http://localhost:8008/formatter-demo/add-employee

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

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

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配置文件)

猜你喜欢

转载自www.cnblogs.com/zyly/p/10836522.html