zl程序教程

您现在的位置是:首页 >  后端

当前栏目

SpringMVC源码总结(十)自定义HandlerMethodArgumentResolver

SpringMVC源码 总结 自定义
2023-09-14 08:59:44 时间

即使用@RequestBody来接受这样的参数。下面还要说说这样做的两个问题,你或许可以试猜一下: 
使用form表单来进行提交,运行: 
问题一: 
首先会遇到415 Unsupported Media Type,如下: 

07110926_jQ2O.png 
我们的form表单默认是以application/x-www-form-urlencoded方式提交的,而@RequestBody又采用的是RequestResponseBodyMethodProcessor这个HandlerMethodArgumentResolver,RequestResponseBodyMethodProcessor内部的处理原理就是用一系列的HttpMessageConverter来进行数据的转换的。这时候就需要找到支持MediaType类型为application/x-www-form-urlencoded和数据的类型为Father的HttpMessageConverter,当然就找不到了。我们本意是想让MappingJackson2HttpMessageConverter来处理的,但是它仅仅支持的MediaType类型为: 
?

即application/json或者application/*+json。所以此时就需要我们更改提交的content-type。然而form表单目前的仅仅支持三种content-type即application/x-www-form-urlencoded、multipart/form-data、text/plain。所以我们需要更换成ajax提交,如下: 
?

此时又有一个问题,teacher.name这样的形式并不能正确解析成Father。仍然需要变换格式: 
?

这样的json形式才能够被正确解析出来。 
所以说方案一有很多的地方要修改,并不是那么优雅。 

方案二: 
我们仍然使用form表单提交: 
?

大体上来说就是在解析每个参数时加上前缀限制。下面就要看看这个过程的源码分析: 
到底选择哪个HandlerMethodArgumentResolver来解析我们的参数呢?它最终会选择ServletModelAttributeMethodProcessor,看下它的判断条件: 
?

这里说明了它可以支持两种情况,一种情况为含有@ModelAttribute注解的参数,另一种情况就是虽然不含@ModelAttribute注解,但它并不是简单类型,如常用的String、Date等。你会发现spring会注册两个ServletModelAttributeMethodProcessor,一个annotationNotRequired为false,另一个为true。这主要是因为调用HandlerMethodArgumentResolver的解析顺序的原因,如果只有一个ServletModelAttributeMethodProcessor,当它判断参数不含@ModelAttribute注解,那它就把参数作为非简单类型来处理,这样的话,后面很多的HandlerMethodArgumentResolver将无法发挥作用。所以annotationNotRequired=true的ServletModelAttributeMethodProcessor是在最后才调用的。 

然后再具体看看ServletModelAttributeMethodProcessor的处理过程: 
?
                mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);         WebDataBinder binder = binderFactory.createBinder(request, attribute, name);

首先就是获取参数名的过程,String name = ModelFactory.getNameForParameter(parameter);具体内容如下: 
?
        return StringUtils.hasText(attrName) ? attrName :  Conventions.getVariableNameForParameter(parameter);
            valueClass = GenericCollectionTypeResolver.getCollectionParameterType(parameter);
                        "Cannot generate variable name for non-typed Collection parameter type");

有了类的简单名称,如果类的简单名称第一个和第二个字母都大写则不进行处理直接返回类的简单名称,否则仅仅将类的第一个大写变成小写。就此获取到了参数名为teacher。 

然后就是获取或者创建我们要绑定的Teacher对象。它首先尝试从要返回的model中能否找到属性名为teacher的model,如找不到,就需要去创建一个: 
?
                                           NativeWebRequest request) throws Exception {         String value = getRequestValueForAttribute(attributeName, request);
            Object attribute = createAttributeFromRequestValue(value, attributeName, parameter, binderFactory, request);
        return super.createAttribute(attributeName, parameter, binderFactory, request);

先尝试从request参数中能否找到teacher这一个参数,找到了就进行绑定和转换。未找到,就需要自己来实例化一个Teacher对象,此时并没有绑定相应的参数值。 

有个返回的目标,然后就是创建WebDataBinder实现绑定的过程: 
WebDataBinder binder = binderFactory.createBinder(request, attribute, name); 
?
public final WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName)
        WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);

这一个过程,我们之前已经分析过。就是调度执行一些@InitBinder方法注册一些PropertyEditor。我们继续要来看看initBinder(dataBinder, webRequest);执行了那些@InitBinder方法: 
?
public void initBinder(WebDataBinder binder, NativeWebRequest request) throws Exception {
                Object returnValue = binderMethod.invokeForRequest(request, null, binder);
                    throw new IllegalStateException("@InitBinder methods should return void: " + binderMethod);
protected boolean isBinderMethodApplicable(HandlerMethod initBinderMethod, WebDataBinder binder) {

当@InitBinder指定了value值的时候,只有那些value值含有binder.getObjectName()的才会执行,而此时的binder.getObjectName()就是我们辛辛苦苦找出来的参数名teacher。所以本例中@InitBinder("teacher")会执行,而@InitBinder("student")则不会执行。 

之后对四个参数 teacher.name=张三、teacher.age=88、student.name=李四、student.age=89 通过前缀进行过滤等其他操作实现了参数绑定。此过程不再分析,有兴趣的可以继续研究。 

方案三: 
使用自定义的HandlerMethodArgumentResolver: 
表单提交的内容为: 
?

经过测试,通过。 
自定义了两个东西,一个就是标签MyForm,另一个就是MyHandlerMethodArgumentResolver,并且我们从上一篇文章中知道如何将自定义HandlerMethodArgumentResolver加入HandlerMethodArgumentResolver大军中。如下: 
?

只有有一个value属性,用来指定from表单的中字段的前缀,若不指定,我将采取类名首字母小写的规则来默认前缀。如@MyForm Teacher a,默认前缀是teacher。 
然后就是MyHandlerMethodArgumentResolver,专门用来解析@MyForm注解的: 
?
            arg = binder.convertIfNecessary(webRequest.getParameter(prefix+"."+fieldName),fieldType, parameter);

其实也挺简单的。对于supportsParameter方法就是看看有没有MyForm注解,若有则处理。 
重点就在resolveArgument方法上:targetType就是MyForm所修饰的Teacher类或Student类,这里以Teacher为例。首先就是调用Teacher的无参的构造函数创建一个Teacher对象。然后由绑定工厂创建出绑定类,WebDataBinder binder = binderFactory.createBinder(webRequest, null,prefix);这一过程已在方案二中分析过了,就是执行那些符合的@InitBinder方法,这里我们传的值为prefix,即MyForm的value,若没指定就是类名的首字母小写,在这里就是teacher。也就是说那些@InitBinder的value值中含有teacher或者@InitBinder没有指定value值的方法才会被执行。因此我们这里注册的日期转换CustomDateEditor会被注册进去。然后就是执行绑定的过程。这个过程就是利用已注册的PropertyEditor和Converter来进行Field类型的转换。如下分析 
遍历它的Field,如String name,fieldType为String。binder.convertIfNecessary(webRequest.getParameter(prefix+"."+fieldName),fieldType, parameter);这里就是把teacher.name参数值转换成fieldType,都是String,所以就不需要转换器。对于Date date,就是把teacher.date参数的字符串值转换成Date类型,然后就用到了我们注册的CustomDateEditor,成功的进行了转换。对于 List String love,就是把teacher.love参数的字符串值转换成List集合,使用的是Spring已经注册的StringToArrayConverter,字符串默认是以,分割。 
该方案只能进行简单类型的转换(Teacher中field都是些简单类型),还不支持Teacher中包含复杂类型如包含其他属性类。其实也可以做成支持的,就是再稍加改造些,对于Field的处理先判断是否是简单类型,如Address类,若不是则递归调用上面的处理过程即对Address再次遍历Field来实现Address中简单类型的绑定。关键就是执行个递归调用,其他也没什么,有兴趣的可以自行研究。本例中的自定义文件可在后面下载。 

方案四: 
根据方案二我们其实就可以想到更改下方案二所用到的ServletModelAttributeMethodProcessor,就可以达到我们想要的结果。即如下: 
?
                mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request); //重点在这里在这里在这里在这里在这里在这里在这里         WebDataBinder binder = binderFactory.createBinder(request, attribute, name);

WebDataBinder binder = binderFactory.createBinder(request, attribute, name);在创建出WebDataBinder后,调用下binder.setFieldDefaultPrefix(prefix);就可以大功告成了。然而,我们会看到该方法是final,不可覆盖的,我就复制粘贴了一份,出来,新建了一个自定义的MyServletModelAttributeMethodProcessor以及它对应的注解标签MyServletModelForm,代码如下: 
MyServletModelForm内容为: 
?
                mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, webRequest);         WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
    public Map String,Object testrequestHeader(@MyServletModelForm Teacher a,@MyServletModelForm Student b){