SpringMVC中Controller参数的解析和WebDataBinder的应用

Controller方法的参数类型可以是基本类型,也可以是封装后的普通Java类型。若这个普通Java类型没有声明任何注解,则意味着它的每一个属性都需要到Request中去查找对应的请求参数。众所周知,无论客户端传入的是什么类型的请求参数,最终都要以字符串的形式传给服务端。而服务端通过Request的getParameter方法取到的参数也都是字符串形式的结果,但是能用Request获取到的Parameter是tomcat进行的解析,tomcat默认支持对一种Content-Type进行解析,就是application/x-www-form-urlencoded,tomcat会将这些字符串解析好之后按照k-v键值对的形式放在request中,如果Conetent-Type是其他类型,tomcat就不能进行解析了,也就是说request.getParameter()方法中就获取不到任何的参数了。
不过好在springMVC非常的强大,其内部存在非常多的解析器,能帮助我们解析很多类型的Content-Type.举个例子,multipart/form-data这种类型的content-type,spring就能正确的帮助我们进行解析,且其内部存在这样一个类DefaultMultipartHttpServletRequest专门用来对应文件上传类型的Request,CommonsMultipartResolver这个类就是用来解析multipart/form-data的,将其进行解析然后放入DefaultMultipartHttpServletRequest的Parameter中,因为是multipart/form-data,所以Parameter的类型可能有文件的,可能有字符串的,都能帮助我们正确的解析出来

其解析时机就在DispatcherServlet中的doDispatch()方法中

// 如果是multipart/form-data的请求的话,返回的processedRequest就是DefaultMultipartHttpServletRequest这个类的对象
// 该对象中包含了所有我们的所有的参数和文件
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
mappedHandler = this.getHandler(processedRequest);

文件上传的body部分比较特殊所以其先要进行解析,解析成普通能识别的字符串的形式,但是我们的controller中的参数可能并不是String类型的,springMVC还要进行转换。

springMVC是如何将字符串参数转换成我们Controller中各种类型的参数的呢?

这就需要用到参数解析器了。我们在springMVC中使用@RequestMapping标识的每一个controller方法,在springMVC中都会被封装成InvocableHandlerMethod类型的对象,每一个InvocableHandlerMethod中都组合了很多种不同类型的参数解析器,所有的参数解析器都被组合到了HandlerMethodArgumentResolverComposite这个类中,这个类作为了InvocalbeMethodHandler类的一个属性。同时springMVC还将Controller的每一个参数封装成了MethodParameter这个类。在请求到一个controller方法之前,都会看Controller需要些什么参数,然后一个一个去解析出这个参数的值用来传递给Controller,解析参数就要用到参数解析器,不同的参数解析器支持不同的参数。

在获取了请求对应的handler和执行handler的Adapter之后,在调用controller方法之前,就要开始进行参数的解析。

HandlerMethodArgumentResolverComposite类中获取参数解析器的方法

@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
	// 先从缓存中判断当前类型的参数是否已经解析过参数解析器了
	// 如果已经解析过就直接返回
    HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
    if (result == null) {
        Iterator var3 = this.argumentResolvers.iterator();

        while(var3.hasNext()) {
            HandlerMethodArgumentResolver methodArgumentResolver = (HandlerMethodArgumentResolver)var3.next();
            if (methodArgumentResolver.supportsParameter(parameter)) {
                result = methodArgumentResolver;
                this.argumentResolverCache.put(parameter, methodArgumentResolver);
                break;
            }
        }
    }

    return result;
}

InvocableHandlerMethod类把解析参数的职责交给了HandlerMethodArgumentResolverComposite对象,然后该对象去查找对应的参数解析器,然后利用参数解析器解析参数然后返回给InvocableHandlerMethod类。获取参数解析器,就是看哪些参数解析器支持当前类型的参数。

就比如说RequestResponseBodyMethodProcessor这个参数解析器,下面就判断他是否支持当前参数的方法

 public boolean supportsParameter(MethodParameter parameter) {
     return parameter.hasParameterAnnotation(RequestBody.class);
 }

可以看出他就是支持被@RequestBody注解标识了的参数。
有兴趣的可以去看看该类的源码,不止能解析参数,还能解析返回值。
HandlerMethodArgumentResolver接口代表的是参数解析器接口,所有实现了该接口的类并且以Resolver为类名结尾的,都表示当前类只能解析参数,但是如果是以Processor结尾的说明该类即能解析参数,也能解析返回值,因为这些类即实现了HandlerMethodArgumentResolver接口也实现了HandlerMethodReturnValueHandler接口

然后是ModelAttributeMethodProcessor参数解析器

public boolean supportsParameter(MethodParameter parameter) {
    return parameter.hasParameterAnnotation(ModelAttribute.class) || this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType());
}

可以看出他支持的是被@ModelAttribute注解标识的,或者说不是一个简单属性的参数。

下面是HandlerMethodArgumentResolverComposite类的获取参数解析器并且解析的方法,先获取参数解析器(上面展示了获取参数解析器的代码),然后再进行解析。

@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
    if (resolver == null) {
        throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
    } else {
        return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    }
}

可以看出如果获取不到对应的参数解析器就会抛出异常。
需要注意的是,InvocableHandleMethod类会将每一个参数对应的参数解析器缓存起来,这样就不用每一次都去遍历所有的参数解析器,然后看是否支持了

上面说到了给参数获取对应的参数解析器,下面我们来看看参数解析器是如何解析参数的

以RequestResponseBodyMethodProcessor为例,这个类是从请求体中解析参数

public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    parameter = parameter.nestedIfOptional();
    // 利用消息HttpMessageConverter从请求中获取参数
    // 这又要涉及到不同的HttpMessageConverter了
    // 是根据Content-Type来判断使用哪个HttpMessageConverter
    // 比如如果我们的Content-Type是application/json,且配置了fastjson的消息转换器,那么fastjson这个消息转换器就能从请求体中解析出参数的值
    Object arg = this.readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
    String name = Conventions.getVariableNameForParameter(parameter);
    if (binderFactory != null) {
        WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
        if (arg != null) {
    		// 使用WebDataBinder来验证参数解析的结果,如果我们在参数上加了@Valid注解
            this.validateIfApplicable(binder, parameter);
            if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
                throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
            }
        }

        if (mavContainer != null) {
            mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
        }
    }

    return this.adaptArgumentIfNecessary(arg, parameter);
}

从上面这些我们可以看出参数解析器其实并不是真正进行参数解析的,进行参数解析的另有他类,比如上面的RequestResponseBodyMethodProcessor这个解析器,真正的解析就不是他来做的,而是由HttpMessageConverter来做的,

WebDataBinder其实也可以进行参数的解析,只不过他只在某些解析器中其解析作用。WebDataBinder不需要我们自己去创建,我可以向它注册参数类型对应的属性编辑器PropertyEditor。PropertyEditor可以将字符串转换成其真正的数据类型,它的void setAsText(String text)方法实现数据转换的过程。具体的做法是,在Controller中声明一个InitBinder方法,方法中利用WebDataBinder将自己实现的或者spring自带的PropertyEditor进行注册。像下面这样:

@InitBinder
public void initBinder(WebDataBinder binder) throws Exception {
    binder.registerCustomEditor(Long.class, new CustomNumberEditor(Long.class, true));
    binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), true));
}

从上面我们给出的ModelAttributeMethodProcessor类的代码,可以发现,该类可以解析参数上没有任何注解标识,且参数是一个javaBean类型的这种类型的参数。
下面是其解析参数的代码

public final Object resolveArgument(
		MethodParameter parameter, ModelAndViewContainer mavContainer,
		NativeWebRequest request, WebDataBinderFactory binderFactory)
		throws Exception {

	String name = ModelFactory.getNameForParameter(parameter);
	Object target = (mavContainer.containsAttribute(name)) ?
			mavContainer.getModel().get(name) : createAttribute(name, parameter, binderFactory, request);

	WebDataBinder binder = binderFactory.createBinder(request, target, name);
	if (binder.getTarget() != null) {
		bindRequestParameters(binder, request);
		validateIfApplicable(binder, parameter);
		if (binder.getBindingResult().hasErrors()) {
			if (isBindExceptionRequired(binder, parameter)) {
				throw new BindException(binder.getBindingResult());
			}
		}
	}

	mavContainer.addAllAttributes(binder.getBindingResult().getModel());
	return binder.getTarget();
}

每次请求到来后的参数解析都会利用WebDataBinderFactory创建一个binder对象,然后从这个binder中取得最终解析好的参数对象。WebDataBinderFactory是在InvocableHandlerMethod中定义的,即不同的Controller方法有着不同的WebDataBinderFactory。其实创建binder的同时还对binder进行了初始化,这个初始化过程就会执行Controller中的InitBinder方法。InitBinderDataBinderFactory实现了初始化binder的方法:

public void initBinder(WebDataBinder binder, NativeWebRequest request) throws Exception {
	for (InvocableHandlerMethod binderMebinderMethod thod : this.binderMethods) {
		if (isBinderMethodApplicable(binderMethod, binder)) {
			Object returnValue = binderMethod.invokeForRequest(request, null, binder);
			if (returnValue != null) {
				throw new IllegalStateException("@InitBinder methods should return void: " + binderMethod);
			}
		}
	}
}

上面方法中的binderMethods就是在Controller中定义的InitBinder方法,并且binderMethod 同Controller中的其他方法一样也是InvocableHandlerMethod。从上面的代码可以看出,InitBinder方法可以声明多个,WebDataBinderFactory初始化binder的时候会分别调用每个InitBinder方法。而我们在初始化的过程中使用了binder.registerCustomEditor,间接地向BeanWrapperImpl中注册了传入的PropertyEditor,以便在参数类型转换的时候使用。
还记得刚才的ModelAttributeMethodProcessor解析参数时,创建binder之后调用了bindRequestParameters实现了请求参数的绑定,它的子类ServletModelAttributeMethodProcessor重写了这个方法:

protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
	ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
	ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
	servletBinder.bind(servletRequest);
}

不论是父类还是子类,其实都是调用了binder的bind方法。下面是ServletRequestDataBinder的bind方法

public void bind(ServletRequest request) {
	MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
	MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
	if (multipartRequest != null) {
		bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
	}
	addBindValues(mpvs, request);
	doBind(mpvs);
}

这个方法跟依赖注入的过程非常相似,依赖注入是根据属性在容器中找到满足条件的对象,然后设置到当前的bean中。而上面的方法不是在容器中查找,而是从Request中获取,即把Request中的请求参数注入到binder的target中去,所以说WebDataBinder只能从request中获取参数对应的值。此时进行类型转换的就是刚刚注册的PropertyEditor,因为InitBinder方法每次都会执行,所以使用者可以在每个Controller中对相同类型的参数定义不同的参数转换方式。
经过了bindRequestParameters方法的处理,现在binder中target(即HandlerMethod的参数)已经包含了Request中的请求参数。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值