返回介绍

3.3 纯对象参数接收

发布于 2025-10-03 18:08:55 字数 9684 浏览 0 评论 0 收藏

假设有如下这样的 Controller:

@RequestMapping("/echoAgain")
public String echo(SimpleModel simpleModel, Model model) {
    model.addAttribute("echo", "hello " + simpleModel.getName() + ", your age is " + simpleModel.getAge() + ".");
    return "echo";
}

经过测试可以发现,SimpleModel 参数既可以接收 get 请求,也可以接收 post 请求。那么在这种情况下请求参数是被哪个参数解析器解析的呢,debug 发现: ServletModelAttributeMethodProcessor:

ServletModelAttributeMethodProcessor

核心的 supportsParameter 方法由父类 ModelAttributeMethodProcessor 实现:

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

可以看出,这里支持带有 ModelAttribute 注解或者是非基本类型的参数解析,同时 annotationNotRequired 必须设为 false,即 ModelAttribute 注解不必存在,这里是在 ServletModelAttributeMethodProcessor 的构造器中进行控制的,
RequestMappingHandlerAdapter.getDefaultArgumentResolvers 部分源码:

resolvers.add(new ServletModelAttributeMethodProcessor(false));

此类的作用是对 @ModelAttribute 注解标注的参数进行解析,假设我们将 Controller 方法改写成:

@RequestMapping("/echoAgain")
public String echo(@ModelAttribute SimpleModel simpleModel, Model model) {
    model.addAttribute("echo", "hello " + simpleModel.getName() + ", your age is " + simpleModel.getAge() + ".");
    System.out.println(model.asMap().get("simpleModel"));
    return "echo";
}

首先,Spring 会首先反射生成一个 SimpleModel 对象,之后将从 request 中获取的参数尝试设置到 SimpleModel 对象中去,最后将此对象放置到 Model 中(本质上就是一个 Map),key 就是 simpleModel.下面我们来看一下具体的解析过程,整个过程可以分为
以下三部分:

参数对象构造

因为 SimpleModel 是一个对象类型,所以要想将参数注入到其中,第一步必然是先创建一个对象,创建的入口位于 ModelAttributeMethodProcessor 的 resolveArgument 方法,相关源码:

//name 在这里便是 simpleModel
String name = ModelFactory.getNameForParameter(parameter);
Object attribute = (mavContainer.containsAttribute(name) ? mavContainer.getModel().get(name) :
                    createAttribute(name, parameter, binderFactory, webRequest));//反射实例化

ModelAndViewContainer 是个什么东西呢,从名字就可以看出就,它是 Spring MVC 里两个重要概念 Model 和 View 的组合体,用来记录在请求响应过程中 Model 和 View 的变化,在这里可以简单理解为去 Model 中检查有没有叫 simpleModel 的属性已经存在。

参数绑定

这里使用到了 DataBinder 接口,按照注释的说明,此接口用以 向执行的对象中设置属性值 ,就是这么简单,其继承体系如下图:

DataBinder

WebDataBinderFactory 接口用以创建 WebDataBinder 对象,其继承体系如下图:

WebDataBinderFactory

默认使用的是 ServletRequestDataBinderFactory,创建了一个 ExtendedServletRequestDataBinder 对象:

@Override
protected ServletRequestDataBinder createBinderInstance(Object target, String objectName, NativeWebRequest request) {
    return new ExtendedServletRequestDataBinder(target, objectName);
}

参数绑定的入口位于 ModelAttributeMethodProcessor.resolveArgument 方法,相关源码:

if (!mavContainer.isBindingDisabled(name)) {
    bindRequestParameters(binder, webRequest);
}

接下来由 ServletRequestDataBinder 的 bind 方法完成,核心源码:

public void bind(ServletRequest request) {
    MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
    doBind(mpvs);
}

在 ServletRequestParameterPropertyValues 构造器中获取了 Request 中所有的属性对。doBind 方法便是调用前面初始化的目标对象的 setter 方法进行参数设置的过程,不再展开。

参数校验

将我们的 Controller 方法改写为下面这种形式便可以启动 Spring MVC 的参数校验:

@RequestMapping("/echoAgain")
public String echo(@Validated SimpleModel simpleModel, Model model) {
    model.addAttribute("echo", "hello " + simpleModel.getName() + ", your age is " + simpleModel.getAge() + ".");
    System.out.println(model.asMap().get("simpleModel"));
    return "echo";
}

在这里 @Validated 注解可以用 @Valid(javax) 替换,前者是 Spring 对 java 校验标准的扩充,增加了校验组的支持。
为什么参数校验要放到参数绑定后面进行说明呢,因为**@Validated 和 @valid 注解不会影响 Spring MVC 参数解析的行为,被这两个注解标注的对象仍是由参数绑定一节提到的解析器进行解析。**

当参数校验绑定之后,Spring MVC 会尝试对参数进行校验,如果我们设置了校验注解。ModelAttributeMethodProcessor.resolveArgument 方法相关源码:

validateIfApplicable(binder, parameter);

protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) {
    Annotation[] annotations = methodParam.getParameterAnnotations();
    for (Annotation ann : annotations) {
        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
        if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
            Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
            Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
            binder.validate(validationHints);
            break;
        }
    }
}

DataBinder.validate:

public void validate(Object... validationHints) {
    for (Validator validator : getValidators()) {
        if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
            ((SmartValidator) validator).validate(getTarget(), getBindingResult(), validationHints);
        } else if (validator != null) {
            validator.validate(getTarget(), getBindingResult());
        }
    }
}

可见,具体的校验交给了 org.springframework.validation.Validator 实现,类图:

Validator

getValidators 方法获取的实际上是 DataBinder 内部的 validators 字段:

private final List<Validator> validators = new ArrayList<Validator>();

根据这里的校验器的来源可以分为以下两种情况。

JSR 校验

需要引入 hibernate-validator 到 classpath 中,回顾最前面配置解析部分,配置:

<mvc:annotation-driven/>

会利用 AnnotationDrivenBeanDefinitionParser 进行相关的解析、初始化工作,正是在其 parse 方法完成了对 JSR 校验的支持。相关源码:

@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
    RuntimeBeanReference validator = getValidator(element, source, parserContext);
}

private RuntimeBeanReference getValidator(Element element, Object source, ParserContext parserContext) {
    //mvc:annotation-driven 配置支持 validator 属性
    if (element.hasAttribute("validator")) {
        return new RuntimeBeanReference(element.getAttribute("validator"));
    } else if (javaxValidationPresent) {
        RootBeanDefinition validatorDef = new RootBeanDefinition(
                "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean");
        validatorDef.setSource(source);
        validatorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        String validatorName = parserContext.getReaderContext().registerWithGeneratedName(validatorDef);
        parserContext.registerComponent(new BeanComponentDefinition(validatorDef, validatorName));
        return new RuntimeBeanReference(validatorName);
    } else {
        return null;
    }
}

javaxValidationPresent 的定义:

private static final boolean javaxValidationPresent =
    ClassUtils.isPresent("javax.validation.Validator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader());

实现了 InitializingBean 接口,所以 afterPropertiesSet 方法是其初始化的入口,具体的校验过程不再展开。
除此之外还有一个有意思的问题,就是上面提到的校验器是如何进入到 DataBinder 中去的呢?答案是 WebDataBinderFactory 创建 DataBinder 对象时会利用 WebBindingInitializer 对 DataBinder 进行初始化,正是在这里
将容器中存在的校验器设置到 DataBinder 中,至于 WebBindingInitializer 又是从哪里来的,不再探究了,否则这细节实在是太麻烦了,意义不大。

自定义校验器

我们可以实现 Spring 提供的 Validator 接口,然后在 Controller 里边这样设置我们要是用的校验器:

@InitBinder
public void initBinder(DataBinder dataBinder) {
    dataBinder.setValidator(new SimpleModelValidator());
    //如果有多个可以使用 addValidators 方法
}

我们的 Controller 方法依然可以如此定义:

@RequestMapping("/echoAgain")
public String echo(@Validated SimpleModel simpleModel, Model model) {
    return "echo";
}

如果有错误,会直接返回 400.

一个有意思的问题

如果我们把 Controller 方法这样定义会怎样?

@RequestMapping(value = "/echoAgain", method = RequestMethod.POST)
public String echo(@Validated @RequestBody SimpleModel simpleModel, Model model) {}

答案是 @RequestBody 注解先于 @Validated 注解起作用,这样既可以利用 @RequestBody 注解向 Controller 传递 json 串,同时又能够达到校验的目的。从源码的角度来说,这在很大程度上是一个顺序的问题:
RequestMappingHandlerAdapter.getDefaultArgumentResolvers 相关源码:

resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));

虽然 ServletModelAttributeMethodProcessor 位于 RequestResponseBodyMethodProcessor 之前,但构造器参数为 false 说明了此解析器必须要求参数被 @ModelAttribute 注解标注,其实在最后还有一个不需要注解的解析器被添加:

// Catch-all
resolvers.add(new ServletModelAttributeMethodProcessor(true));

至此,真相大白。

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。