• Spring MVC -- 转换器和格式化


    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参数将会记录到表单支持对象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;
        }
    
    }
    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,但是表单内容不会丢失,并且会在表单中看到错误的消息:

    可以看到在将表单提交的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更合适。

    参考文章

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

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

    [3]Spring MVC学习指南

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

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

  • 相关阅读:
    夺命雷公狗---linux NO:8 linux的通配符和ll以及ls的使用方法
    夺命雷公狗---linux NO:7 linux命令基本格式
    夺命雷公狗---linux NO:6 linux远程登录和关机和重启
    夺命雷公狗---解决网络和共享中心打不开的问题
    夺命雷公狗---linux NO:5 linux系统登录和注销
    夺命雷公狗---linux NO:4 linux系统运行级别
    利用win7系统自带服务在内网搭建ftp服务器
    2017-05-31--夺命雷公狗发牢骚
    夺命雷公狗C/C++-----9---自定义一个函数测试简单的运算
    夺命雷公狗C/C++-----8---使用ShellExecute打开一个文件和一个网址和打印文件
  • 原文地址:https://www.cnblogs.com/zyly/p/10836522.html
Copyright © 2020-2023  润新知