SpringMVC多种数据类型转换器 Day30 2018-12-20

SpringMVC 数据转换

  • springMVC数据绑定流程
  • 使用ConversionService转换数据
  • 使用PropertyEditor@InitBinder转换数据
  • 使用WebBindingInitializer转换数据

1. springMVC数据绑定流程

    1. Spring MVC 主框架将 ServletRequest对象及目标方法的入参实例传递给WebDataBinderFactory实例,以创建DataBinder实例对象。
    2. DataBinder调用装配在Spring Web上下文的ConversionService组件进行 数据类型转换、数据格式化 工作。将ServletRequest 中的请求信息填充到入参对象中。
    3. 调用 Validtor 组件对已经绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果BindingResult对象。
    4. Spring MVC 抽取BindingResult 中的入参对象和校验错误对象,将它们赋给处理方法的响应入参。
  • Spring MVC 通过反射机制对目标处理方法进行解析,将请求消息绑定到处理方法的入参中。数据绑定的核心部件是DataBinder,运行机制如图片所示:

SpringMVC 数据绑定机制

2 数据转换异常

2.1 实体类中的变量为java.util.Date类型

  • 未配置自定义字符转换器时报的异常:
Field error in object 'user' on field 'birthday': rejected value [1995-10-10]; codes [typeMismatch.user.birthday,typeMismatch.birthday,typeMismatch.java.util.Date,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.birthday,birthday]; arguments []; default message [birthday]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Date' for property 'birthday'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [java.util.Date] for value '1995-10-10'; nested exception is java.lang.IllegalArgumentException]]
  • 解决方式

springmvc-config.xml中添加自定义字符转换器配置即可解决。传入字符串1995-10-10,实体变量接收到转换的数据应为:Tue Oct 10 00:00:00 CST 1995

2.2 实体类中的变量为java.sql.Date类型

正常情况下实体类参数不应该为java.sql.Date类型。

  • 传入字符串1995-10-10,实体类接收为birthday=1995-10-10,不报错
  • 传入其他格式1995-10-10 14:30:301995/10/10等均报错

查看源码可知,只支持传入(yyyy-[m]m-[d]d)格式:

/**
     * Converts a string in JDBC date escape format to
     * a <code>Date</code> value.
     *
     * @param s a <code>String</code> object representing a date in
     *        in the format "yyyy-[m]m-[d]d". The leading zero for <code>mm</code>
     * and <code>dd</code> may also be omitted.
     * @return a <code>java.sql.Date</code> object representing the
     *         given date
     * @throws IllegalArgumentException if the date given is not in the
     *         JDBC date escape format (yyyy-[m]m-[d]d)
     */
    public static Date valueOf(String s) {
        final int YEAR_LENGTH = 4;
        final int MONTH_LENGTH = 2;
        final int DAY_LENGTH = 2;
        final int MAX_MONTH = 12;
        final int MAX_DAY = 31;
        int firstDash;
        int secondDash;
        Date d = null;
        if (s == null) {
            throw new java.lang.IllegalArgumentException();
        }

        firstDash = s.indexOf('-');
        secondDash = s.indexOf('-', firstDash + 1);

        if ((firstDash > 0) && (secondDash > 0) && (secondDash < s.length() - 1)) {
            String yyyy = s.substring(0, firstDash);
            String mm = s.substring(firstDash + 1, secondDash);
            String dd = s.substring(secondDash + 1);
            if (yyyy.length() == YEAR_LENGTH &&
                    (mm.length() >= 1 && mm.length() <= MONTH_LENGTH) &&
                    (dd.length() >= 1 && dd.length() <= DAY_LENGTH)) {
                int year = Integer.parseInt(yyyy);
                int month = Integer.parseInt(mm);
                int day = Integer.parseInt(dd);

                if ((month >= 1 && month <= MAX_MONTH) && (day >= 1 && day <= MAX_DAY)) {
                    d = new Date(year - 1900, month - 1, day);
                }
            }
        }
        if (d == null) {
            throw new java.lang.IllegalArgumentException();
        }

        return d;

    }

3. 使用ConversionService转换数据

3.1 自定义类型转换器实现Converter<S,T>接口

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

import org.springframework.core.convert.converter.Converter;
// 实现Converter<S,T>接口
public class StringToDateConverter implements Converter<String, Date> {
    //日期类型模板。如:"yyyy-MM-dd"
    private String datePattern;
    
    public void setDatePattern(String datePattern) {
        this.datePattern = datePattern;
    }

    // Converter<S,T>接口的类型转换方法
    @Override
    public Date convert(String date) {
        try {
            SimpleDateFormat sdf = new SimpleDateFormat(this.datePattern);
            // 将日期字符串转换成Date类型返回
            return sdf.parse(date);
        } catch (ParseException e) {
            e.printStackTrace();
            System.out.println("日期转换失败!");
            return null;
        }
    }

}

3.2 springMVC配置文件中配置

  • springmvc-config.xml
<!-- 装配自定义的类型转换器 -->
    <mvc:annotation-driven conversion-service="conversionService"/>
    
    <!-- 自定义的类型转换器 -->
     <bean id="conversionService" 
        class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <list>
                <bean class="com.zhougl.web.converter.StringToDateConverter"
                p:datePattern="yyyy-MM-dd"></bean>
            </list>
        </property>
    </bean>
  • 注必须引入声明xmlns:p="http://www.springframework.org/schema/p",否则会报错:与元素类型 "bean" 相关联的属性 "p:datePattern" 的前缀 "p" 未绑定。

3.3 测试代码

3.3.1 实体类
public class User implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 8393614516445523837L;
    private String loginName;
    private Date birthday;
    
    public User() {
        super();
    }
    public User(String loginName,Date birthday) {
        super();
        this.loginName = loginName;
        this.birthday = 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;
    }

}

3.3.2 Controller控制层
@Controller
@RequestMapping(value="/converterTest")
public class UserController {
    private static final Log logger = LogFactory.getLog(UserController.class);
    @RequestMapping(value="/{formName}")
    public String loginForm(@PathVariable String formName) {
        //动态跳转页面
        return "converterTest/"+formName;
    }
    @RequestMapping(value="/register",method=RequestMethod.POST)
    public String register(@ModelAttribute User user,Model model) {
        logger.info(user);
        model.addAttribute("user", user);
        return "converterTest/success";
    }
}

3.3.3 jsp
  • /converterTest/registerForm.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>测试数据转换器</title>
</head>
<body>
    <h3>注册页面</h3>
    <form action="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>
  • /converterTest/success.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>测试ConversionService</title>
</head>
<body>
登录名:${requestScope.user.loginName }<br>
生日:<fmt:formatDate value="${requestScope.user.birthday}" 
    pattern="yyyy年MM月dd日"/><br>
</body>
</html>
  • 注:使用jstl标签需要引入jstl.jar,否则会报错:org.apache.jasper.JasperException: The absolute uri: http://java.sun.com/jsp/jstl/fmt cannot be resolved in either web.xml or the jar files deployed with this application

4. 使用PropertyEditor转换数据

PropertyEditor是属性编辑器的接口,它规定了将外部设置值转换为内部JavaBean属性值的转换接口方法。PropertyEditor主要的接口方法说明如下:

  • Object getValue():返回属性的当前值。基本类型被封装成对应的包装类实例;
  • void setValue(Object newValue):设置属性的值,基本类型以包装类传入(自动装箱);
  • String getAsText():将属性对象用一个字符串表示,以便外部的属性编辑器能以可视化的方式显示。缺省返回null,表示该属性不能以字符串表示;
  • void setAsText(String text):用一个字符串去更新属性的内部值,这个字符串一般从外部属性编辑器传入;
  • String[] getTags():返回表示有效属性值的字符串数组(如boolean属性对应的有效Tagtruefalse),以便属性编辑器能以下拉框的方式显示出来。缺省返回null,表示属性没有匹配的字符值有限集合;
  • String getJavaInitializationString():为属性提供一个表示初始值的字符串,属性编辑器以此值作为属性的默认值。

4.1 自定义属性编辑器继承PropertyEditorSupport

//自定义属性编辑器
public class DateEditor extends PropertyEditorSupport {
    private String datePattern = "yyyy-MM-dd";

    public DateEditor(String strPattern) {
        this.datePattern = strPattern;
    }

    public String getDatePattern() {
        return datePattern;
    }

    public void setDatePattern(String datePattern) {
        this.datePattern = datePattern;
    }
    // 将传如的字符串数据转换成Date类型
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        SimpleDateFormat sdf = new SimpleDateFormat(this.datePattern);
        try {
            Date date = sdf.parse(text);
            super.setValue(date);//设置属性的值
        } catch (ParseException e) {
            e.printStackTrace();
        }
        
    }
    
}

4.2 使用@InitBinder在控制层中初始化编辑器

在Controller层增加

 // 在控制器初始化时注册属性编辑器
     @InitBinder
      public void initBinder(WebDataBinder binder){
        // 注册自定义编辑器
        //binder.registerCustomEditor(Date.class, new DateEditor());
        binder.registerCustomEditor(Date.class, new DateEditor("yyyy-MM-dd HH:mm:ss"));
      }

4.3 利用CustomEditorConfigurer类配置初始化

CustomEditorConfigurer 类 用于实现在Spring 中注册自己定义的编辑器 。它是Spring 当中一个非常有用的工厂后处理类(工厂后处理通过SpringBeanFactoryPostProcessor 接口实现, 它是在Spring容器启动并初始化之后进行对Spring 容器的操作类)。在Spring中已经注册了不少编辑器类,他们都用于String 类型转换为其他的数据类型,如URL,Date等。

配置CustomEditorConfigurer 类:

CustomEditorConfigurer类中有一个customEditor属性,它是一个Map 类型。通过配置它便实现了自定义的编辑器注册。这个Map 的键值对对应着转换类型和编辑器(转换类型是Key,编辑器是Value)。

自定义编辑器可以简化Spring 的装配Bean。使其更加的简单。不容易发生配置错误。 PS:如果使用SpringApplicationContext容器,那么只需在Spring 的配置文件中进行简单的装配,而对于Bean 工厂可能需要手动的注册才能使用。

  • DateEditor.java
//自定义属性编辑器
public class DateEditor extends PropertyEditorSupport {
    private String datePattern = "yyyy-MM-dd";

    public String getDatePattern() {
        return datePattern;
    }
    public void setDatePattern(String datePattern) {
        this.datePattern = datePattern;
    }
    // 将传如的字符串数据转换成Date类型
    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        SimpleDateFormat sdf = new SimpleDateFormat(this.datePattern);
        try {
            Date date = sdf.parse(text);
            super.setValue(date);//设置属性的值
        } catch (ParseException e) {
            e.printStackTrace();
        }
        
    }
    public DateEditor(String datePattern) {
        super();
        this.datePattern = datePattern;
    }
    
}
  • DateEditorRegistrar.java
import java.beans.PropertyEditor;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.PropertyEditorRegistrar;
import org.springframework.beans.PropertyEditorRegistry;

public class DateEditorRegistrar implements PropertyEditorRegistrar {
    private Map<Class<?>, PropertyEditor> customEditors;
    @Override
    public void registerCustomEditors(PropertyEditorRegistry registry) {
        if (customEditors != null) {
            Set<Map.Entry<Class<?>, PropertyEditor>> entries = customEditors.entrySet();
            for (Map.Entry<Class<?>, PropertyEditor> entry : entries) {
                registry.registerCustomEditor(entry.getKey(), entry.getValue());
                }
            } 
    }
    public Map<Class<?>, PropertyEditor> getCustomEditors() {
        return customEditors;
    }
    public void setCustomEditors(Map<Class<?>, PropertyEditor> customEditors) {
        this.customEditors = customEditors;
    }

}
  • spring 配置1
<bean id="customEditorRegistrar"
        class="com.zhougl.web.converter.DateEditorRegistrar">
        <property name="customEditors">
            <map>
                <entry key="java.util.Date" value-ref="customDateEditor" />
            </map>
        </property>
    </bean>
    <bean id="customDateEditor"
        class="com.zhougl.web.converter.DateEditor">
        <constructor-arg type="String"
            value="yyyy-MM-dd HH:mm:ss" />
    </bean>
    <bean id="customEditorConfigurer"
        class="org.springframework.beans.factory.config.CustomEditorConfigurer">
        <property name="propertyEditorRegistrars">
            <list>
                <ref bean="customEditorRegistrar" />
            </list>
        </property>
    </bean>   
  • spring 配置2
<bean id="user" class="com.zhougl.web.bean.User">
    <property name="loginName" value="Lokesh" />
    <property name="password" value="Gupta" />
    <property name="userName" value="Manager" />
    <property name="birthday" value="2007-09-30 14:30:30" />
</bean>

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
       <property name="customEditors">
         <map>
           <entry key="java.util.Date" value="com.zhougl.web.converter.DateEditor"/>
         </map>
       </property>
     </bean>
  • 测试类
public void test() {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
         
        User user = (User) context.getBean("user");
         
        System.out.println(user.getBirthday());
        System.out.println(user.toString());
    }

总结
以上配置,在项目中均不起作用,只有配置2在测试类中起作用,配置1不起作用。有待研究。

5. 使用WebBindingInitializer转换数据

可以通过实现WebBindingInitializer接口,实现类中的自定义编辑器,注册全局自定义编辑器转换数据。

5.1 实现WebBindingInitializer接口

//实现WebBindingInitializer接口
public class DateBindingInitializer implements WebBindingInitializer {

    @Override
    public void initBinder(WebDataBinder binder) {
        // 注册自定义编辑器
                binder.registerCustomEditor(Date.class, new DateEditor("yyyy-MM-dd HH:mm:ss"));
    }

}
  • DateEditor同4.1

5.2 springmvc配置

  • spring-webmvc-4.2.0
<!-- 通过AnnotationMethodHandlerAdapter装配自定义编辑器 -->
    <bean
        class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="webBindingInitializer">
            <bean class="com.zhougl.web.binding.DateBindingInitializer" />
        </property>
    </bean>
    
  • spring5之后AnnotationMethodHandlerAdapterorg.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter替代;使用<mvc:annotation-driven>扫描时,自动注册了该类,<mvc:annotation-driven />配置中已经包含了webBindingInitializer的配置,所以该配置放在<mvc:annotation-driven />后面不会起作用。
  • <mvc:annotation-driven /> 必须放到最后,否则全局binder不起作用。
  • @InitBinder功能一致,只是此处为注册全局变量。

6. 多种转换器的优先顺序

    1. 查询通过@InitBinder装配的自定义编辑器
    2. 查询通过ConversionService装配的自定义转换器
    3. 查询通过WebBindingInitializer接口装配的全局自定义编辑器

总结:

使用时<mvc:annotation-driven />配置时,使用conversion-service来注册自定义的converter实现自定义的类型转换,即采用3小节使用的方法

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

推荐阅读更多精彩内容