10|数据绑定: 如何自动转换传入的参数?

你好,我是郭屹。今天我们继续手写MiniSpring,这节课我们讨论传入参数的转换问题。

上节课,我们已经基本完成了对Dispatcher的扩展,用HanderMapping来处理映射关系,用HandlerAdapter来处理映射后具体方法的调用。

在处理请求的过程中,我们用ServletRequest接收请求参数,而获取参数用的是getParameter()方法,它的返回值是String字符串,这也意味着无论是获取字符串参数、数字参数,还是布尔型参数,它获取到的返回值都是字符串。而如果要把请求参数转换成Java对象,就需要再处理,那么每一次获取参数后,都需要显式地编写大量重复代码,把String类型的参数转换成其他类型。显然这不符合我们对框架的期望,我们希望框架能帮助我们自动处理这些常规数据格式的转换。

再扩大到整个访问过程,后端处理完毕后,返回给前端的数据再做返回,也存在格式转换的问题,传入传出两个方向我们都要处理。而这节课我们讨论的重点是“传入”方向。

传入参数的绑定

我们先考虑传入方向的问题:请求参数怎么和Java对象里的属性进行自动映射?

这里,我们引入WebDataBinder来处理。这个类代表的是一个内部的目标对象,用于将Request请求内的字符串参数转换成不同类型的参数,来进行适配。所以比较自然的想法是这个类里面要持有一个目标对象target,然后还要定义一个bind()方法,通过来绑定参数和目标对象,这是WebDataBinder里的核心。

1
2
3
4
5
    public void bind(HttpServletRequest request) {
        PropertyValues mpvs = assignParameters(request);
        addBindValues(mpvs, request);
        doBind(mpvs);
    }

通过bind方法的实现,我们可以看出,它主要做了三件事。

  1. 把Request里的参数解析成PropertyValues。
  2. 把Request里的参数值添加到绑定参数中。
  3. 把两者绑定在一起。

你可以看一下WebDataBinder的详细实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.minis.web;

import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import com.minis.beans.PropertyValues;
import com.minis.util.WebUtils;

public class WebDataBinder {
    private Object target;
    private Class<?> clz;
    private String objectName;
    public WebDataBinder(Object target) {
        this(target, "");
    }
    public WebDataBinder(Object target, String targetName) {
        this.target = target;
        this.objectName = targetName;
        this.clz = this.target.getClass();
    }
    //核心绑定方法,将request里面的参数值绑定到目标对象的属性上
    public void bind(HttpServletRequest request) {
        PropertyValues mpvs = assignParameters(request);
        addBindValues(mpvs, request);
        doBind(mpvs);
    }
    private void doBind(PropertyValues mpvs) {
        applyPropertyValues(mpvs);
    }
    //实际将参数值与对象属性进行绑定的方法
    protected void applyPropertyValues(PropertyValues mpvs) {
        getPropertyAccessor().setPropertyValues(mpvs);
    }
    //设置属性值的工具
    protected BeanWrapperImpl getPropertyAccessor() {
        return new BeanWrapperImpl(this.target);
    }
    //将Request参数解析成PropertyValues
    private PropertyValues assignParameters(HttpServletRequest request) {
        Map<String, Object> map = WebUtils.getParametersStartingWith(request, "");
        return new PropertyValues(map);
    }
    protected void addBindValues(PropertyValues mpvs, HttpServletRequest request) {
    }
}

在处理请求参数的过程中,首先通过调用assignParameters()方法将请求中的参数转换为内存中的一个Map对象。这一过程利用了底层的WebUtils工具类来简化操作。

核心的操作在于getPropertyAccessor().setPropertyValues(mpvs);这一步骤。这里,getPropertyAccessor返回的是一个内置的BeanWrapperImpl实例,该实例中包含了目标对象(target)。BeanWrapperImpl作为Bean的一个包装实现类,它的作用是将属性映射(map)绑定到目标对象上。

对于单个参数的转换过程而言,它通常涉及到从字符串类型到其他数据类型的转变,因为请求参数往往是以字符串形式传递的。为了支持这种灵活的数据转换,可以定义一个通用接口PropertyEditor。这个接口提供了必要的方法,以实现字符串与其他对象之间的双向转换。

1
2
3
4
5
6
7
8
package com.minis.beans;

public interface PropertyEditor {
    void setAsText(String text);
    void setValue(Object value);
    Object getValue();
    Object getAsText();
}

现在我们来定义两个PropertyEditor的实现类:CustomNumberEditor和StringEditor,分别处理Number类型和其他类型,并进行类型转换。你可以看一下CustomNumberEditor的相关源码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.minis.beans;

import java.text.NumberFormat;
import com.minis.util.NumberUtils;
import com.minis.util.StringUtils;

public class CustomNumberEditor implements PropertyEditor{
    private Class<? extends Number> numberClass; //数据类型
    private NumberFormat numberFormat; //指定格式
    private boolean allowEmpty;
    private Object value;
    public CustomNumberEditor(Class<? extends Number> numberClass, boolean allowEmpty) throws IllegalArgumentException {
        this(numberClass, null, allowEmpty);
    }
    public CustomNumberEditor(Class<? extends Number> numberClass, NumberFormat numberFormat, boolean allowEmpty) throws IllegalArgumentException {
        this.numberClass = numberClass;
        this.numberFormat = numberFormat;
        this.allowEmpty = allowEmpty;
    }
    //将一个字符串转换成number赋值
    public void setAsText(String text) {
		if (this.allowEmpty && !StringUtils.hasText(text)) {
			setValue(null);
		}
		else if (this.numberFormat != null) {
			// 给定格式
			setValue(NumberUtils.parseNumber(text, this.numberClass, this.numberFormat));
		}
		else {
			setValue(NumberUtils.parseNumber(text, this.numberClass));
		}
    }
    //接收Object作为参数
    public void setValue(Object value) {
        if (value instanceof Number) {
            this.value = (NumberUtils.convertNumberToTargetClass((Number) value, this.numberClass));
        }
        else {
            this.value = value;
        }
    }
    public Object getValue() {
        return this.value;
    }
    //将number表示成格式化串
    public Object getAsText() {
        Object value = this.value;
		if (value == null) {
			return "";
		}
		if (this.numberFormat != null) {
			// 给定格式.
			return this.numberFormat.format(value);
		}
		else {
			return value.toString();
		}
    }
}

整体实现也比较简单,在内部定义一个名为value的域,接收传入的格式化text或者value值。如果遇到的值是Number类型的子类,比较简单,就进行强制转换。这里我们用到了一个底层工具类NumberUtils,它提供了一个NumberUtils.parseNumber(text, this.numberClass, this.numberFormat)方法,方便我们在数值和文本之间转换。你可以看下StringEditor实现的相关源代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.minis.beans;

import java.text.NumberFormat;
import com.minis.util.NumberUtils;
import com.minis.util.StringUtils;

public class StringEditor implements PropertyEditor{
    private Class<String> strClass;
    private String strFormat;
    private boolean allowEmpty;
    private Object value;
    public StringEditor(Class<String> strClass,
                        boolean allowEmpty) throws IllegalArgumentException {
         this(strClass, "", allowEmpty);
    }
    public StringEditor(Class<String> strClass,
                        String strFormat, boolean allowEmpty) throws IllegalArgumentException {
        this.strClass = strClass;
        this.strFormat = strFormat;
        this.allowEmpty = allowEmpty;
    }
    public void setAsText(String text) {
        setValue(text);
    }
    public void setValue(Object value) {
        this.value = value;
    }
    public String getAsText() {
        return value.toString();
    }
    public Object getValue() {
        return this.value;
    }
}

StringEditor的实现

StringEditor类主要负责字符串的处理。它的构造函数支持传入一个字符串格式strFormat,这个参数为后续的类型转换提供了灵活性。

构造函数特点

  • 支持传入字符串格式strFormat,为类型转换预留了接口。

BeanWrapperImpl的实现

BeanWrapperImpl类是实现的关键。它利用了基本类型的Editor类作为工具,以实现更复杂的功能。

关键功能

  • 利用基本类型的Editor类作为工具。

  • 实现了对Bean属性的封装和访问。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package com.minis.web;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import com.minis.beans.PropertyEditor;
import com.minis.beans.PropertyEditorRegistrySupport;
import com.minis.beans.PropertyValue;
import com.minis.beans.PropertyValues;

public class BeanWrapperImpl extends PropertyEditorRegistrySupport {
    Object wrappedObject; //目标对象
    Class<?> clz;
    PropertyValues pvs; //参数值
    public BeanWrapperImpl(Object object) {
        registerDefaultEditors(); //不同数据类型的参数转换器editor
        this.wrappedObject = object;
        this.clz = object.getClass();
    }
    public void setBeanInstance(Object object) {
        this.wrappedObject = object;
    }
    public Object getBeanInstance() {
        return wrappedObject;
    }
    //绑定参数值
    public void setPropertyValues(PropertyValues pvs) {
        this.pvs = pvs;
        for (PropertyValue pv : this.pvs.getPropertyValues()) {
          setPropertyValue(pv);
        }
    }
    //绑定具体某个参数
    public void setPropertyValue(PropertyValue pv) {
        //拿到参数处理器
        BeanPropertyHandler propertyHandler = new BeanPropertyHandler(pv.getName());
        //找到对该参数类型的editor
        PropertyEditor pe = this.getDefaultEditor(propertyHandler.getPropertyClz());
        //设置参数值
        pe.setAsText((String) pv.getValue());
        propertyHandler.setValue(pe.getValue());
    }
    //一个内部类,用于处理参数,通过getter()和setter()操作属性
    class BeanPropertyHandler {
        Method writeMethod = null;
        Method readMethod = null;
        Class<?> propertyClz = null;
        public Class<?> getPropertyClz() {
            return propertyClz;
        }
        public BeanPropertyHandler(String propertyName) {
			try {
                //获取参数对应的属性及类型
                Field field = clz.getDeclaredField(propertyName);
                propertyClz = field.getType();
                //获取设置属性的方法,按照约定为setXxxx()
                this.writeMethod = clz.getDeclaredMethod("set" +
    propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1), propertyClz);
                //获取读属性的方法,按照约定为getXxxx()
                this.readMethod = clz.getDeclaredMethod("get" +
    propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1), propertyClz);
            } catch (Exception e) {
				e.printStackTrace();
			}        }
        //调用getter读属性值
        public Object getValue() {
            Object result = null;
            writeMethod.setAccessible(true);
			try {
                result = readMethod.invoke(wrappedObject);
			} catch (Exception e) {
				e.printStackTrace();
			}
            return result;
        }
        //调用setter设置属性值
        public void setValue(Object value) {
            writeMethod.setAccessible(true);
			try {
                writeMethod.invoke(wrappedObject, value);
			} catch (Exception e) {
				e.printStackTrace();
			}
        }
    }
}

这个类的核心在于利用反射对Bean属性值进行读写,具体是通过setter和getter方法。但具体的实现,则有赖于继承的PropertyEditorRegistrySupport这个类。我们再来看看PropertyEditorRegistrySupport是如何实现的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.minis.beans;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class PropertyEditorRegistrySupport {
    private Map<Class<?>, PropertyEditor> defaultEditors;
    private Map<Class<?>, PropertyEditor> customEditors;
    //注册默认的转换器editor
    protected void registerDefaultEditors() {
        createDefaultEditors();
    }
    //获取默认的转换器editor
    public PropertyEditor getDefaultEditor(Class<?> requiredType) {
        return this.defaultEditors.get(requiredType);
    }
    //创建默认的转换器editor,对每一种数据类型规定一个默认的转换器
    private void createDefaultEditors() {
        this.defaultEditors = new HashMap<>(64);
        // Default instances of collection editors.
        this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false));
        this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true));
        this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false));
        this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true));
        this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false));
        this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true));
        this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false));
        this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true));
        this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true));
        this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true));
        this.defaultEditors.put(String.class, new StringEditor(String.class, true));
    }
    //注册客户化转换器
    public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) {
        if (this.customEditors == null) {
            this.customEditors = new LinkedHashMap<>(16);
        }
        this.customEditors.put(requiredType, propertyEditor);
    }
    //查找客户化转换器
    public PropertyEditor findCustomEditor(Class<?> requiredType) {
        Class<?> requiredTypeToUse = requiredType;
        return getCustomEditor(requiredTypeToUse);
    }
    public boolean hasCustomEditorForElement(Class<?> elementType) {
        return (elementType != null && this.customEditors != null && this.customEditors.containsKey(elementType));
    }
    //获取客户化转换器
    private PropertyEditor getCustomEditor(Class<?> requiredType) {
        if (requiredType == null || this.customEditors == null) {
            return null;
        }
        PropertyEditor editor = this.customEditors.get(requiredType);
        return editor;
    }
}

PropertyEditorRegistrySupport类的核心实现中,createDefaultEditors方法扮演了关键角色。此方法不仅内置了大量的基本类型或其包装类型的转换器(即Editor),还允许定义自定义的转换器,从而使得WebDataBinder能够处理多种数据类型的转换任务。目前我们的实现主要集中在支持数字和字符串等几个基础类型的数据转换上,并且暂未扩展至数组、列表以及映射(map)等复杂结构的数据格式。

接下来的工作重点是开发一个WebDataBinderFactory。这个工厂将帮助我们更加灵活便捷地创建和配置WebDataBinder实例,进一步增强我们在不同场景下绑定请求参数到Java对象的能力。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.minis.web;

import javax.servlet.http.HttpServletRequest;

public class WebDataBinderFactory {
    public WebDataBinder createBinder(HttpServletRequest request, Object target, String objectName) {
        WebDataBinder wbd = new WebDataBinder(target, objectName);
        initBinder(wbd, request);
        return wbd;
    }
    protected void initBinder(WebDataBinder dataBinder, HttpServletRequest request) {
    }
}

数据绑定实现步骤

在Spring框架中,HTTP请求通过一系列的处理最终会映射到对应的方法上。这一过程主要通过RequestMappingHandlerAdapter中的handleInternal方法来实现,该方法会调用invokeHandlerMethod方法完成参数绑定。以下是改造invokeHandlerMethod方法以实现数据绑定的步骤:

  1. 理解请求映射
  • HTTP请求首先被映射到对应的处理器方法上,这是通过RequestMappingHandlerAdapter实现的。
  1. 调用handleInternal方法
  • 一旦请求被映射,RequestMappingHandlerAdapterhandleInternal方法会被调用来处理请求。
  1. 改造invokeHandlerMethod方法
  • invokeHandlerMethod方法中,我们可以对参数进行绑定。这通常涉及到将请求中的参数(如表单数据、JSON数据等)映射到方法的参数上。
  1. 参数解析与绑定
  • 根据请求的内容类型(如application/json),解析请求体并将其转换为相应的Java对象。

  • 使用适当的编辑器(如BeanPropertyBindingResult)将解析后的数据绑定到方法参数上。

  1. 异常处理
  • 在绑定过程中,如果发生错误(如类型不匹配、必填字段缺失等),需要适当地处理这些异常,并向客户端返回错误信息。
  1. 返回响应
  • 一旦参数绑定完成,可以继续执行处理器方法,并根据方法的返回值构建响应返回给客户端。 以上步骤展示了如何在Spring框架中改造invokeHandlerMethod方法以实现数据绑定。通过这种方式,我们可以灵活地处理各种类型的HTTP请求,并将其映射到对应的处理器方法上。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    protected void invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
        WebDataBinderFactory binderFactory = new WebDataBinderFactory();
        Parameter[] methodParameters =
handlerMethod.getMethod().getParameters();
        Object[] methodParamObjs = new Object[methodParameters.length];
        int i = 0;
        //对调用方法里的每一个参数,处理绑定
        for (Parameter methodParameter : methodParameters) {
            Object methodParamObj = methodParameter.getType().newInstance();
            //给这个参数创建WebDataBinder
            WebDataBinder wdb = binderFactory.createBinder(request,
methodParamObj, methodParameter.getName());
            wdb.bind(request);
            methodParamObjs[i] = methodParamObj;
            i++;
        }
        Method invocableMethod = handlerMethod.getMethod();
        Object returnObj = invocableMethod.invoke(handlerMethod.getBean(), methodParamObjs);
        response.getWriter().append(returnObj.toString());
    }

invokeHandlerMethod方法的实现中,methodParameters变量用于存储即将调用的方法的所有参数。对于这些参数,会进行循环处理,并且创建一个名为methodParamObj的新对象,该对象是绑定操作的目标。通过binderFactory.createBinder()可以创建一个WebDataBinder实例,它负责将请求中的参数绑定到目标对象上。当整个循环完成后,Request中的参数就被绑定到了调用方法的参数上,随后就可以执行方法了。

这个绑定过程强调了参数顺序的重要性,因为它们是按照在方法签名中出现的次序逐个绑定的。

为了支持数据类型转换,框架提供了默认的CustomNumberEditorStringEditor来处理数字和字符串类型的转换,从而能够将ServletRequest中的请求参数转换为Java对象的数据类型。然而,这种默认机制可能不够灵活,无法满足所有自定义类型的需求。

为了增强框架的扩展性,我们需要探讨如何支持自定义编辑器(Editor)。实际上,在PropertyEditorRegistrySupport类中已经预留了添加自定义转换器的空间。开发者可以通过向注册表中添加自定义的PropertyEditor来扩展数据绑定的能力,从而让框架能够处理更多的数据类型。这样,每当遇到需要特殊转换逻辑的属性时,对应的PropertyEditor就会被用来进行相应的转换。

1
2
3
public class PropertyEditorRegistrySupport {
    private Map<Class<?>, PropertyEditor> defaultEditors;
    private Map<Class<?>, PropertyEditor> customEditors;

我们利用客户化Editor这个“口子”,新建一个部件,把客户自定义的Editor注册进来就可以了。

我们先在原有的WebDataBinder 类里,增加registerCustomEditor方法,用来注册自定义的Editor,你可以看一下相关代码。

1
2
3
public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) {
      getPropertyAccessor().registerCustomEditor(requiredType, propertyEditor);
}

在这里,可以自定义属于我们自己的CustomEditor ,比如在com.test 包路径下,自定义CustomDateEditor,这是一个自定义的日期格式处理器,来配合我们的测试。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.test;

import java.text.NumberFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import com.minis.beans.PropertyEditor;
import com.minis.util.NumberUtils;
import com.minis.util.StringUtils;

public class CustomDateEditor implements PropertyEditor {
    private Class<Date> dateClass;
    private DateTimeFormatter datetimeFormatter;
    private boolean allowEmpty;
    private Date value;
	public CustomDateEditor() throws IllegalArgumentException {
		this(Date.class, "yyyy-MM-dd", true);
	}
	public CustomDateEditor(Class<Date> dateClass) throws IllegalArgumentException {
		this(dateClass, "yyyy-MM-dd", true);
	}
	public CustomDateEditor(Class<Date> dateClass,
				  boolean allowEmpty) throws IllegalArgumentException {
		this(dateClass, "yyyy-MM-dd", allowEmpty);
	}
	public CustomDateEditor(Class<Date> dateClass,
				String pattern, boolean allowEmpty) throws IllegalArgumentException {
		this.dateClass = dateClass;
		this.datetimeFormatter = DateTimeFormatter.ofPattern(pattern);
		this.allowEmpty = allowEmpty;
	}
    public void setAsText(String text) {
		if (this.allowEmpty && !StringUtils.hasText(text)) {
			setValue(null);
		}
		else {
			LocalDate localdate = LocalDate.parse(text, datetimeFormatter);
			setValue(Date.from(localdate.atStartOfDay(ZoneId.systemDefault()).toInstant()));
		}
    }
    public void setValue(Object value) {
            this.value = (Date) value;
    }
    public String getAsText() {
        Date value = this.value;
		if (value == null) {
			return "";
		}
		else {
			LocalDate localDate = value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
			return localDate.format(datetimeFormatter);
		}
    }
    public Object getValue() {
        return this.value;
    }
}

程序也比较简单,用DateTimeFormatter来转换字符串和日期就可以了。

接下来我们定义一个WebBindingInitializer,其中有一个initBinder实现方法,为自定义的CustomEditor注册做准备。

1
2
3
public interface WebBindingInitializer {
    void initBinder(WebDataBinder binder);
}

下面,我们再实现WebBindingInitializer接口,在实现方法initBinder里,注册自定义的CustomDateEditor,你可以看下相关代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.test;

import java.util.Date;
import com.minis.web.WebBindingInitializer;
import com.minis.web.WebDataBinder;

public class DateInitializer implements WebBindingInitializer{
	public void initBinder(WebDataBinder binder) {
		binder.registerCustomEditor(Date.class, new CustomDateEditor(Date.class,"yyyy-MM-dd", false));
	}
}

通过上述实现可以看到,我们自定义了“yyyy-MM-dd”这样一种日期格式,也可以根据具体业务需要,自定义其他日期格式。

然后,我们要使用它们,回到RequestMappingHandlerAdapter 这个类里,新增WebBindingInitializer 的属性定义,调整原有的RequestMappingHandlerAdapter(WebApplicationContext wac)这个构造方法的具体实现,你可以看下调整后的代码。

1
2
3
4
    public RequestMappingHandlerAdapter(WebApplicationContext wac) {         this.wac = wac;
       this.webBindingInitializer = (WebBindingInitializer)
this.wac.getBean("webBindingInitializer");
    }

其实也就是增加了webBindingInitializer属性的设置。

然后再利用IoC容器,让这个构造方法,支持用户通过applicationContext.xml 配置webBindingInitializer,我们可以在applicationContext.xml里新增下面这个配置。

1
<bean id="webBindingInitializer" class="com.test.DateInitializer">    </bean>

最后我们只需要在BeanWrapperImpl 实现类里,修改setPropertyValue(PropertyValue pv)这个方法的具体实现,把最初我们直接获取DefaultEditor的代码,改为先获取CustomEditor ,如果它不存在,再获取DefaultEditor,你可以看下相关实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    public void setPropertyValue(PropertyValue pv) {
        BeanPropertyHandler propertyHandler = new BeanPropertyHandler(pv.getName());
        PropertyEditor pe = this.getCustomEditor(propertyHandler.getPropertyClz());
        if (pe == null) {
            pe = this.getDefaultEditor(propertyHandler.getPropertyClz());
        }

        pe.setAsText((String) pv.getValue());
        propertyHandler.setValue(pe.getValue());
}

改造后,就能支持用户自定义的CustomEditor,增强了扩展性。同样的类型,如果既有用户自定义的实现,又有框架默认的实现,那用户自定义的优先。到这里,传入参数的处理问题我们就探讨完了。

小结

这节课,我们重点探讨了MVC里前后端参数的自动转换,把Request里的参数串自动转换成调用方法里的参数对象。为了完成传入参数的自动绑定,我们使用了WebDataBinder,它内部用BeanWrapperImpl对象,把属性值的map绑定到目标对象上。绑定的过程中,要对每一种数据类型分别进行格式转换,对基本的标准数据类型,由框架给定默认的转换器,但是对于别的数据类型或者是文化差异很大的数据类型,如日期型,我们可以通过CustomEditor机制让用户自定义。通过数据的自动绑定,我们不用再通过request.getParameter()方法手动获取参数值,再手动转成对象了,这些HTTP请求里的参数值就自动变成了后端方法里的参数对象值,非常便利。实际上后面我们会看到,这种两层之间的数据自动绑定和转换,在许多场景中都非常有用,比如Jdbc Template。所以这节课的内容需要你好好消化,灵活运用。 完整源代码参见 https://github.com/YaleGuo/minis

课后题

学完这节课的内容,我也给你留一道思考题。我们现在的实现是把Request里面的参数值,按照内部的次序隐含地自动转成后台调用方法参数对象中的某个属性值,那么可不可以使用一个手段,让程序员手动指定某个调用方法的参数跟哪个Request参数进行绑定呢?欢迎你在留言区和我交流讨论,也欢迎你把这节课分享给需要的朋友。我们下节课见!