在 SpringMVC 中,大家都知道有一个特殊的参数 Model,它的使用方式像下面这样:
1 2 3 4 5 6 7 8
| @Controller public class HelloController { @GetMapping("/01") public String hello(Model model) { model.addAttribute("name", "javaboy"); return "01"; } }
|
这样一个看起来人畜无害的普通参数,里边也会包含你的知识盲区吗?说不定真的包含了,不信你就往下看。
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 2 3 4 5 6 7 8 9
| @Controller @SessionAttributes("name") public class HelloController { @GetMapping("/01") public String hello(Model model) { model.addAttribute("name", "javaboy"); return "01"; } }
|
name 属性会被临时保存在 session 中,在前端页面中,我们既可以从 request 域中获取也可以从 session 域中获取,以 Thymeleaf 页面模版为例:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div> <div th:text="${#request.getAttribute('name')}"></div> <div th:text="${#session.getAttribute('name')}"></div> </div> </body> </html>
|
如果没有使用 @SessionAttributes
注解,那就只能从 request 域中获取,而不能从 session 域中获取。
- 在后面的请求中,也可以通过
session.getAttribute
获取。
参数既然存在 session 中,那就有一个好处,就是无论是服务器端跳转还是客户端跳转,参数都不会丢失。例如如下接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Controller @SessionAttributes("name") public class HelloController { @GetMapping("/01") public String hello(Model model) { model.addAttribute("name", "javaboy"); return "forward:/index"; }
@GetMapping("/02") public String hello2(Model model) { model.addAttribute("name", "javaboy"); return "redirect:/index"; }
@GetMapping("/index") public String index() { return "01"; } }
|
无论开发者访问 http://localhost:8080/01
还是 http://localhost:8080/02
,都能看到页面,并且 name 属性的值也能在页面上渲染出来。
不知道小伙伴们有没有想起来什么?对了,重定向的参数传递问题,之前松哥和大家分享了 FlashMap(SpringMVC 中的参数还能这么传递?涨姿势了!),现在你看到了,这也是一种方案。
- 在后续的请求中,也可以直接从 Model 中获取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Controller @SessionAttributes("name") public class HelloController { @GetMapping("/01") public String hello(Model model) { model.addAttribute("name", "javaboy"); return "forward:/index"; } @GetMapping("/03") @ResponseBody public void hello3(Model model) { Object name = model.getAttribute("name"); System.out.println("name = " + name); } }
|
访问完 /01
接口之后,再去访问 /03
接口,也可以拿到 Model 中的数据。
第三种方式还有一个变体,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Controller @SessionAttributes("name") public class HelloController { @GetMapping("/01") public String hello(Model model) { model.addAttribute("name", "javaboy"); return "forward:/index"; } @GetMapping("/04") @ResponseBody public void hello4(@SessionAttribute("name") String name) { System.out.println("name = " + name); } }
|
就是参数中不使用 Model,而是使用 @SessionAttribute
注解,直接将 session 中的属性绑定到参数上。
使用了 @SessionAttributes
注解之后,可以调用 SessionStatus.setComplete
方法来清除数据,注意这个方法只是清除 SessionAttribute
里的参数,而不会清除正常 Session 中的参数。
例如下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Controller @SessionAttributes("name") public class HelloController { @GetMapping("/01") public String hello(Model model) { model.addAttribute("name", "javaboy"); return "forward:/index"; }
@GetMapping("/04") @ResponseBody public void hello4(@SessionAttribute("name") String name) { System.out.println("name = " + name); }
@GetMapping("/05") @ResponseBody public void hello5(SessionStatus sessionStatus) { sessionStatus.setComplete(); } }
|
首先访问 /01
接口,访问完了就有数据了,这个时候访问 /04
接口,就会打印出数据,继续访问 /05
接口,访问完成后,再去访问 /04
接口,此时就会发现数据没了,因为被清除了。
现在,大家对 @SessionAttributes
注解的用法应该有了一定的认知了吧。
3.ModelFactory
接下来我们就来研究一下 ModelFactory,ModelFactory 是用来维护 Model 的,上面这一切,我们可以从 ModelFactory 中找到端倪。
整体上来说,ModelFactory 包含两方面的功能:1.初始化 Model;2.将 Model 中相应的参数更新到 SessionAtrributes 中。两方面的功能我们分别来看,先来看初始化问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception { Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request); container.mergeAttributes(sessionAttributes); invokeModelAttributeMethods(request, container); for (String name : findSessionAttributeArguments(handlerMethod)) { if (!container.containsAttribute(name)) { Object value = this.sessionAttributesHandler.retrieveAttribute(request, name); if (value == null) { throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name); } container.addAttribute(name, value); } } }
|
这个 initModel 方法比较逻辑比较简单:
- 首先它会从
@SessionAttributes
中取出参数,然后合并进 ModelAndViewContainer 容器中(不懂 ModelAndViewContainer 容器的话,可以参考松哥前面的文章:Spring Boot 中如何统一 API 接口响应格式?)。
- 接下来调用含有
@ModelAttribute
注解的方法,并将结果合并进 ModelAndViewContainer 容器中。
- 寻找那些既有
@ModelAttribute
注解又有 @SessionAttributes
注解的属性,找到后,如果这些属性不存在于 ModelAndViewContainer 容器中,就从 SessionAttributes 中获取并设置到 ModelAndViewContainer 容器中。
我们先来看看第一个 retrieveAttributes 方法:
1 2 3 4 5 6 7 8 9 10
| public Map<String, Object> retrieveAttributes(WebRequest request) { Map<String, Object> attributes = new HashMap<>(); for (String name : this.knownAttributeNames) { Object value = this.sessionAttributeStore.retrieveAttribute(request, name); if (value != null) { attributes.put(name, value); } } return attributes; }
|
这个其实没啥好说的,因为逻辑很清晰,knownAttributeNames 就是我们在使用 @SessionAttributes
注解时配置的属性名字,属性名字可以是一个数组。遍历 knownAttributeNames 属性,从 session 中获取相关数据存入 Map 集合中。
再来看第二个 invokeModelAttributeMethods 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| private void invokeModelAttributeMethods(NativeWebRequest request, ModelAndViewContainer container) throws Exception { while (!this.modelMethods.isEmpty()) { InvocableHandlerMethod modelMethod = getNextModelMethod(container).getHandlerMethod(); ModelAttribute ann = modelMethod.getMethodAnnotation(ModelAttribute.class); if (container.containsAttribute(ann.name())) { if (!ann.binding()) { container.setBindingDisabled(ann.name()); } continue; } Object returnValue = modelMethod.invokeForRequest(request, container); if (modelMethod.isVoid()) { if (StringUtils.hasText(ann.value())) { } continue; } String returnValueName = getNameForReturnValue(returnValue, modelMethod.getReturnType()); if (!ann.binding()) { container.setBindingDisabled(returnValueName); } if (!container.containsAttribute(returnValueName)) { container.addAttribute(returnValueName, returnValue); } } }
|
- 首先获取含有
@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 2 3 4 5 6 7 8 9 10 11 12
| public void updateModel(NativeWebRequest request, ModelAndViewContainer container) throws Exception { ModelMap defaultModel = container.getDefaultModel(); if (container.getSessionStatus().isComplete()){ this.sessionAttributesHandler.cleanupAttributes(request); } else { this.sessionAttributesHandler.storeAttributes(request, defaultModel); } if (!container.isRequestHandled() && container.getModel() == defaultModel) { updateBindingResult(request, defaultModel); } }
|
修改的时候会首先判断一下是否已经调用了 sessionStatus.setComplete();
方法,如果调用过了,就执行清除操作,否则就进行正常的更新操作即可,更新的数据就是 ModelAndViewContainer 中的 defaultModel。最后判断是否需要进行页面渲染,如果需要,再给参数分别设置 BindingResult 以备视图使用。
现在,大家应该已经清楚了 ModelFactory 的功能了。
一句话,ModelFactory 在初始化的时候,就直接从 SessionAttributes 以及 ModelAttribute 处加载到数据,放到 ModelAndViewContainer 中,更新的时候,则有可能清除 SessionAttributes 中的数据。这里大家需要把握一点,就是数据最终被存入 ModelAndViewContainer 中了。
3.相关的参数解析器
这是 Model 初始化的过程,初始化完成后,参数最终会在参数解析器中被解析,关于参数解析器,大家可以参考如下两篇文章:
这里涉及到的参数解析器就是 ModelMethodProcessor,我们来看下它里边两个关键的方法:
1 2 3 4 5 6 7 8 9 10
| @Override public boolean supportsParameter(MethodParameter parameter) { return Model.class.isAssignableFrom(parameter.getParameterType()); } @Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { return mavContainer.getModel(); }
|
可以看到,支持的参数类型就是 Model,参数的值则是直接返回 ModelAndViewContainer 中的 model 对象。
这里还有一个类似的参数处理器 MapMethodProcessor:
1 2 3 4 5 6 7 8 9 10 11
| @Override public boolean supportsParameter(MethodParameter parameter) { return (Map.class.isAssignableFrom(parameter.getParameterType()) && parameter.getParameterAnnotations().length == 0); } @Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { return mavContainer.getModel(); }
|
这个是处理 Map 类型的参数,最终返回的也是 ModelAndViewContainer 中的 model,你是否发现什么了?对了,在本文第二小节的案例中,你把 Model 参数换成 Map 或者 ModelMap(ModelMap 本质上也是 Map,使用的参数解析器也是 MapMethodProcessor),最终效果是一样的!
前面我们还使用了 @SessionAttribute
注解,这个注解的 name 属性就绑定了 SessionAttributes 中对应的属性并赋值给变量,它使用的参数解析器是 SessionAttributeMethodArgumentResolver,我们来看下它里边的核心方法:
1 2 3 4 5 6 7 8 9 10 11
| public class SessionAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(SessionAttribute.class); } @Override @Nullable protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) { return request.getAttribute(name, RequestAttributes.SCOPE_SESSION); } }
|
可以看到,这个参数最终对应的值就是从 session 中取出对应的 name 属性值。
最后,我们再来梳理一下整个过程:当请求到达后,首先要初始化 Model,初始化 Model 的时候,会根据 @SessionAttributes
注解从 session 中读取相关数据放入 ModelAndViewContainer 中,同时也会加载 @ModelAttribute
注解配置的全局数据到 ModelAndViewContainer 中。最终在参数解析器中,返回 ModelAndViewContainer 中的 model 即可。
现在大家可以回答文章标题提出的问题了吧!