乐趣区

从原理层面掌握ModelAttribute的使用核心原理篇一起学Spring-MVC

每篇一句

我们应该做一个:胸中有蓝图,脚底有计划的人

前言

Spring MVC提供的基于注释的编程模型,极大的简化了 web 应用的开发,我们都是受益者。比如我们在 @RestController 标注的 Controller 控制器组件上用 @RequestMapping@ExceptionHandler 等注解来表示请求映射、异常处理等等。
使用这种注解的方式来开发控制器我认为最重要的优势是:

  1. 灵活的方法签名(入参随意写)
  2. 不必继承基类
  3. 不必实现接口

== 总之一句话:灵活性非常强,耦合度非常低。==

在众多的注解使用中,Spring MVC中有一个非常强大但几乎被忽视的一员:@ModelAttribute。关于这个注解的使用情况,我在群里 / 线下问了一些人,感觉很少人会使用这个注解(甚至有的不知道有这个注解),这着实让我非常的意外。我认为至少这对于 ” 久经战场 ” 的一个老程序员来说这是不应该的吧。

不过没关系,有幸看到此文,能够帮你弥补弥补这块的盲区。
@ModelAttribute它不是开发必须的注解(不像 @RequestMapping 那么重要),so 即使你不知道它依旧能正常书写控制器。当然,正所谓没有最好只有更好,倘若你掌握了它,便能够帮助你 更加高效 的写代码,让你的代码复用性更强、代码更加简洁、可维护性更高。

这种知识点就像反射、就像内省,即使你不知道它你完全也可以工作、写业务需求。但是若你能够熟练使用,那你的可想象空间就会更大了,未来可期。虽然它不是必须,但是它是个很好的辅助~

@ModelAttribute 官方解释

首先看看 Spring 官方的 JavaDoc 对它怎么说:它将方法参数 / 方法返回值绑定到 web viewModel里面。只支持 @RequestMapping 这种类型的控制器哦。它既可以标注在方法入参上,也可以标注在方法(返回值)上。

但是请注意,当请求处理导致异常时 ,引用数据和所有其他模型内容对 Web 视图 不可用 ,因为该异常随时可能引发,使Model 内容不可靠。因此,标注有 @Exceptionhandler 的方法不提供对 Model 参数的访问~

// @since 2.5  只能用在入参、方法上
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelAttribute {@AliasFor("name")
    String value() default "";
    // The name of the model attribute to bind to. 注入如下默认规则
    // 比如 person 对应的类是:mypackage.Person(类名首字母小写)// personList 对应的是:List<Person>  这些都是默认规则咯~~~ 数组、Map 的省略
    // 具体可以参考方法:Conventions.getVariableNameForParameter(parameter)的处理规则
    @AliasFor("value")
    String name() default "";

    // 若是 false 表示禁用数据绑定。// @since 4.3
    boolean binding() default true;}

基本原理

我们知道 @ModelAttribute 能标注在入参上,也可以标注在方法上。下面就从原理处深入理解,从而掌握它的使用,后面再给出多种使用场景的使用 Demo
和它相关的两个类是 ModelFactoryModelAttributeMethodProcessor

@ModelAttribute缺省处理的是 Request 请求域,Spring MVC还提供了 @SessionAttributes 来处理和 Session 域相关的模型数据,详见:从原理层面掌握 @SessionAttributes 的使用【一起学 Spring MVC】

关于 ModelFactory 的介绍,在这里讲解 @SessionAttributes 的时候已经介绍一大部分了,但特意留了一部分关于 @ModelAttribute 的内容,在本文继续讲解

ModelFactory

ModelFactory所在包 org.springframework.web.method.annotation,可见它和 web 是强关联的在一起的。作为上篇文章的补充说明,接下里只关心它对@ModelAttribute 的解析部分:

// @since 3.1
public final class ModelFactory {

    // 初始化 Model 这个时候 `@ModelAttribute` 有很大作用
    public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception {
        // 拿到 sessionAttr 的属性
        Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
        // 合并进容器内
        container.mergeAttributes(sessionAttributes);
        // 这个方法就是调用执行标注有 @ModelAttribute 的方法们~~~~
        invokeModelAttributeMethods(request, container);
        ... 
    }

    // 调用标注有注解的方法来填充 Model
    private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer container) throws Exception {
        // modelMethods 是构造函数进来的  一个个的处理吧
        while (!this.modelMethods.isEmpty()) {
            // getNextModelMethod:通过 next 其实能看出 执行是有顺序的  拿到一个可执行的 InvocableHandlerMethod
            InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod();

            // 拿到方法级别的标注的 @ModelAttribute~~
            ModelAttribute ann = modelMethod.getMethodAnnotation(ModelAttribute.class);
            Assert.state(ann != null, "No ModelAttribute annotation");
            if (container.containsAttribute(ann.name())) {if (!ann.binding()) { // 若 binding 是 false  就禁用掉此 name 的属性  让不支持绑定了  此方法也处理完成
                    container.setBindingDisabled(ann.name());
                }
                continue;
            }

            // 调用目标的 handler 方法,拿到返回值 returnValue 
            Object returnValue = modelMethod.invokeForRequest(request, container);
            // 方法返回值不是 void 才需要继续处理
            if (!modelMethod.isVoid()){

                // returnValueName 的生成规则 上文有解释过  本处略
                String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType());
                if (!ann.binding()) { // 同样的 若禁用了绑定,此处也不会放进容器里
                    container.setBindingDisabled(returnValueName);
                }
        
                // 在个判断是个小细节:只有容器内不存在此属性,才会放进去   因此并不会有覆盖的效果哦~~~
                // 所以若出现同名的  请自己控制好顺序吧
                if (!container.containsAttribute(returnValueName)) {container.addAttribute(returnValueName, returnValue);
                }
            }
        }
    }

    // 拿到下一个标注有此注解方法~~~
    private ModelMethod getNextModelMethod(ModelAndViewContainer container) {
        
        // 每次都会遍历所有的构造进来的 modelMethods
        for (ModelMethod modelMethod : this.modelMethods) {
            // dependencies:表示该方法的所有入参中 标注有 @ModelAttribute 的入参们
            // checkDependencies 的作用是:所有的 dependencies 依赖们必须都是 container 已经存在的属性,才会进到这里来
            if (modelMethod.checkDependencies(container)) {
                // 找到一个 就移除一个
                // 这里使用的是 List 的 remove 方法,不用担心并发修改异常???哈哈其实不用担心的  小伙伴能知道为什么吗??this.modelMethods.remove(modelMethod);
                return modelMethod;
            }
        }

        // 若并不是所有的依赖属性 Model 里都有,那就拿第一个吧~~~~
        ModelMethod modelMethod = this.modelMethods.get(0);
        this.modelMethods.remove(modelMethod);
        return modelMethod;
    }
    ...
}

ModelFactory这部分做的事:执行所有的标注有 @ModelAttribute 注解的方法,并且是顺序执行哦。那么问题就来了,这些 handlerMethods 是什么时候被“找到”的呢???这个时候就来到了 RequestMappingHandlerAdapter,来看看它是如何找到这些标注有此注解@ModelAttribute 的处理器的~~~

RequestMappingHandlerAdapter

RequestMappingHandlerAdapter是个非常庞大的体系,本处我们只关心它对 @ModelAttribute 也就是对 ModelFactory 的创建,列出相关源码如下:

//  @since 3.1
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {

    // 该方法不能标注有 @RequestMapping 注解,只标注了 @ModelAttribute 才算哦~
    public static final MethodFilter MODEL_ATTRIBUTE_METHODS = method ->
            (!AnnotatedElementUtils.hasAnnotation(method, RequestMapping.class) && AnnotatedElementUtils.hasAnnotation(method, ModelAttribute.class));
    ...
    // 从 Advice 里面分析出来的标注有 @ModelAttribute 的方法(它是全局的)private final Map<ControllerAdviceBean, Set<Method>> modelAttributeAdviceCache = new LinkedHashMap<>();

    @Nullable
    protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        // 每调用一次都会生成一个 ModelFactory ~~~
        ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
        ...
        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
        mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
        // 初始化 Model
        modelFactory.initModel(webRequest, mavContainer, invocableMethod);
        mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
        ...
        return getModelAndView(mavContainer, modelFactory, webRequest);
    }

    // 创建出一个 ModelFactory,来管理 Model
    // 显然和 Model 相关的就会有 @ModelAttribute @SessionAttributes 等注解啦~
    private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
        // 从缓存中拿到和此 Handler 相关的 SessionAttributesHandler 处理器~~ 处理 SessionAttr
        SessionAttributesHandler sessionAttrHandler = getSessionAttributesHandler(handlerMethod);
        Class<?> handlerType = handlerMethod.getBeanType();

        // 找到当前类(Controller)所有的标注的 @ModelAttribute 注解的方法
        Set<Method> methods = this.modelAttributeCache.get(handlerType);
        if (methods == null) {methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
            this.modelAttributeCache.put(handlerType, methods);
        }
        
        List<InvocableHandlerMethod> attrMethods = new ArrayList<>();
        // Global methods first
        // 全局的有限,最先放进 List 最先执行~~~~
        this.modelAttributeAdviceCache.forEach((clazz, methodSet) -> {if (clazz.isApplicableToBeanType(handlerType)) {Object bean = clazz.resolveBean();
                for (Method method : methodSet) {attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
                }
            }
        });
        for (Method method : methods) {Object bean = handlerMethod.getBean();
            attrMethods.add(createModelAttributeMethod(binderFactory, bean, method));
        }
        return new ModelFactory(attrMethods, binderFactory, sessionAttrHandler);
    }

    // 构造 InvocableHandlerMethod 
    private InvocableHandlerMethod createModelAttributeMethod(WebDataBinderFactory factory, Object bean, Method method) {InvocableHandlerMethod attrMethod = new InvocableHandlerMethod(bean, method);
        if (this.argumentResolvers != null) {attrMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
        }
        attrMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
        attrMethod.setDataBinderFactory(factory);
        return attrMethod;
    }
}

RequestMappingHandlerAdapter这部分处理逻辑:每次请求过来它都会创建一个 ModelFactory,从而收集到全局的(来自@ControllerAdvice)+ 本Controller 控制器上的所有的标注有 @ModelAttribute 注解的方法们。
@ModelAttribute标注在单独的方法上(木有 @RequestMapping 注解),它可以在每个控制器方法 调用之前 ,创建出一个ModelFactory 从而管理 Model 数据~

ModelFactory管理着 Model,提供了@ModelAttribute 以及 @SessionAttributes 等对它的影响

同时 @ModelAttribute 可以标注在入参、方法(返回值)上的,标注在不同地方处理的方式是不一样的,那么接下来又一主菜 ModelAttributeMethodProcessor 就得登场了。

ModelAttributeMethodProcessor

从命名上看它是个 Processor,所以根据经验它既能处理入参,也能处理方法的返回值:HandlerMethodArgumentResolver + HandlerMethodReturnValueHandler。解析@ModelAttribute 注解标注的方法参数,并处理 @ModelAttribute 标注的方法返回值。

== 先看它对方法入参的处理(稍显复杂):==

// 这个处理器用于处理入参、方法返回值~~~~
// @since 3.1
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
    private final boolean annotationNotRequired;

    public ModelAttributeMethodProcessor(boolean annotationNotRequired) {this.annotationNotRequired = annotationNotRequired;}


    // 入参里标注了 @ModelAttribute 或者(注意这个或者)annotationNotRequired = true 并且不是 isSimpleProperty()
    // isSimpleProperty():八大基本类型 / 包装类型、Enum、Number 等等 Date Class 等等等等
    // 所以划重点:即使你没标注 @ModelAttribute  单子还要不是基本类型等类型,都会进入到这里来处理
    // 当然这个行为是是收到 annotationNotRequired 属性影响的,具体的具体而论  它既有 false 的时候  也有 true 的时候
    @Override
    public boolean supportsParameter(MethodParameter parameter) {return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
                (this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
    }

    // 说明:能进入到这里来的  证明入参里肯定是有对应注解的???// 显然不是,上面有说  这事和属性值 annotationNotRequired 有关的~~~
    @Override
    @Nullable
    public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    
        // 拿到 ModelKey 名称~~~(注解里有写就以注解的为准)String name = ModelFactory.getNameForParameter(parameter);
        // 拿到参数的注解本身
        ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
        if (ann != null) {mavContainer.setBinding(name, ann.binding());
        }

        Object attribute = null;
        BindingResult bindingResult = null;

        // 如果 model 里有这个属性,那就好说,直接拿出来完事~
        if (mavContainer.containsAttribute(name)) {attribute = mavContainer.getModel().get(name);
        } else { // 若不存在,也不能让是 null 呀
            // Create attribute instance
            // 这是一个复杂的创建逻辑:// 1、如果是空构造,直接 new 一个实例出来
            // 2、若不是空构造,支持 @ConstructorProperties 解析给构造赋值
            //   注意: 这里就支持 fieldDefaultPrefix 前缀、fieldMarkerPrefix 分隔符等能力了 最终完成获取一个属性
            // 调用 BeanUtils.instantiateClass(ctor, args)来创建实例
            // 注意:但若是非空构造出来,是立马会执行 valid 校验的,此步骤若是空构造生成的实例,此步不会进行 valid 的,但是下一步会哦~
            try {attribute = createAttribute(name, parameter, binderFactory, webRequest);
            } catch (BindException ex) {if (isBindExceptionRequired(parameter)) {
                    // No BindingResult parameter -> fail with BindException
                    throw ex;
                }
                // Otherwise, expose null/empty value and associated BindingResult
                if (parameter.getParameterType() == Optional.class) {attribute = Optional.empty();
                }
                bindingResult = ex.getBindingResult();}
        }

        // 若是空构造创建出来的实例,这里会进行数据校验  此处使用到了 ((WebRequestDataBinder) binder).bind(request);  bind() 方法  唯一一处
        if (bindingResult == null) {
            // Bean property binding and validation;
            // skipped in case of binding failure on construction.
            WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
            if (binder.getTarget() != null) {
                // 绑定 request 请求数据
                if (!mavContainer.isBindingDisabled(name)) {bindRequestParameters(binder, webRequest);
                }
                // 执行 valid 校验~~~~
                validateIfApplicable(binder, parameter);
                // 注意:此处抛出的异常是 BindException
                //RequestResponseBodyMethodProcessor 抛出的异常是:MethodArgumentNotValidException
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {throw new BindException(binder.getBindingResult());
                }
            }
            // Value type adaptation, also covering java.util.Optional
            if (!parameter.getParameterType().isInstance(attribute)) {attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
            }
            bindingResult = binder.getBindingResult();}

        // Add resolved attribute and BindingResult at the end of the model
        // at the end of the model  把解决好的属性放到 Model 的末尾~~~
        // 可以即使是标注在入参上的 @ModelAtrribute 的属性值,最终也都是会放进 Model 里的~~~ 可怕吧
        Map<String, Object> bindingResultModel = bindingResult.getModel();
        mavContainer.removeAttributes(bindingResultModel);
        mavContainer.addAllAttributes(bindingResultModel);

        return attribute;
    }

    // 此方法 `ServletModelAttributeMethodProcessor` 子类是有复写的哦~~~~
    // 使用了更强大的:ServletRequestDataBinder.bind(ServletRequest request)方法
    protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {((WebRequestDataBinder) binder).bind(request);
    }
}

模型属性 首先从 Model 中获取 ,若没有获取到,就使用 默认构造函数 (可能是有无参,也可能是有参)创建,然后会把ServletRequest 请求的数据绑定上来,然后进行 @Valid 校验(若添加有校验注解的话),最后会把属性添加到 Model 里面

最后加进去的代码是:mavContainer.addAllAttributes(bindingResultModel);这里我贴出参考值:

如下示例,它会正常打印 person 的值,而不是 null(因为 Model 内有 person 了~)
请求链接是:/testModelAttr?name=wo&age=10

    @GetMapping("/testModelAttr")
    public void testModelAttr(@Valid Person person, ModelMap modelMap) {Object personAttr = modelMap.get("person");
        System.out.println(personAttr); //Person(name=wo, age=10)
    }

注意:虽然 person 上没有标注 @ModelAtrribute,但是modelMap.get("person") 依然是能够获取到值的哦,至于为什么,原因上面已经分析了,可自行思考。


下例中:

    @GetMapping("/testModelAttr")
    public void testModelAttr(Integer age, Person person, ModelMap modelMap) {System.out.println(age); // 直接封装的值
        System.out.println("-------------------------------");
        System.out.println(modelMap.get("age"));
        System.out.println(modelMap.get("person"));
    }

请求:/testModelAttr?name=wo&age=10 输入为:

10
-------------------------------
null
Person(name=wo, age=10)

可以看到 普通类型 (注意理解这个普通类型)若不标注@ModelAtrribute,它是不会自动识别为Model 而放进来的哟~~~ 若你这么写:

    @GetMapping("/testModelAttr")
    public void testModelAttr(@ModelAttribute("age") Integer age, Person person, ModelMap modelMap) {System.out.println(age); // 直接封装的值
        System.out.println("-------------------------------");
        System.out.println(modelMap.get("age"));
        System.out.println(modelMap.get("person"));
    }

打印如下:

10
-------------------------------
10
Person(name=wo, age=10)

请务必注意以上 case 的区别,加深记忆。使用的时候可别踩坑了~


== 再看它对方法(返回值)的处理(很简单):==

public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

    // 方法返回值上标注有 @ModelAttribute 注解(或者非简单类型)默认都会放进 Model 内哦~~
    @Override
    public boolean supportsReturnType(MethodParameter returnType) {return (returnType.hasMethodAnnotation(ModelAttribute.class) ||
                (this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType())));
    }

    // 这个处理就非常非常的简单了,注意:null 值是不放的哦~~~~
    // 注意:void 的话  returnValue 也是 null
    @Override
    public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
            ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {if (returnValue != null) {String name = ModelFactory.getNameForReturnValue(returnValue, returnType);
            mavContainer.addAttribute(name, returnValue);
        }
    }
}

它对 方法返回值 的处理非常简单,只要不是 null(当然不能是 void)就都会放进Model 里面,供以使用

总结

本文介绍的是 @ModelAttribute 的核心原理,他对我们实际使用有重要的理论支撑。下面系列文章主要在原理的基础上,展示各种各样场景下的使用Demo,敬请关注~

相关阅读

从原理层面掌握 @SessionAttributes 的使用【一起学 Spring MVC】
从原理层面掌握 @RequestAttribute、@SessionAttribute 的使用【一起学 Spring MVC】
从原理层面掌握 @ModelAttribute 的使用(使用篇)【一起学 Spring MVC】

知识交流

==The last:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被 作者本人许可的~==

** 若对技术内容感兴趣可以加入 wx 群交流:Java 高工、架构师 3 群
若群二维码失效,请加 wx 号:fsx641385712(或者扫描下方 wx 二维码)。并且备注:"java 入群" 字样,会手动邀请入群 **

若文章 格式混乱 或者 图片裂开,请点击 `:原文链接 - 原文链接 - 原文链接

退出移动版