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

相关文章
|
25天前
|
Java 关系型数据库 数据库
深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
在Java开发中,Spring框架通过事务管理机制,帮我们轻松实现了这种“承诺”。它不仅封装了底层复杂的事务控制逻辑(比如手动开启、提交、回滚事务),还提供了灵活的配置方式,让开发者能专注于业务逻辑,而不用纠结于事务细节。
|
2月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
177 0
|
2月前
|
缓存 安全 Java
Spring 框架核心原理与实践解析
本文详解 Spring 框架核心知识,包括 IOC(容器管理对象)与 DI(容器注入依赖),以及通过注解(如 @Service、@Autowired)声明 Bean 和注入依赖的方式。阐述了 Bean 的线程安全(默认单例可能有安全问题,需业务避免共享状态或设为 prototype)、作用域(@Scope 注解,常用 singleton、prototype 等)及完整生命周期(实例化、依赖注入、初始化、销毁等步骤)。 解析了循环依赖的解决机制(三级缓存)、AOP 的概念(公共逻辑抽为切面)、底层动态代理(JDK 与 Cglib 的区别)及项目应用(如日志记录)。介绍了事务的实现(基于 AOP
111 0
|
2月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
110 0
|
2月前
|
JSON 前端开发 Java
第05课:Spring Boot中的MVC支持
第05课:Spring Boot中的MVC支持
148 0
|
2月前
|
监控 架构师 NoSQL
spring 状态机 的使用 + 原理 + 源码学习 (图解+秒懂+史上最全)
spring 状态机 的使用 + 原理 + 源码学习 (图解+秒懂+史上最全)
|
Java Spring
Spring原理学习系列之五:IOC原理之Bean加载
其实很多同学都想通过阅读框架的源码以汲取框架设计思想以及编程营养,Spring框架其实就是个很好的框架源码学习对象。我们都知道Bean是Spring框架的最小操作单元,Spring框架通过对于Bean的统一管理实现其IOC以及AOP等核心的框架功能,那么Spring框架是如何把Bean加载到环境中来进行管理的呢?本文将围绕这个话题进行详细的阐述,并配合Spring框架的源码解析。
Spring原理学习系列之五:IOC原理之Bean加载
|
2月前
|
Java Spring 容器
SpringBoot自动配置的原理是什么?
Spring Boot自动配置核心在于@EnableAutoConfiguration注解,它通过@Import导入配置选择器,加载META-INF/spring.factories中定义的自动配置类。这些类根据@Conditional系列注解判断是否生效。但Spring Boot 3.0后已弃用spring.factories,改用新格式的.imports文件进行配置。
725 0
|
6月前
|
前端开发 Java 数据库
微服务——SpringBoot使用归纳——Spring Boot集成Thymeleaf模板引擎——Thymeleaf 介绍
本课介绍Spring Boot集成Thymeleaf模板引擎。Thymeleaf是一款现代服务器端Java模板引擎,支持Web和独立环境,可实现自然模板开发,便于团队协作。与传统JSP不同,Thymeleaf模板可以直接在浏览器中打开,方便前端人员查看静态原型。通过在HTML标签中添加扩展属性(如`th:text`),Thymeleaf能够在服务运行时动态替换内容,展示数据库中的数据,同时兼容静态页面展示,为开发带来灵活性和便利性。
296 0
|
2月前
|
缓存 JSON 前端开发
第07课:Spring Boot集成Thymeleaf模板引擎
第07课:Spring Boot集成Thymeleaf模板引擎
368 0
第07课:Spring Boot集成Thymeleaf模板引擎

热门文章

最新文章