在 SpringMVC 中,大家都知道有一个特殊的参数 Model,它的使用方式像下面这样:
1 | @Controller |
这样一个看起来人畜无害的普通参数,里边也会包含你的知识盲区吗?说不定真的包含了,不信你就往下看。
1.基本用法
仅仅从使用上来说,Model 有两方面的功能:
- 携带参数
- 返回参数
先说携带参数:当我们在一个接口中放上 Model 这个参数之后,这个 Model 不一定是空白的,它里边可能已经有了携带的参数,携带的参数可能来自上一次 @SessionAttributes
注解标记过的参数,也可能来自 @ModelAttribute
注解标记过的全局参数。
在来说返回参数,Model 中的属性,你最终都可以在前端视图中获取到,这个没啥好说的。
前面提到了 @ModelAttribute
注解,这个如果有小伙伴不清楚,可以看看松哥之前的文章:
或者在公众号后台回复 666 有文字教程,回复 ssm 有视频教程,都有关于 @ModelAttribute
的讲解。
至于 @SessionAttributes
,松哥现在和大家分享一下,毕竟只有先懂怎么用,后面才会懂源码。
2.@SessionAttributes
@SessionAttributes
作用于处理器类上,这个注解可以把参数存储到 session 中,进而可以实现在多个请求之间传递参数。
@SessionAttributes
的作用类似于 Session 的 Attribute 属性,但不完全一样,一般来说 @SessionAttributes
设置的参数只用于临时的参数传递,而不是长期的保存,参数用完之后可以通过 SessionStatus 将之清除。
通过 @SessionAttributes
注解设置的参数我们可以在三个地方获取:
- 在当前的视图中直接通过
request.getAttribute
或session.getAttribute
获取。
例如如下接口:
1 | @Controller |
name 属性会被临时保存在 session 中,在前端页面中,我们既可以从 request 域中获取也可以从 session 域中获取,以 Thymeleaf 页面模版为例:
1 | <!DOCTYPE html> |
如果没有使用 @SessionAttributes
注解,那就只能从 request 域中获取,而不能从 session 域中获取。
- 在后面的请求中,也可以通过
session.getAttribute
获取。
参数既然存在 session 中,那就有一个好处,就是无论是服务器端跳转还是客户端跳转,参数都不会丢失。例如如下接口:
1 | @Controller |
无论开发者访问 http://localhost:8080/01
还是 http://localhost:8080/02
,都能看到页面,并且 name 属性的值也能在页面上渲染出来。
不知道小伙伴们有没有想起来什么?对了,重定向的参数传递问题,之前松哥和大家分享了 FlashMap(SpringMVC 中的参数还能这么传递?涨姿势了!),现在你看到了,这也是一种方案。
- 在后续的请求中,也可以直接从 Model 中获取。
1 | @Controller |
访问完 /01
接口之后,再去访问 /03
接口,也可以拿到 Model 中的数据。
第三种方式还有一个变体,如下:
1 | @Controller |
就是参数中不使用 Model,而是使用 @SessionAttribute
注解,直接将 session 中的属性绑定到参数上。
使用了 @SessionAttributes
注解之后,可以调用 SessionStatus.setComplete
方法来清除数据,注意这个方法只是清除 SessionAttribute
里的参数,而不会清除正常 Session 中的参数。
例如下面这样:
1 | @Controller |
首先访问 /01
接口,访问完了就有数据了,这个时候访问 /04
接口,就会打印出数据,继续访问 /05
接口,访问完成后,再去访问 /04
接口,此时就会发现数据没了,因为被清除了。
现在,大家对 @SessionAttributes
注解的用法应该有了一定的认知了吧。
3.ModelFactory
接下来我们就来研究一下 ModelFactory,ModelFactory 是用来维护 Model 的,上面这一切,我们可以从 ModelFactory 中找到端倪。
整体上来说,ModelFactory 包含两方面的功能:1.初始化 Model;2.将 Model 中相应的参数更新到 SessionAtrributes 中。两方面的功能我们分别来看,先来看初始化问题。
1 | public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) |
这个 initModel 方法比较逻辑比较简单:
- 首先它会从
@SessionAttributes
中取出参数,然后合并进 ModelAndViewContainer 容器中(不懂 ModelAndViewContainer 容器的话,可以参考松哥前面的文章:Spring Boot 中如何统一 API 接口响应格式?)。 - 接下来调用含有
@ModelAttribute
注解的方法,并将结果合并进 ModelAndViewContainer 容器中。 - 寻找那些既有
@ModelAttribute
注解又有@SessionAttributes
注解的属性,找到后,如果这些属性不存在于 ModelAndViewContainer 容器中,就从 SessionAttributes 中获取并设置到 ModelAndViewContainer 容器中。
我们先来看看第一个 retrieveAttributes 方法:
1 | public Map<String, Object> retrieveAttributes(WebRequest request) { |
这个其实没啥好说的,因为逻辑很清晰,knownAttributeNames 就是我们在使用 @SessionAttributes
注解时配置的属性名字,属性名字可以是一个数组。遍历 knownAttributeNames 属性,从 session 中获取相关数据存入 Map 集合中。
再来看第二个 invokeModelAttributeMethods 方法:
1 | private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer container) |
- 首先获取含有
@ModelAttribute
注解的方法,然后获取到该注解。 - 获取
@ModelAttribute
注解,并提取出它的 name 属性值,然后查看 ModelAndViewContainer 容器中是否已经包含了该属性,如果已经包含了,并且在@ModelAttribute
注解中设置了不绑定,则将该属性添加到 ModelAndViewContainer 容器中的禁止绑定上面去。 - 接下来通过
invokeForRequest
方法去调用含有@ModelAttribute
注解的方法,并获取返回值。 - 如果含有
@ModelAttribute
注解的方法返回值为 void,则该方法到此为止。 - 接下来解析出返回值的参数名,有的小伙伴们说,参数名不就是
@ModelAttribute
注解中配置的 name 属性吗?这当然没错!但是有时候用户没有配置 name 属性,那么这个时候就会对应一套默认的 name 生成方案。默认的名字生成方案是这样的:- 如果返回对象前两个字母都是大写,那就原封不动返回,否则首字母小写后返回。
- 如果返回类型是数组或者集合,则在真实类型后加上 List,例如 List
对象 longList。
- 有了 returnValueName 之后,再去判断是否要禁止属性绑定。最后如果 ModelAndViewContainer 容器中不包含该属性,则添加进来。
这就是 Model 初始化的过程,可以看到,数据最终都被保存进 ModelAndViewContainer 容器中了,至于在该容器中数据被保存到哪个属性,则要看实际情况,可能是 defaultModel 也可能是 redirectModel,具体参见Spring Boot 中如何统一 API 接口响应格式?)。
最后我们再来看看 ModelFactory 中修改 Model 的过程:
1 | public void updateModel(NativeWebRequest request, ModelAndViewContainer container) throws Exception { |
修改的时候会首先判断一下是否已经调用了 sessionStatus.setComplete();
方法,如果调用过了,就执行清除操作,否则就进行正常的更新操作即可,更新的数据就是 ModelAndViewContainer 中的 defaultModel。最后判断是否需要进行页面渲染,如果需要,再给参数分别设置 BindingResult 以备视图使用。
现在,大家应该已经清楚了 ModelFactory 的功能了。
一句话,ModelFactory 在初始化的时候,就直接从 SessionAttributes 以及 ModelAttribute 处加载到数据,放到 ModelAndViewContainer 中,更新的时候,则有可能清除 SessionAttributes 中的数据。这里大家需要把握一点,就是数据最终被存入 ModelAndViewContainer 中了。
3.相关的参数解析器
这是 Model 初始化的过程,初始化完成后,参数最终会在参数解析器中被解析,关于参数解析器,大家可以参考如下两篇文章:
这里涉及到的参数解析器就是 ModelMethodProcessor,我们来看下它里边两个关键的方法:
1 | @Override |
可以看到,支持的参数类型就是 Model,参数的值则是直接返回 ModelAndViewContainer 中的 model 对象。
这里还有一个类似的参数处理器 MapMethodProcessor:
1 | @Override |
这个是处理 Map 类型的参数,最终返回的也是 ModelAndViewContainer 中的 model,你是否发现什么了?对了,在本文第二小节的案例中,你把 Model 参数换成 Map 或者 ModelMap(ModelMap 本质上也是 Map,使用的参数解析器也是 MapMethodProcessor),最终效果是一样的!
前面我们还使用了 @SessionAttribute
注解,这个注解的 name 属性就绑定了 SessionAttributes 中对应的属性并赋值给变量,它使用的参数解析器是 SessionAttributeMethodArgumentResolver,我们来看下它里边的核心方法:
1 | public class SessionAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { |
可以看到,这个参数最终对应的值就是从 session 中取出对应的 name 属性值。
最后,我们再来梳理一下整个过程:当请求到达后,首先要初始化 Model,初始化 Model 的时候,会根据 @SessionAttributes
注解从 session 中读取相关数据放入 ModelAndViewContainer 中,同时也会加载 @ModelAttribute
注解配置的全局数据到 ModelAndViewContainer 中。最终在参数解析器中,返回 ModelAndViewContainer 中的 model 即可。
现在大家可以回答文章标题提出的问题了吧!