接口参数 Model 中的数据放在 session 中还是 request 中?
在 SpringMVC 中,大家都知道有一个特殊的参数 Model,它的使用方式像下面这样:
@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
获取。
例如如下接口:
@Controller
@SessionAttributes("name")
public class HelloController {
@GetMapping("/01")
public String hello(Model model) {
model.addAttribute("name", "javaboy");
return "01";
}
}
name 属性会被临时保存在 session 中,在前端页面中,我们既可以从 request 域中获取也可以从 session 域中获取,以 Thymeleaf 页面模版为例:
<!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 中,那就有一个好处,就是无论是服务器端跳转还是客户端跳转,参数都不会丢失。例如如下接口:
@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 中获取。
@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 中的数据。
第三种方式还有一个变体,如下:
@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 中的参数。
例如下面这样:
@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 中。两方面的功能我们分别来看,先来看初始化问题。
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 方法:
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 方法:
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 的过程:
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,我们来看下它里边两个关键的方法:
@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:
@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,我们来看下它里边的核心方法:
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 即可。