【Spring MVC研究】聊聊web绑定器(WebDataBinder、@InitBinder)


本文主要介绍@InitBinder 的用法和原理,用法主要就是在 Controller 的基类上注册一些“属性编辑器”,跟 Spring 的 ConversionService 作用类似。

1. 绑定器的作用

  • WebDataBinder 的作用

在WebDataBinder类的注释上描述了,他的作用:“把 request 的请求参数绑定到 JavaBean 对象”。注释说的不是很好懂,翻译一下:
1、首先使用参数解析器从 request 中解析得到“解析器解析后的参数值”。

2、绑定器把“解析器解析后的参数值”转换为“Controller 方法需要的目标值”。

  • WebDataBinder 与 conversionService 异同

作者个人理解:WebDataBinder 的作用跟 conversionService� 的转换服务类似。

相同点:

在MVC的绑定参数中,WebDataBinder 调用了conversionService 来进行数据绑定。

不同点:

1、WebDataBinder 专职与“web 数据绑定”。@InitBinder更加适合做"跟web相关的定制化的转换",而ConversionService适合"通用的转换"

2、conversionService 作为 Spring 的转换服务,似乎用途更广。

3、一般使用,@InitBinder 要定义在 Controller 中。

2. 使用方式(测试代码)@InitBinder

如下的测试代码是 Controller 的基类,所有的自定义 Controller 都要求继承这个 Controller**。如果不继承则会失去 BaseController 的绑定器功能。**

public class BaseController
{
    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 将前台传递过来的日期格式的字符串,自动转化为Date类型
     */
    @InitBinder
    public void initBinder(WebDataBinder binder)
    {
        // Date 类型转换
        binder.registerCustomEditor(Date.class, new PropertyEditorSupport()
        {
            @Override
            public void setAsText(String text)
            {
                setValue(DateUtils.parseDate(text));
            }
        });
    }
}

以上代码完成的功能是:注册了一个原始字符串 text 到 Date 类型的转换服务。

注意:

1、绑定器是定义在基础 Controller 中,BaseController 中

2、如果我们的 Controller 不是继承 BaseController,是不是就失去了字符串 text 到 Date 类型的转换。。。还真有可能是的。。。读者自行尝试。。
。。

答案:不会生效。因为根据普通的Controller没有继承BaseController根据Controller的类类型是找不到@InitBinder方法,所以就不会生效。

3. 相关的几个核心类的真实类型

  • WebDataBinderFactory(真实类型是ServletRequestDataBinderFactory)
  • WebBindingInitializer(真实类型要看RequestMappingHandlerAdapter的创建过程)

纯 MVC 真实类型是:ConfigurableWebBindingInitializer。可能 Spring Boot 有拓展。

  • WebDataBinder(真实类型是ExtendedServletRequestDataBinder)
  • SimpleTypeConverter�(真实类型是SimpleTypeConverter��)
  • PropertyEditorRegistrySupport�(属性编辑器注册表支持)

其中存储了很多自定义属性编辑器

4. 原理

前提:对 DispatcherServlet 的 doDispatcher 方法必须有了解
参考:https://www.yuque.com/yuchangyuan/kkc8mp/hvq3485beg7e4eoz

分析原理的方法是:采用 正向推理反向推理,如果找到结合点那就推理完成啦!

正向推理:从DispatcherServlet 处理请求的 doDispatcher 方法开始

反向推理:看哪里用到了 InitBinder注解。

4.1. 正向推理

1、假设读者了解了doDispatcher 方法。既然是数据绑定,即把数据绑定到 Controller 的方法参数上。作用的位置一定是在doDispatcher 流程的处理方法参数中。我们不废话,直接定位到处理参数的代码。
org.springframework.web.method.support.InvocableHandlerMethod#getMethodArgumentValues方法。

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {

    .......
    for (int i = 0; i < parameters.length; i++) {
        ......
        // <1> 是否支持参数
        if (!this.resolvers.supportsParameter(parameter)) {
            throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
        }
        try {
            // <2> 解析器具体的解析参数
            args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
        }
        .......
    }
    return args;
}

看到在解析器解析参数中用到了 dataBinderFactory

2、再看看 dataBinderFactory的来源是RequestMappingHandlerAdapter类,如下:

protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
        HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    try {
        // getDataBinderFactory方法
        WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
        .....
    }
}

3、继续看getDataBinderFactory 方法

private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
    Class<?> handlerType = handlerMethod.getBeanType();
    // <1> 遍历handlerType子类父类接口,看看有没有标注@InitBinder注解
    Set<Method> methods = this.initBinderCache.get(handlerType);
    if (methods == null) {
        methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
        this.initBinderCache.put(handlerType, methods);
    }
    .......
    // <2> 根据methods创建initBinderMethods方法
    for (Method method : methods) {
        Object bean = handlerMethod.getBean();
        initBinderMethods.add(createInitBinderMethod(bean, method));
    }
    return createDataBinderFactory(initBinderMethods);
}

在<1>处,看到了遍历 Controller 所在类的父类父接口,看看有没有标注了@InitBinder注解的方法。

注意:遍历 Controller 及其父类父接口,是不是意味着如果我们的 Controller 不是继承 BaseController,是不是就失去了字符串 text 到 Date 类型的转换。。。还真有可能是的。。。读者自行尝试。。。

在<2>处,把标注了@InitBinder注解的方法都转换为InvocableHandlerMethod,最后存储在了ServletRequestDataBinderFactory类中。

4、正向推理临时先到这里,再看反向推理。

4.2. 反向推理

从@InitBinder的注释上看到了,注释跟WebDataBinder 有关、而WebDataBinder又跟WebDataBinderFactory�有关。

4.3. 正向反向推理结合分析

1、现在来看,正向推理和反向推理的连接点似乎就是WebDataBinderFactory。继续之前的正向过程,this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); 这个代码进入到具体的“参数解析器”环节了,不同的解析器情况不同

而“绑定器dataBinderFactory”作为一个参数,这不会意味着“解析器可能使用或不使用绑定器”把。

2、看一下参数解析器的体系吧
参数解析器的体系如下:非常庞大
image.png
通过观察发现有的解析器用到了dataBinderFactory,有的没有用到,但是大部分我们常用的解析器都用到了。

注意:有的用到了,有的没用到,是否意味着,绑定器只适用于部分场景。这值得思考。
实际情况:绑定器的涵盖范围广,不仅仅是 web,web 场景给我放心大胆的用

3、我们看常用情况,即AbstractNamedValueMethodArgumentResolver类:

	@Override
	@Nullable
	public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

		.......
    	// <1> 调用解析器的方法解析得到“参数值”
		Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
    	......

    	// <2> 应用绑定器
		if (binderFactory != null) {
			WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
			try {
				arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
			}
        	.......
		}

		handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);

		return arg;
	}

在注释的<1>处,先调用“参数解析器”解析得到了“参数值”。
在注释的<2>处,转换参数值的类型为目标类型,此处充分体现了 WebDataBinder 的作用是“转换参数为目标类型”

4.4. 重点来了(如果前后衔接是接4.3章节)

4.1、4.2、4.3 章节属于交代前因后果,属于补充上下文。
4.4 章节属于纯粹聊@InitBinder 注解。

4.4.1. @InitBinder注解的注册

继续看AbstractNamedValueMethodArgumentResolver 类的WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);代码

  • createBinder 方法(ServletRequestDataBinderFactory 类)

1、binderFactory 的实际类型是ServletRequestDataBinderFactory,这点是从 4.1 章节知道的。

public final WebDataBinder createBinder(
        NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception {

    // <1> 返回ExtendedServletRequestDataBinder
    WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);
    if (this.initializer != null) {
        // initializer类型是:ConfigurableWebBindingInitializer
        this.initializer.initBinder(dataBinder, webRequest);
    }
    // <3> 注册BaseController 中的注解
    initBinder(dataBinder, webRequest);
    return dataBinder;
}

在<1>处,返回的实际类型是ExtendedServletRequestDataBinder。
在<2>处,其实也没有做什么
在<3>处,注册 BaseController 中的注解。还记得在 4.1 章节解析到的@InitBinder注解的信息存储在ServletRequestDataBinderFactory 吗???

2、看initBinder 方法(ServletRequestDataBinderFactory 类)

public void initBinder(WebDataBinder dataBinder, NativeWebRequest request) throws Exception {
    for (InvocableHandlerMethod binderMethod : this.binderMethods) {
        if (isBinderMethodApplicable(binderMethod, dataBinder)) {
            // 执行binderMethod
            Object returnValue = binderMethod.invokeForRequest(request, null, dataBinder);
            if (returnValue != null) {
                throw new IllegalStateException(
                        "@InitBinder methods must not return a value (should be void): " + binderMethod);
            }
        }
    }
}

执行binderMethod 方法,此时的binderMethod 方法就是@InitBinder注解的方法。直接执行。

3、执行注解的方法(binder 的类型是ExtendedServletRequestDataBinder)

    @InitBinder
    public void initBinder(WebDataBinder binder)
    {
        // Date 类型转换
        binder.registerCustomEditor(Date.class, new PropertyEditorSupport()
        {
            @Override
            public void setAsText(String text)
            {
                setValue(DateUtils.parseDate(text));
            }
        });
    }
@Override
public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) {
    getPropertyEditorRegistry().registerCustomEditor(requiredType, propertyEditor);
}

target 是 null 呀。返回类型SimpleTypeConverter。

4、然后调用registerCustomEditor注册方法

@Override
public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) {
    registerCustomEditor(requiredType, null, propertyEditor);
}

@Override
public void registerCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath, PropertyEditor propertyEditor) {
    if (requiredType == null && propertyPath == null) {
        throw new IllegalArgumentException("Either requiredType or propertyPath is required");
    }
    if (propertyPath != null) {
        if (this.customEditorsForPath == null) {
            this.customEditorsForPath = new LinkedHashMap<>(16);
        }
        this.customEditorsForPath.put(propertyPath, new CustomEditorHolder(propertyEditor, requiredType));
    }
    else {
        if (this.customEditors == null) {
            this.customEditors = new LinkedHashMap<>(16);
        }
        // 注册
        this.customEditors.put(requiredType, propertyEditor);
        this.customEditorCache = null;
    }
}

@InitBinder注解被注册到了this.customEditors。

4.4.2. 执行参数绑定

接着AbstractNamedValueMethodArgumentResolver 的arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);�代码执行 Controller 方法参数的绑定。

public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
        @Nullable MethodParameter methodParam) throws TypeMismatchException {

    return getTypeConverter().convertIfNecessary(value, requiredType, methodParam);
}

1、getTypeConverter()方法返回 4.1 章节的SimpleTypeConverter。
SimpleTypeConverter —> TypeConverterDelegate

2、执行convertIfNecessary 方法(委派给TypeConverterDelegate 类了)

public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue,
        @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {

    // Custom editor for this type?
	// <1> 获取到 注册的自定义属性编辑器
    PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);

    ConversionFailedException conversionAttemptEx = null;

    // No custom editor but custom ConversionService specified?
	// <2> 应用spring的ConversionService服务
    ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
    if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
        TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
        if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
            try {
                return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
            }
            catch (ConversionFailedException ex) {
                // fallback to default conversion logic below
                conversionAttemptEx = ex;
            }
        }
    }
	.......
	.......
}

上面的代码职责是整个 Spring 的“类型转换”。
在<1>处,获取到 4.4.1 章节注册到的自定义类型转换。即从SimpleTypeConverter的 this.customEditors 获取。跟 4.4.1 章节对应起来了。
在<2>处,如果没有自定义的属性编辑器editor,就用 Spring 提供的ConversionService。spring 默认提供了很多种“类型转换器”。

备注:从这一点也可以知道。WebDataBinder 跟 ConversionService 是有相似点的。

3、找到转换器 editor 之后,就开始一步一步执行转换。直到应用到我们定义的@InitBinder定义的“类型转换器”。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fire Fish

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值