• 【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));
                }
            });
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    以上代码完成的功能是:注册了一个原始字符串 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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    看到在解析器解析参数中用到了 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);
            .....
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在<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
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在注释的<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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在<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);
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    执行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));
                }
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    @Override
    public void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor) {
        getPropertyEditorRegistry().registerCustomEditor(requiredType, propertyEditor);
    }
    
    • 1
    • 2
    • 3
    • 4

    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;
        }
    }
    
    • 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

    @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
    • 2
    • 3
    • 4
    • 5

    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;
                }
            }
        }
    	.......
    	.......
    }
    
    • 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

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

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

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

  • 相关阅读:
    虹科分析 | 终端安全 | 移动目标防御是“变革性”技术——GARTNER
    新版jadx-gui导入dex会提示Bad checksum
    剑指offer 45. 数字序列中某一位的数字
    leetcode 26.删除有序数组中的重复项
    卡尔曼滤波(Kalman Filter)原理浅析-数学理论推导-2
    zabbix自定义监控、钉钉、邮箱报警
    MASM 64汇编
    大模型应用疯狂加速,洗牌却在静悄悄进行了
    Flutter中如何让Android的手势导航栏完全透明?
    Maven
  • 原文地址:https://blog.csdn.net/yuchangyuan5237/article/details/133692980