Spring mvc WebDataBinder数据绑定器

前言

DataBinder 是数据绑定器,它的作用是把一组属性值绑定到目标对象上,参数绑定的方式一般是通过 Java 反射。WebDataBinder 顾名思义,它是专门在 Web 环境使用的数据绑定器。

"数据绑定"对于框架而言是一项基础能力,很多地方都有数据绑定的需求。以 Spring MVC 为例,框架需要把 HttpServletRequest 参数绑定到 Java 对象上。

DataBinder

先来看一下最基础的数据绑定器如何使用,再来分析会比较有思路。

DataBinder 实现了 PropertyEditorRegistry 和 TypeConverter 接口。前者是属性编辑器的注册表,用于注册和查找属性编辑器 PropertyEditor,PropertyEditor 允许开发者自由设置属性值,例如把一个字符串的值设置为 LocalDate;后者是一个类型转换接口,它可以实现不同类型对象间的转换。在底层实现上,TypeConverter 其实是依赖 PropertyEditorRegistry 查找对应的 PropertyEditor 来实现类型转换的。

DataBinder 有两大职责:数据绑定、数据校验,对应的方法是bind()validate(),数据校验先跳过,本文重点分析数据绑定。

下面是一个简单示例,利用 DataBinder 把三个属性绑定到了目标对象 user。DataBinder 还自动把"2005-01-01"字符串转换成了 LocalDate 类型,默认情况下这个转换不会发生,因为 DataBinder 并不知道如何把一段文本转换成日期,所以我们手动注册了一个 PropertyEditor。

java 复制代码
public static void main(String[] args) throws Exception {
    User user = new User();
    DataBinder dataBinder = new DataBinder(user);
    dataBinder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {
        @Override
        public void setAsText(String text) throws IllegalArgumentException {
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
            setValue(LocalDate.parse(text, formatter));
        }
    });

    MutablePropertyValues pvs = new MutablePropertyValues();
    pvs.add("name", "Lisa");
    pvs.add("age", "18");
    pvs.add("birthday", "2005-01-01");
    dataBinder.bind(pvs);
}

@Data
public static class User {
    String name;
    Integer age;
    LocalDate birthday;
}

我们已经知道,它实现了 TypeConverter 接口,所以自带数据类型转换的能力,下面是把字符串转换成日期对象:

java 复制代码
LocalDate localDate = dataBinder.convertIfNecessary("2005-01-01", LocalDate.class);

这些就是 DataBinder 的基本能力。

ExtendedServletRequestDataBinder

如下示例,是一个最简单的接口方法。参数 user 是我们自定义的 POJO 类,且没有加任何注解。

java 复制代码
@GetMapping("get/{id}")
public Object get(User user) {
    return "success";
}

@Data
public class User {
    Long id;
    String name;
}

启动服务,访问以下链接:

basic 复制代码
http://127.0.0.1:8080/get/10?name=lisa

你会发现 user 对象的 id 和 name 都有对应的值,也就是说 Spring MVC 帮我们完成了 HttpServletRequest 参数到 Java 对象的属性绑定,而且数据源不止是 Request 参数,还包含 URL 的路径变量,这一切是如何做到的呢?

我们已经知道,DispatcherServlet 调用目标方法前,会通过 HandlerMethodArgumentResolver 组件来解析方法参数,对于上面的例子,解析器实现是 ServletModelAttributeMethodProcessor。它在解析参数时,首先会查找到参数类的构造器对象,再利用反射实例化参数对象,此时对象还是空的,紧接着就会调用ServletRequestDataBinder#bind把 HttpServletRequest 参数绑定到目标方法的参数对象上。

Spring MVC 使用被增强的子类 ExtendedServletRequestDataBinder 来把 HttpServletRequest 参数绑定到 Controller 方法参数上。类图如下:

  • DataBinder:是个基础的通用的数据绑定器
  • WebDataBinder:适用于 Web 环境,但是没有直接依赖 Servlet API
  • ServletRequestDataBinder:用于把 ServletRequest 属性绑定到目标对象
  • ExtendedServletRequestDataBinder:扩展了属性源,把 Path 变量也添加到属性源了

再来看bind()流程:

  • 首先提取 HttpServletRequest Parameter 参数构建 MutablePropertyValues,用作属性源
  • 紧接着把上传的文件也添加到属性源,用于 MultipartFile 对象的绑定
  • 调用子类扩展方法 addBindValues,这里是把 Path 变量添加到属性源
  • 最后调用doBind()开始绑定
java 复制代码
public void bind(ServletRequest request) {
    // 提取Request Parameter参数构建MutablePropertyValues
    MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
    // 上传的文件也添加到mpvs
    MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
    if (multipartRequest != null) {
        bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
    }
    else if (StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.MULTIPART_FORM_DATA_VALUE)) {
        HttpServletRequest httpServletRequest = WebUtils.getNativeRequest(request, HttpServletRequest.class);
        if (httpServletRequest != null && HttpMethod.POST.matches(httpServletRequest.getMethod())) {
            StandardServletPartUtils.bindParts(httpServletRequest, mpvs, isBindEmptyMultipartFiles());
        }
    }
    // 子类扩展 往mpvs添加自定义属性
    // ExtendedServletRequestDataBinder会把Path变量填充进去
    addBindValues(mpvs, request);
    // 开始绑定
    doBind(mpvs);
}

WebDataBinder#doBind主要是对属性源的默认值和空值做了一些处理,最终把属性源 mpvs 交给父类,由父类统一处理数据绑定:

java 复制代码
protected void doBind(MutablePropertyValues mpvs) {
    /**
     * 处理字段的默认值,字段名以"!"开头
     * 例如:有!name=lisa,如果Request参数不含name,则自动填入name=lisa,有则忽略
     */
    checkFieldDefaults(mpvs);
    /**
     * 处理字段的控制,字段名以"_"开头
     * 例如:有_old,如果Request参数不含old,则自动填入old=false,有则忽略
     * 处理控制的类包括:Boolean/Array/Collection/Map
     */
    checkFieldMarkers(mpvs);
    // 处理以"[]"结尾的属性名
    adaptEmptyArrayIndices(mpvs);
    // 调用父类方法绑定数据
    super.doBind(mpvs);
}

DataBinder#doBind移除了不被允许绑定的属性和必要字段的校验处理,最终调用applyPropertyValues()实现目标对象属性的绑定:

java 复制代码
protected void doBind(MutablePropertyValues mpvs) {
    // 移除不被允许绑定的属性
    checkAllowedFields(mpvs);
    // 检查必须要的属性值
    checkRequiredFields(mpvs);
    // 属性值绑定到目标对象
    applyPropertyValues(mpvs);
}

属性绑定的流程是:遍历属性源 mpvs,根据要绑定的目标属性类型通过PropertyEditorRegistrySupport#findCustomEditor找到对应类型的属性编辑器,实现属性类型的转换,再通过反射调用目标 setter 方法给属性赋值,以完成数据绑定。

尾巴

WebDataBinder 是用于 Web 环境的数据绑定器,它的职责是把 HttpServletRequest 参数绑定到 Controller 方法里没有被注解的自定义 POJO 类型的参数对象上。绑定的数据源有两个,一个是 HttpServletRequest 参数、一个是请求 Uri 的路径变量。除了绑定参数,它还会自动进行数据类型的转换,例如把字符串转换成日期类型,内置了一些基础的转换器,对于复杂类型的转换,你需要手动注册自定义的 PropertyEditor。

相关推荐
魔道不误砍柴功1 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2341 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨1 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
测开小菜鸟3 小时前
使用python向钉钉群聊发送消息
java·python·钉钉
P.H. Infinity4 小时前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天4 小时前
java的threadlocal为何内存泄漏
java
caridle4 小时前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^4 小时前
数据库连接池的创建
java·开发语言·数据库
苹果醋34 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花4 小时前
【JAVA基础】Java集合基础
java·开发语言·windows