从原理层面掌握@ModelAttribute的使用(核心原理篇)【享学Spring MVC】(上)

简介: 从原理层面掌握@ModelAttribute的使用(核心原理篇)【享学Spring MVC】(上)

前言


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

使用这种注解的方式来开发控制器我认为最重要的优势是:


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


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


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


不过没关系,有幸看到此文,能够帮你弥补弥补这块的盲区。

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


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


@ModelAttribute官方解释


首先看看Spring官方的JavaDoc对它怎么说:它将方法参数/方法返回值绑定到web view的Model里面。只支持@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。

和它相关的两个类是ModelFactory和ModelAttributeMethodProcessor


@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就得登场了。

相关文章
|
8天前
|
XML Java 开发者
Spring Boot开箱即用可插拔实现过程演练与原理剖析
【11月更文挑战第20天】Spring Boot是一个基于Spring框架的项目,其设计目的是简化Spring应用的初始搭建以及开发过程。Spring Boot通过提供约定优于配置的理念,减少了大量的XML配置和手动设置,使得开发者能够更专注于业务逻辑的实现。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,为开发者提供一个全面的理解。
19 0
|
1月前
|
Java Spring 容器
Spring底层原理大致脉络
Spring底层原理大致脉络
|
1月前
|
JSON 前端开发 Java
SSM:SpringMVC
本文介绍了SpringMVC的依赖配置、请求参数处理、注解开发、JSON处理、拦截器、文件上传下载以及相关注意事项。首先,需要在`pom.xml`中添加必要的依赖,包括Servlet、JSTL、Spring Web MVC等。接着,在`web.xml`中配置DispatcherServlet,并设置Spring MVC的相关配置,如组件扫描、默认Servlet处理器等。然后,通过`@RequestMapping`等注解处理请求参数,使用`@ResponseBody`返回JSON数据。此外,还介绍了如何创建和配置拦截器、文件上传下载的功能,并强调了JSP文件的放置位置,避免404错误。
|
1月前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
【10月更文挑战第1天】Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
128 9
|
1月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
57 2
|
1月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
123 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
1月前
|
XML 前端开发 Java
拼多多1面:聊聊Spring MVC的工作原理!
本文详细剖析了Spring MVC的工作原理,涵盖其架构、工作流程及核心组件。Spring MVC采用MVC设计模式,通过DispatcherServlet、HandlerMapping、Controller和ViewResolver等组件高效处理Web请求。文章还探讨了DispatcherServlet的初始化和请求处理流程,以及HandlerMapping和Controller的角色。通过理解这些核心概念,开发者能更好地构建可维护、可扩展的Web应用。适合面试准备和技术深挖
43 0
|
1月前
|
负载均衡 Java API
Spring Cloud原理详解
Spring Cloud原理详解
69 0
|
1月前
|
负载均衡 Java 网络架构
Spring Cloud原理详解
介绍了Spring Cloud的原理和核心组件,包括服务注册与发现、配置管理、负载均衡、断路器、智能路由、分布式消息传递、分布式追踪和服务熔断等,旨在帮助开发人员快速构建和管理微服务架构中的分布式系统。
56 0
|
Java Spring
Spring原理学习系列之五:IOC原理之Bean加载
其实很多同学都想通过阅读框架的源码以汲取框架设计思想以及编程营养,Spring框架其实就是个很好的框架源码学习对象。我们都知道Bean是Spring框架的最小操作单元,Spring框架通过对于Bean的统一管理实现其IOC以及AOP等核心的框架功能,那么Spring框架是如何把Bean加载到环境中来进行管理的呢?本文将围绕这个话题进行详细的阐述,并配合Spring框架的源码解析。
Spring原理学习系列之五:IOC原理之Bean加载
下一篇
无影云桌面