跳到主要内容

ModelAttributeMethodProcessor参数解析过程

ModelAttributeMethodProcessor对参数的解析过程如下。ModelAttributeMethodProcessor主要针对加@ModelAttribute注解的参数,和非简单对象类型(哪些类型属于简单对象类型,在后文中会给出说明)的参数进行解析处理。

  • ModelAttributeMethodProcessor#resolveArgument:参数解析的入口方法
  • ModelAttributeMethodProcessor#constructAttribute:创建参数对象,通过获取参数类型的构造函数,然后通过反射机制,用构造函数实例化一个参数对象
  • ModelAttributeMethodProcessor#bindRequestParameters:绑定参数对象的值

入口方法resolveArgument的核心源码如下:

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

......

if (bindingResult == null) {
ResolvableType type = ResolvableType.forMethodParameter(parameter);
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name, type);
if (attribute == null) {
constructAttribute(binder, webRequest);
attribute = wrapAsOptionalIfNecessary(parameter, binder.getTarget());
}
if (!binder.getBindingResult().hasErrors()) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
}
......
}

......
}
java

ModelAttributeMethodProcessor会通过ModelAttributeMethodProcessor#constructAttribute方法来创建参数对象。从参数名称上也能看出来是通过构造函数来创建实例的。而这个方法最终会通过 DataBinder#createObject 方法来创建对象。部分代码如下

@Nullable
private Object createObject(ResolvableType objectType, String nestedPath, ValueResolver valueResolver) {
Class<?> clazz = objectType.resolve();
boolean isOptional = (clazz == Optional.class);
clazz = (isOptional ? objectType.resolveGeneric(0) : clazz);
if (clazz == null) {
throw new IllegalStateException(
"Insufficient type information to create instance of " + objectType);
}

Object result = null;
Constructor<?> ctor = BeanUtils.getResolvableConstructor(clazz);
......
}
java

报错的代码在Constructor<?> ctor = BeanUtils.getResolvableConstructor(clazz);,因为Set是一个接口,没有构造函数,因此无法获取到其构造函数,所以这里会报错。

那如果换成HashSet是不是就可以了呢?当然不行!!换成HashSet后创建对象的步骤虽然不会报错,但是集合内没有任何数据,也即只是创建了一个空对象。

因为实例创建成功后会调用实例创建后会调用org.springframework.web.method.annotation.ModelAttributeMethodProcessor#bindRequestParameters来绑定参数值,这样虽然HashSet对象能创建成功,但是绑定参数是通过读取对象中的字段,然后通过字段的Setter方法来给对象中的字段赋值的。而HashSet中的没有对应的字段。因此虽然不会报错,但是参数对象中是空的,没有任何元素。

ModelAttributeMethodProcessor#bindRequestParameters方法最终通过DataBinder#applyPropertyValues方法为对象的字段赋值。具体流程如下:

下面略了解下代码

protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = (ServletRequest)request.getNativeRequest(ServletRequest.class);
Assert.state(servletRequest != null, "No ServletRequest");
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder)binder;
servletBinder.bind(servletRequest);
}
java

org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor#bindRequestParameters方法中调用了servletBinder.bind方法。

public void bind(ServletRequest request) {
if (!this.shouldNotBindPropertyValues()) {
MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
MultipartRequest multipartRequest = (MultipartRequest)WebUtils.getNativeRequest(request, MultipartRequest.class);
if (multipartRequest != null) {
this.bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
} else if (isFormDataPost(request)) {
HttpServletRequest httpServletRequest = (HttpServletRequest)WebUtils.getNativeRequest(request, HttpServletRequest.class);
if (httpServletRequest != null && HttpMethod.POST.matches(httpServletRequest.getMethod())) {
StandardServletPartUtils.bindParts(httpServletRequest, mpvs, this.isBindEmptyMultipartFiles());
}
}

this.addBindValues(mpvs, request);
this.doBind(mpvs);
}
}
java

servletBinder.bind方法是一个核心方法,在这个方法中组装了一个MutablePropertyValues对象。也即将request中所有的参数取出,然后封装成一个MutablePropertyValues对象,用于在后面为对象赋值。

最终通过DataBinder#applyPropertyValues方法为对象的字段赋值。

protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
// Bind request parameters onto target object.
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
}
catch (PropertyBatchUpdateException ex) {
// Use bind error processor to create FieldErrors.
for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
}
}
}
java

getPropertyAccessor()方法返回的对象类型是org.springframework.beans.BeanWrapperImplBeanWrapperImpl又是AbstractPropertyAccessor的子类。setPropertyValues方法其实是AbstractPropertyAccessor的方法。

setPropertyValues方法中为字段赋值时,会对字段进行类型转换,而类型转换最终是通过ConversionService来做的。具体调用过程如下:

在Spring所有的Converter中,有一个StringToCollectionConverter是专门处理字符串到集合类型的转换的。

我们来一个简单的总结

  • ModelAttributeMethodProcessor来处理参数为非简单对象类型的参数。
  • 先通过参数类型实例化一个参数对象。
  • 通过读取request中的参数,通过反射机制将字段的值set到对象内。
  • 如果字段类型不是字符串,会通过ConversionService对类型进行转换。

以上便是ModelAttributeMethodProcessor的核心功能。

ConversionService 在SpringMVC中是最重要的类之一。在Spring中大多情况下,请求和应答的类型转换都通过它进行。而另一个涉及类型转换的很重要的类是MessageConverter

那你是否想到了怎么在不加@RequestParam注解的前提下,接收Set类型的参数了吗?

那就是定义一个对象,对象中有一个Set类型字段,而字段的名称为请求参数的名称即可。如下:

@Data
public class SetParam {

private Set<String> paramSet;

}

@RestController
@RequestMapping("/test")
public class TestController {

@GetMapping("/setParam")
public ResponseBean setTest(SetParam params) {
System.out.println(params);
return new ResponseBean(true, "OK");
}
}
java