从原理层面掌握@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就得登场了。

相关文章
|
2月前
|
Java
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
这篇文章是Spring5框架的实战教程,深入讲解了AOP的基本概念、如何利用动态代理实现AOP,特别是通过JDK动态代理机制在不修改源代码的情况下为业务逻辑添加新功能,降低代码耦合度,并通过具体代码示例演示了JDK动态代理的实现过程。
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
|
19天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
7天前
|
XML 缓存 前端开发
springMVC02,restful风格,请求转发和重定向
文章介绍了RESTful风格的基本概念和特点,并展示了如何使用SpringMVC实现RESTful风格的请求处理。同时,文章还讨论了SpringMVC中的请求转发和重定向的实现方式,并通过具体代码示例进行了说明。
springMVC02,restful风格,请求转发和重定向
|
2月前
|
Java 数据库连接 Spring
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
文章是关于Spring、SpringMVC、Mybatis三个后端框架的超详细入门教程,包括基础知识讲解、代码案例及SSM框架整合的实战应用,旨在帮助读者全面理解并掌握这些框架的使用。
后端框架入门超详细 三部曲 Spring 、SpringMVC、Mybatis、SSM框架整合案例 【爆肝整理五万字】
|
2月前
|
XML JSON 数据库
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
这篇文章详细介绍了RESTful的概念、实现方式,以及如何在SpringMVC中使用HiddenHttpMethodFilter来处理PUT和DELETE请求,并通过具体代码案例分析了RESTful的使用。
SpringMVC入门到实战------七、RESTful的详细介绍和使用 具体代码案例分析(一)
|
2月前
|
XML Java 数据格式
Spring5入门到实战------2、IOC容器底层原理
这篇文章深入探讨了Spring5框架中的IOC容器,包括IOC的概念、底层原理、以及BeanFactory接口和ApplicationContext接口的介绍。文章通过图解和实例代码,解释了IOC如何通过工厂模式和反射机制实现对象的创建和管理,以及如何降低代码耦合度,提高开发效率。
Spring5入门到实战------2、IOC容器底层原理
|
2月前
|
前端开发 应用服务中间件 数据库
SpringMVC入门到实战------八、RESTful案例。SpringMVC+thymeleaf+BootStrap+RestFul实现员工信息的增删改查
这篇文章通过一个具体的项目案例,详细讲解了如何使用SpringMVC、Thymeleaf、Bootstrap以及RESTful风格接口来实现员工信息的增删改查功能。文章提供了项目结构、配置文件、控制器、数据访问对象、实体类和前端页面的完整源码,并展示了实现效果的截图。项目的目的是锻炼使用RESTful风格的接口开发,虽然数据是假数据并未连接数据库,但提供了一个很好的实践机会。文章最后强调了这一章节主要是为了练习RESTful,其他方面暂不考虑。
SpringMVC入门到实战------八、RESTful案例。SpringMVC+thymeleaf+BootStrap+RestFul实现员工信息的增删改查
|
2月前
|
开发框架 前端开发 .NET
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
36 0
|
5月前
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
157 0
|
5月前
|
开发框架 前端开发 JavaScript
JavaScript云LIS系统源码ASP.NET CORE 3.1 MVC + SQLserver + Redis医院实验室信息系统源码 医院云LIS系统源码
实验室信息系统(Laboratory Information System,缩写LIS)是一类用来处理实验室过程信息的软件,云LIS系统围绕临床,云LIS系统将与云HIS系统建立起高度的业务整合,以体现“以病人为中心”的设计理念,优化就诊流程,方便患者就医。
68 0
下一篇
无影云桌面