Spring MVC -- 轉換器和格式化 Spring MVC -- 數據綁定和表單標籤庫 Spring MVC -- 數據綁定和表單標籤庫 愉快且方便的處理時間-- LocalDate Sprin

Spring MVC -- 數據綁定和表單標籤庫中咱們已經見證了數據綁定的威力,並學習瞭如何使用表單標籤庫中的標籤。可是,Spring的數據綁定並不是沒有任何限制。有案例代表,Spring在如何正確綁定數據方面是雜亂無章的。下面舉兩個例子:css

1)Spring MVC -- 數據綁定和表單標籤庫中的tags-demo應用中,若是在/input-book頁面輸入一個非數字的價格,而後點擊」Add book「,將會跳轉到/save-book頁面:html

然而事實上/save-book頁面並不會加載成功:
java

這主要是由於沒法將表單輸入的價格從String類型綁定到Model屬性"book"所對應的Book對象的price屬性上(表單輸入價格是445edfg,price是BigDecimal類型,類型轉換失敗)。web

五月 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完成。spring

本篇博客將會討論Converter和Formatter的內容。這二者都可用於將一個對象的類型轉換成另外一種類型。Converter是通用元件,能夠在應用程序的任意層中使用,而Formatter則是專門爲Web層設計的。express

本篇博客有兩個示例程序:converter-demo和formatter-demo。二者都使用一個messageSource bean來幫助顯示受控的錯誤消息,這個bean的功能在本篇博客只會簡單的提到,後面的博客會詳細介紹。apache

一 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類:spring-mvc

public class LongToLocalDateConverter implements Converter<Long, LocalDate> {
}

在類實體中,須要編寫一個來自Converter接口的convert方法實現,這個方法的簽名以下:

    T convert(S source);

二 converter-demo範例

本小節將會建立一個converter-demo的web應用。用來演示Converter的使用。

一、目錄結構

二、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頁面所示。

三、視圖、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

四、Converter

爲了使EmployeeForm.jsp頁面中表單輸入的日期可使用不一樣於當前語言區域的日期樣式,,咱們建立了一個StringToLocalDateConverter類:

package converter;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import org.springframework.core.convert.converter.Converter;

public class StringToLocalDateConverter implements Converter<String, LocalDate> {

    private String datePattern;

    //設定日期樣式
    public StringToLocalDateConverter(String datePattern) {
        this.datePattern = datePattern;
    }
    
    @Override
    public LocalDate convert(String s) {
        try {
            //使用指定的formatter從字符串中獲取一個LocalDate對象  若是字符串不符合formatter指定的樣式要求,轉換會失敗
            return LocalDate.parse(s, DateTimeFormatter.ofPattern(datePattern));
        } catch (DateTimeParseException e) {
            // the error message will be displayed when using <form:errors>
            throw new IllegalArgumentException(
                    "invalid date format. Please use this pattern\""
                            + datePattern + "\"");
        }
    }
}

StringToLocalDateConverter類的convert()方法,它利用傳給構造器的日期樣式,將一個String轉換成LocalDate。

若是輸入的日期格式有問題,將會拋出IllegalArgumentException異常,這代表如下代碼中input標籤綁定到表單支持對象的birthDate屬性出現錯誤:

        <p>
            <label for="birthDate">Date Of Birth (MM-dd-yyyy): </label>
            <form:input path="birthDate" tabindex="3" />
        </p>

在/save-product頁面對應的請求處理方法saveEmployee()中,bindingResult參數將會記錄到表單支持對象birthDate屬性的類型轉換錯誤。

    //訪問URL:/save-employee  保存並顯示信息   將表單提交的數據綁定到employee對象的字段上
    //@ModelAttribute 會將employee對象添加到Model對象上,用於視圖顯示
    @RequestMapping(value="/save-employee")
    public String saveEmployee(@ModelAttribute Employee employee, BindingResult bindingResult,
            Model model) {
        if (bindingResult.hasErrors()) {
            FieldError fieldError = bindingResult.getFieldError();
            return "EmployeeForm";
        }
        
        // save employee here
        
        model.addAttribute("employee", employee);
        return "EmployeeDetails";
    }

請求處理方法saveEmployee()將會返回EmployeeForm.jsp頁面。EmployeeForm.jsp頁面中的errros標籤(咱們能夠將其看作BindingResult)將會顯示出與表單支持對象birthDate屬性相關的錯誤信息。

此外,咱們也能夠本身指定birthDate屬性錯誤消息,即在Spring MVC配置文件中指定錯誤信息源:

    <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
        <property name="basename" value="/WEB-INF/resource/messages" />
    </bean>

錯誤信息保存在在/WEB-INF/resource/messages.properties文件中:

typeMismatch.employee.birthDate=Invalid date. Please use the specified date pattern

注意:表單支持對象employee能夠省略,便可以寫成:

typeMismatch.birthDate=Invalid date. Please use the specified date pattern

五、配置文件

爲了使用Spring MVC應用中定製的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

六、應用測試

部署項目,並在瀏覽器輸入:

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應用。

一、目錄結構

 

二、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頁面;

三、視圖、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

四、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 + "\"");
        }
    }
}

五、配置文件

爲了使用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

六、應用測試

部署項目,並在瀏覽器輸入:

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

若是提交有效的表單信息:

七、用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的注入

相關文章
相關標籤/搜索