mini-spring-course

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

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

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

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

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

传入参数的绑定

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

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

    public void bind(HttpServletRequest request) {
        PropertyValues mpvs = assignParameters(request);
        addBindValues(mpvs, request);
        doBind(mpvs);
    }

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

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

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

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(),把Request里的参数换成内存里的一个map对象,这一步用到了底层的WebUtils工具类,这个转换对我们来说比较简单。而最核心的方法是getPropertyAccessor().setPropertyValues(mpvs);,这个getPropertyAccessor则是内置了一个BeanWrapperImpl对象,内部包含了target。由名字可以看出它是Bean的包装实现类,把属性map绑定到目标对象上去。

有了这个大流程,我们再来探究一下一个具体的参数是如何转换的,我们知道Request的转换都是从字符串转为其他类型,所以我们可以定义一个通用接口,名叫PropertyEditor,内部提供一些方法可以让字符串和Obejct之间进行双向灵活转换。

package com.minis.beans;

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

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

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实现的相关源代码。

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的实现类就更加简单了,因为它是字符串本身的处理,但它的构造函数有些不一样,支持传入字符串格式strFormat,这也是为后续类型转换格式留了一个“口子”。

有了两个基本类型的Editor作为工具,现在我们再来看关键的类BeanWapperImpl的实现。

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是如何实现的。

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,还定义了可以定制化的转换器Editor,这也是WebDataBinder能做不同类型转换的原因。不过我们目前的实现,只支持数字和字符串几个基本类型的转换,暂时不支持数组、列表、map等格式。

现在,我们已经实现了一个完整的WebDataBinder,用来绑定数据。我们接下来将提供一个WebDataBinderFactory,能够更方便、灵活地操作WebDataBinder。

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) {
    }
}

有了上面一系列工具之后,我们看怎么使用它们进行数据绑定。从前面的讲解中我们已经知道,这个 HTTP Request请求最后会找到映射的方法上,也就是通过RequestMappingHandlerAdapter里提供的handleInternal 方法,来调用invokeHandlerMethod 方法,所以我们从这个地方切入,改造 invokeHandlerMethod 方法,实现参数绑定。

    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则是创建了WebDtaBinder,对目标对象进行绑定。整个循环结束之后,Request里面的参数就绑定了调用方法里的参数,之后就可以被调用。

我们从这个绑定过程中可以看到,循环过程就是按照参数在方法中出现的次序逐个绑定的,所以这个次序是很重要的。

客户化转换器

现在我们已经实现了Request数据绑定过程,也提供了默认的CustomNumberEditor和StringEditor,来进行数字和字符串两种类型的转换,从而把ServletRequest里的请求参数转换成Java对象里的数据类型。但这种默认的方式比较固定,如果你希望转换成自定义的类型,那么原有的两个Editor就没办法很好地满足需求了。

因此我们要继续探讨,如何支持自定义的Editor,让我们的框架具有良好的扩展性。其实上面我们看到PropertyEditorRegistrySupport里,已经提前准备好了客户化转换器的地方,你可以看下代码。

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

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

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

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

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

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注册做准备。

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

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

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)这个构造方法的具体实现,你可以看下调整后的代码。

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

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

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

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

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

    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参数进行绑定呢?欢迎你在留言区和我交流讨论,也欢迎你把这节课分享给需要的朋友。我们下节课见!