Spring MVC的数据转换及数据格式化

Spring MVC会根据请求方法签名不同,将请求消息中信息以一定方式转换并绑定到请求方法的参数中。

1.数据绑定流程

Spring MVC通过反射机制对目标处理方法的签名进行分析,并将请求消息绑定到处理方法的参数上。数据绑定的核心部件是Databinder

Spring MVC数据绑定机制
  • 1.Spring MVC框架将ServletRequest对象及处理方法的参数实例传递给DataBinder。

  • 2.DataBinder调用装配在Spring Web上下文中的ConversionService组件进行数据类型转换、
    数据格式化工作,并将ServletRequest中的消息填充到参数对象中。

  • 3.然后再调用Validator组件对已经绑定的请求消息数据的参数对象进行数据合法性校验。

  • 4.最终生成数据绑定结果BindingResult对象,BindingResult包含已完成数据绑定的参数对象,还包含相应的校验错误的对象。

  • 5.Spring MVC抽取BindingResult中的参数对象及校验对象,将它们赋给处理方法的相应参数。

2.数据转换(ConversionService)

在Java语言中,在java.beans包中提供了一个PropertyEditor接口来进行数据转换(只能用于字符串和Java对象的转换)。其功能就是将一个字符串转换为一个Java对象。

Spring 3.0,添加了一个通用的类型转换模块,位于org.springframework.core.convert包中。

2.1 ConversionServiceboolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType)

org.springframework.core.convert.ConversionService是Spring类型转换的核心接口。

  • boolean canConvert(Class<?> sourceType, Class<?> targetType)

判断是否可以将一个Java类转换为另一个Java类

  • boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType)

TypeDescriptor不但描述了需要转换类的信息,还描述了类的上下文信息。这样可以利用这些信息做出更多的各种灵活的控制。

  • <T> T convert(Object source, Class<T> targetType)

将源类型对象转换为目标类型对象

  • Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType)

将源类型从源类型对象转换为目标类型对象,通常利用到类中的上下文信息

可以利用org.springframework.context.support.ConversionServiceFactoryBean在Spring的上下文中定义一个ConversionService。Spring将自动识别出上下文种的ConversionService,并在Spring MVC处理方法的参数绑定中使用它进行数据转换。

<bean class="org.springframework.context.support.ConversionServiceFactoryBean"/>

2.2 Spring支持的转换器

Spring 在org.springframework.core.convert.converter包中定义了3种类型的转换器接口,我们可以实现其中任意一种转换接口,并将它作为自定义转换器注册到ConversionServiceFactoryBean当中。

  • Converter<S, T>

Spring中最简单的一个转换器接口。该方法负责将S类型转换为T类型的对象。

  • ConverterFactory<S, R>

如果希望将一种类型的对象转换为另一种类型及其子类对象,比如将String类型转换为Number以及Number的子类Integer、Double等对象。就需要一系列的Converter。该接口的作用就是将这一系列的相同的Converter封装在一起。

  • GenericConverter

Converter<S,T>只是负责将一个类型的对象转换为另一个类型的对象,它并没有考虑类型对象的上下文信息。因此不能完成复杂类型的转换工作。而该接口会根据源类型的对象及其上下文进行类型转换。

Code

public class User implements Serializable {
    private String loginname;

    private Date birthday;

    public String getLoginname() {
        return loginname;
    }

    public void setLoginname(String loginname) {
        this.loginname = loginname;
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        this.birthday = birthday;
    }
}
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Sign Up</title>
</head>
<body>
<form action="/user/register" method="post">
    <table>
        <tr>
            <td><label>登录名:</label></td>
            <td><input type="text" id="loginname" name="loginname"></td>
        </tr>
        <tr>
            <td><label>生日:</label></td>
            <td><input type="text" id="birthday" name="birthday"></td>
        </tr>
        <tr>
            <td>
                <input type="submit" id="submit" value="登录">
            </td>
        </tr>
    </table>
</form>
</body>
</html>
@RequestMapping(value = "register", method = RequestMethod.POST)
public String register(@ModelAttribute User user, Model model) {
    model.addAttribute("user", user);
    return "success";
}

这时候,前台输入的生日为String格式的。而User实体定义的是Date时间类型的。那么后台再接收的时候,就会报错。

这时候,我们就自定义类型转换器。实现ConversionService里面的最简单的Converter<S,T>

public class StringToDateConverter implements Converter<String, Date> {

    /**
     * 日期类型模板,如yyyy-MM-dd
     */
    private String datePattern;

    public void setDatePattern(String datePattern) {
        this.datePattern = datePattern;
    }

    @Override
    public Date convert(String date) {
        Date result = null;
        try {
            SimpleDateFormat dateFormat = new SimpleDateFormat(this.datePattern);
            result = dateFormat.parse(date);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return result;
    }
}
  • 定义xml节点
<!--装配自定义的类型转换器-->
<mvc:annotation-driven conversion-service="conversionService"/>

<!--自定义Date类型转换器-->
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <bean class="utils.StringToDateConverter" p:datePattern="yyyy-MM-dd"/>
    </property>
</bean>

注册方法

  • 1.InitBinder(不推荐使用java.beans.PropertyEditor)

刚才上面的注册方式是通过xml配置进行的操作,那么我们可以不借助xml配置,使用@InitBinder添加自定义编辑转换数据。这里就用到了Java自身的PropertyEditor类。

  • 自定义方法实现PropertyEditor的相关类
public class DateEditor extends PropertyEditorSupport {

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        try {
            Date date = dateFormat.parse(text);
            setValue(date);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}
  • 在控制器处进行注册初始化
@InitBinder
public void initBinder(WebDataBinder binder){
    binder.registerCustomEditor(Date.class,new DateEditor());
}

  • 2.WebBindingInitializer(不推荐使用java.beans.PropertyEditor)

如果这个数据转换需要在系统多处使用,那么这个自定义转换器方法需要进行全局注册。使用WebBindingInitializer进行全局范围的注册。

3.数据格式化(Fommatter<T>)

Spring使用Converter转换器进行源类型对象到目标类型对象的转换。但是Converter并不能够进行输入或输出的信息的格式化。

Spring 3.0引入格式化转换框架,org.springframework.format,Formatte<T>为最重要的接口。

Converter接口是完成任意Object与Object之间的转换,而Formatter是完成任意Object与String的转换。所以Formatter接口更适合在Web层,处理用户表单提交的数据格式化。

Formatter<T>接口,完成T类型对象的格式化和解析功能。

public interface Formatter<T> extends Printer<T>, Parser<T> {

}

3.1重要接口

  • Printer<T>

格式化显示接口

  • Parser<T>
T parse(String text, Locale locale) throws ParseException;

解析接口,参考本地信息,将一个格式化后的字符串转换为T类型的对象。

  • Formatter<T>

Formatter<T>继承上面两个接口类,具备所继承接口的所有功能。

  • FormatterRegistrar

注册格式化转换器。一般很少单独用到,我们一般用到的FormattingConversionServiceFactoryBean这个里面已经封装了这个接口对象。

  • AnnotationFormatterFactory<A extends Annotation>

注解驱动的字段格式化工厂,用于创建带注解的对象字段的Printer和Parser。即是用于格式化和解析带注释的对象字段。

//注解A的应用范围,哪些属性类可以标注A注解
Set<Class<?>> getFieldTypes();
//根据A注解,获取特定属性类型Printer
Printer<?> getPrinter(A annotation, Class<?> fieldType);
//根据A注解,获取特定属性类型Parser
Parser<?> getParser(A annotation, Class<?> fieldType);

3.2 自定义实现Formatter接口(了解下实现方式)

  • 后台代码
public class DateFormatter implements Formatter<Date> {

    private String datePattern;

    private SimpleDateFormat dateFormat;

    public DateFormatter(String datePattern) {
        this.datePattern = datePattern;
        dateFormat = new SimpleDateFormat(datePattern);

    }

    @Override
    public Date parse(String text, Locale locale) throws ParseException {
        Date result = null;
        try {
            result = dateFormat.parse(text);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return result;
    }

    @Override
    public String print(Date object, Locale locale) {
        return dateFormat.format(object);
    }
}
  • xml配置
<!--装配自定义格式转化器-->
<mvc:annotation-driven conversion-service="conversionService"/>
<!--配置自定义格式化转换器bean-->
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
        <bean class="utils.DateFormatter" c:datePattern="yyyy-MM-dd"/>
    </property>
</bean>

3.3 使用系统内置的转换器(推荐这种方式)

当然Spring本身就提供了很多内置的转换器,不需要我们再写多余的代码。比如上面我们自定义的时间格式转化器。Spring内置的org.springframework.format.datetime包中就有对应的DateFormatter实现类。

我们只需要定义xml就可以了,如下。

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
        <bean class="org.springframework.format.datetime.DateFormatter" c:pattern="yyyy-MM-dd"/>
    </property>
</bean>

3.4 自定义使用FormatterRegister注册Formatter(只需要了解,无须掌握,太麻烦)

前面我们直接在xml里面注册Formatter实现类,那么我们还可以直接在xml里面注册Registrar,来替代直接注册Formatter。

  • 后台代码

自定义出FormatterRegistrar类

public class CustomerFormatterRegistrar implements FormatterRegistrar {

    private DateFormatter dateFormatter;

    public CustomerFormatterRegistrar(DateFormatter dateFormatter) {
        this.dateFormatter = dateFormatter;
    }

    @Override
    public void registerFormatters(FormatterRegistry registry) {
        registry.addFormatter(dateFormatter);
    }
}

自定义出DateFormmater类

public class DateFormatter implements Formatter<Date> {

    private String datePattern;

    private SimpleDateFormat dateFormat;

    public DateFormatter(String datePattern) {
        this.datePattern = datePattern;
        dateFormat = new SimpleDateFormat(datePattern);

    }

    @Override
    public Date parse(String text, Locale locale) throws ParseException {
        Date result = null;
        try {
            result = dateFormat.parse(text);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return result;
    }

    @Override
    public String print(Date object, Locale locale) {
        return dateFormat.format(object);
    }
}
  • xml配置
<!--装配自定义格式转化器-->
<mvc:annotation-driven conversion-service="conversionService"/>
<!--在Spring上下文定义出自定义的时间转化器组件-->
<bean id="dateFormatter" class="utils.DateFormatter" c:datePattern="yyyy-MM-dd"></bean>

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatterRegistrars">
        <bean class="utils.CustomerFormatterRegistrar" c:dateFormatter-ref="dateFormatter"></bean>
    </property>
</bean>

3.5 使用注解的方式来进行格式化工作(AnnotationFormatterFactory<A extends Annotation>)

前面的例子无论是自定义实现数据格式工作还是使用系统内置的类,都需要通过进行繁琐的xml配置。现在我们直接使用注解_Annotation的方式进行实现格式化工作。

org.springframework.format.annotation 定义了两个格式化的注解类型

  • 1.DateTimeFormat

@DateTimeFormat 注解可以对java.util.Date、java.util.Calendar等时间类型的属性进行标注。该类支持下面三种互斥属性

==互斥属性指的是只能拥有其一,不然同时具备。==

//自定义时间格式
private String pattern;

private String stylePattern;

private ISO iso;

stylePattern

/**
 * Set the two character to use to format date values. The first character used for
 * the date style, the second is for the time style. Supported characters are
 * <ul>
 * <li>'S' = Small</li>短日期/时间的样式
 * <li>'M' = Medium</li>中日期/时间的样式
 * <li>'L' = Long</li>长日期/时间的样式
 * <li>'F' = Full</li>完整日期/时间的样式
 * <li>'-' = Omitted</li>忽略日期/时间的样式
 * <ul>
 * This method mimics the styles supported by Joda-Time.
 * @param stylePattern two characters from the set {"S", "M", "L", "F", "-"}
 * @since 3.2
 */
public void setStylePattern(String stylePattern) {
    this.stylePattern = stylePattern;
}

IOS几种可选值

formats.put(ISO.DATE, "yyyy-MM-dd");
formats.put(ISO.TIME, "HH:mm:ss.SSSZ");
formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSZ");
  • 2.NumberFormat

NumberFormat可对类似数字类型的属性进行标注,它拥有两个互斥的属性。

String pattern()

Style style()

style可选枚举值

enum Style {

    /**
     * The default format for the annotated type: typically 'number' but possibly
     * 'currency' for a money type (e.g. {@code javax.money.MonetaryAmount)}.
     * @since 4.2
     */
    DEFAULT,

    /**
     * The general-purpose number format for the current locale.
     */
    NUMBER,

    /**
     * The percent format for the current locale.
     */
    PERCENT,

    /**
     * The currency format for the current locale.
     */
    CURRENCY
}

代码演示

  • 后台代码新建需要进行数据转换及格式化的类。省略了get、set方法
public class User implements Serializable {
    private String loginname;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthday;

    /**
     * 薪水,以财务格式接收
     */
    @NumberFormat(style = NumberFormat.Style.NUMBER, pattern = "#,###")
    private double salary;

    /**
     * 业绩完成比例
     */
    @NumberFormat(style = NumberFormat.Style.PERCENT)
    private double performance;

    /**
     * 薪水的货币类型展示
     */
    @NumberFormat(style = NumberFormat.Style.CURRENCY)
    private double salaryDisplay;
}
  • xml配置
<mvc:annotation-driven/>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容