从原理层面掌握@SessionAttributes的使用【享学Spring MVC】(下)

简介: 从原理层面掌握@SessionAttributes的使用【享学Spring MVC】(下)

ModelFactory


Spring MVC对@SessionAttributes的处理操作入口,是在ModelFactory.initModel()方法里会对@SessionAttributes的注解进行解析、处理,然后方法完成之后也会对它进行属性同步。


ModelFactory是用来维护Model的,具体包含两个功能:


  • 处理器执行前,初始化Model
  • 处理器执行后,将Model中相应的参数同步更新到SessionAttributes中(不是全量,而是符合条件的那些)


// @since 3.1
public final class ModelFactory {
  // ModelMethod它是一个私有内部类,持有InvocableHandlerMethod的引用  和方法的dependencies依赖们
  private final List<ModelMethod> modelMethods = new ArrayList<>();
  private final WebDataBinderFactory dataBinderFactory;
  private final SessionAttributesHandler sessionAttributesHandler;
  public ModelFactory(@Nullable List<InvocableHandlerMethod> handlerMethods, WebDataBinderFactory binderFactory, SessionAttributesHandler attributeHandler) {
    // 把InvocableHandlerMethod转为内部类ModelMethod
    if (handlerMethods != null) {
      for (InvocableHandlerMethod handlerMethod : handlerMethods) {
        this.modelMethods.add(new ModelMethod(handlerMethod));
      }
    }
    this.dataBinderFactory = binderFactory;
    this.sessionAttributesHandler = attributeHandler;
  }
  // 该方法完成Model的初始化
  public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception {
    // 先拿到sessionAttr里所有的属性们(首次进来肯定木有,但同一个session第二次进来就有了)
    Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request);
    // 和当前请求中 已经有的model合并属性信息
    // 注意:sessionAttributes中只有当前model不存在的属性,它才会放进去
    container.mergeAttributes(sessionAttributes);
    // 此方法重要:调用模型属性方法来填充模型  这里ModelAttribute会生效
    // 关于@ModelAttribute的内容  我放到了这里:https://blog.csdn.net/f641385712/article/details/98260361
    // 总之:完成这步之后 Model就有值了~~~~
    invokeModelAttributeMethods(request, container);
    // 最后,最后,最后还做了这么一步操作~~~
    // findSessionAttributeArguments的作用:把@ModelAttribute的入参也列入SessionAttributes(非常重要) 详细见下文
    // 这里一定要掌握:因为使用中的坑坑经常是因为没有理解到这块逻辑
    for (String name : findSessionAttributeArguments(handlerMethod)) {
      // 若ModelAndViewContainer不包含此name的属性   才会进来继续处理  这一点也要注意
      if (!container.containsAttribute(name)) {
        // 去请求域里检索为name的属性,若请求域里没有(也就是sessionAttr里没有),此处会抛出异常的~~~~
        Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
        if (value == null) {
          throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name);
        }
        // 把从sessionAttr里检索到的属性也向容器Model内放置一份~
        container.addAttribute(name, value);
      }
    }
  }
  // 把@ModelAttribute标注的入参也列入SessionAttributes 放进sesson里(非常重要)
  // 这个动作是很多开发者都忽略了的
  private List<String> findSessionAttributeArguments(HandlerMethod handlerMethod) {
    List<String> result = new ArrayList<>();
    // 遍历所有的方法参数
    for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
      // 只有参数里标注了@ModelAttribute的才会进入继续解析~~~
      if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
        // 关于getNameForParameter拿到modelKey的方法,这个策略是需要知晓的
        String name = getNameForParameter(parameter);
        Class<?> paramType = parameter.getParameterType();
        // 判断isHandlerSessionAttribute为true的  才会把此name合法的添加进来
        // (也就是符合@SessionAttribute标注的key或者type的)
        if (this.sessionAttributesHandler.isHandlerSessionAttribute(name, paramType)) {
          result.add(name);
        }
      }
    }
    return result;
  }
  // 静态方法:决定了parameter的名字  它是public的,因为ModelAttributeMethodProcessor里也有使用
  // 请注意:这里不是MethodParameter.getParameterName()获取到的形参名字,而是有自己的一套规则的
  // @ModelAttribute指定了value值就以它为准,否则就是类名的首字母小写(当然不同类型不一样,下面有给范例)
  public static String getNameForParameter(MethodParameter parameter) {
    ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class);
    String name = (ann != null ? ann.value() : null);
    return (StringUtils.hasText(name) ? name : Conventions.getVariableNameForParameter(parameter));
  }
  // 关于方法这块的处理逻辑,和上差不多,主要是返回类型和实际类型的区分
  // 比如List<String>它对应的名是:stringList。即使你的返回类型是Object~~~
  public static String getNameForReturnValue(@Nullable Object returnValue, MethodParameter returnType) {
    ModelAttribute ann = returnType.getMethodAnnotation(ModelAttribute.class);
    if (ann != null && StringUtils.hasText(ann.value())) {
      return ann.value();
    } else {
      Method method = returnType.getMethod();
      Assert.state(method != null, "No handler method");
      Class<?> containingClass = returnType.getContainingClass();
      Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass);
      return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue);
    }
  }
  // 将列为@SessionAttributes的模型数据,提升到sessionAttr里
  public void updateModel(NativeWebRequest request, ModelAndViewContainer container) throws Exception {
    ModelMap defaultModel = container.getDefaultModel();
    if (container.getSessionStatus().isComplete()){
      this.sessionAttributesHandler.cleanupAttributes(request);
    } else { // 存储到sessionAttr里
      this.sessionAttributesHandler.storeAttributes(request, defaultModel);
    }
    // 若该request还没有被处理  并且 Model就是默认defaultModel
    if (!container.isRequestHandled() && container.getModel() == defaultModel) {
      updateBindingResult(request, defaultModel);
    }
  }
  // 将bindingResult属性添加到需要该属性的模型中。
  // isBindingCandidate:给定属性在Model模型中是否需要bindingResult。
  private void updateBindingResult(NativeWebRequest request, ModelMap model) throws Exception {
    List<String> keyNames = new ArrayList<>(model.keySet());
    for (String name : keyNames) {
      Object value = model.get(name);
      if (value != null && isBindingCandidate(name, value)) {
        String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + name;
        if (!model.containsAttribute(bindingResultKey)) {
          WebDataBinder dataBinder = this.dataBinderFactory.createBinder(request, value, name);
          model.put(bindingResultKey, dataBinder.getBindingResult());
        }
      }
    }
  }
  // 看看这个静态内部类ModelMethod
  private static class ModelMethod {
    // 持有可调用的InvocableHandlerMethod 这个方法
    private final InvocableHandlerMethod handlerMethod;
    // 这字段是搜集该方法标注了@ModelAttribute注解的入参们
    private final Set<String> dependencies = new HashSet<>();
    public ModelMethod(InvocableHandlerMethod handlerMethod) {
      this.handlerMethod = handlerMethod;
      // 把方法入参中所有标注了@ModelAttribute了的Name都搜集进来
      for (MethodParameter parameter : handlerMethod.getMethodParameters()) {
        if (parameter.hasParameterAnnotation(ModelAttribute.class)) {
          this.dependencies.add(getNameForParameter(parameter));
        }
      }
    }
    ...
  }
}


ModelFactory协助在控制器方法调用之前初始化Model模型,并在调用之后对其进行更新。


  • 初始化时,通过调用方法上标注有@ModelAttribute的方法,使用临时存储在会话中的属性填充模型。
  • 在更新时,模型属性与会话同步,如果缺少,还将添加BindingResult属性。


关于默认名称规则的核心在Conventions.getVariableNameForParameter(parameter)这个方法里,我在上文给了一个范例,介绍常见的各个类型的输出值,大家记忆一下便可。参考:从原理层面掌握HandlerMethod、InvocableHandlerMethod、ServletInvocableHandlerMethod的使用【享学Spring MVC】


将一个参数设置到@SessionAttribute中需要同时满足两个条件:


在@SessionAttribute注解中设置了参数的名字或者类型

在处理器(Controller)中将参数设置到了Model中(这样方法结束后会自动的同步到SessionAttr里)


总结


@SessionAttributes指的是Spring MVC的Session。向其中添加值得时候,同时会向 HttpSession中添加一条。在sessionStatus.setComplete();的时候,会清空Spring MVC

的Session,同时清除对应键的HttpSession内容,但是通过,request.getSession.setAttribute()方式添加的内容不会被清除掉。其他情况下,Spring MVC的Session和HttpSession使用情况相同。


这篇文章介绍了@SessionAttributes的核心处理原理,以及也给了一个Demo来介绍它的基本使用,不出意外阅读下来你对它应该是有很好的收获的,希望能帮助到你简化开发~

相关文章
|
25天前
|
Java 关系型数据库 数据库
深度剖析【Spring】事务:万字详解,彻底掌握传播机制与事务原理
在Java开发中,Spring框架通过事务管理机制,帮我们轻松实现了这种“承诺”。它不仅封装了底层复杂的事务控制逻辑(比如手动开启、提交、回滚事务),还提供了灵活的配置方式,让开发者能专注于业务逻辑,而不用纠结于事务细节。
|
5月前
|
存储 人工智能 自然语言处理
RAG 调优指南:Spring AI Alibaba 模块化 RAG 原理与使用
通过遵循以上最佳实践,可以构建一个高效、可靠的 RAG 系统,为用户提供准确和专业的回答。这些实践涵盖了从文档处理到系统配置的各个方面,能够帮助开发者构建更好的 RAG 应用。
2576 114
|
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 状态机 的使用 + 原理 + 源码学习 (图解+秒懂+史上最全)
|
4月前
|
前端开发 Java 数据库连接
Spring核心原理剖析与解说
每个部分都是将一种巨大并且复杂的技术理念传达为更易于使用的接口,而这就是Spring的价值所在,它能让你专注于开发你的应用,而不必从头开始设计每一部分。
165 32
|
4月前
|
Java 开发者 Spring
Spring框架 - 深度揭秘Spring框架的基础架构与工作原理
所以,当你进入这个Spring的世界,看似一片混乱,但细看之下,你会发现这里有个牢固的结构支撑,一切皆有可能。不论你要建设的是一座宏大的城堡,还是个小巧的花园,只要你的工具箱里有Spring,你就能轻松搞定。
187 9
|
5月前
|
安全 前端开发 Java
Spring Boot 项目中触发 Circular View Path 错误的原理与解决方案
在Spring Boot开发中,**Circular View Path**错误常因视图解析与Controller路径重名引发。当视图名称(如`login`)与请求路径相同,Spring MVC无法区分,导致无限循环调用。解决方法包括:1) 明确指定视图路径,避免重名;2) 将视图文件移至子目录;3) 确保Spring Security配置与Controller路径一致。通过合理设定视图和路径,可有效避免该问题,确保系统稳定运行。
343 0