5

接口参数 Model 中的数据放在 session 中还是 request 中?

 3 years ago
source link: http://www.javaboy.org/2021/0330/sessionattribute.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在 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 注解设置的参数我们可以在三个地方获取:

  1. 在当前的视图中直接通过 request.getAttributesession.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 域中获取。

  1. 在后面的请求中,也可以通过 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 中的参数还能这么传递?涨姿势了!),现在你看到了,这也是一种方案。

  1. 在后续的请求中,也可以直接从 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 方法比较逻辑比较简单:

  1. 首先它会从 @SessionAttributes 中取出参数,然后合并进 ModelAndViewContainer 容器中(不懂 ModelAndViewContainer 容器的话,可以参考松哥前面的文章:Spring Boot 中如何统一 API 接口响应格式?)。
  2. 接下来调用含有 @ModelAttribute 注解的方法,并将结果合并进 ModelAndViewContainer 容器中。
  3. 寻找那些既有 @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);
}
}
}
  1. 首先获取含有 @ModelAttribute 注解的方法,然后获取到该注解。
  2. 获取 @ModelAttribute 注解,并提取出它的 name 属性值,然后查看 ModelAndViewContainer 容器中是否已经包含了该属性,如果已经包含了,并且在 @ModelAttribute 注解中设置了不绑定,则将该属性添加到 ModelAndViewContainer 容器中的禁止绑定上面去。
  3. 接下来通过 invokeForRequest 方法去调用含有 @ModelAttribute 注解的方法,并获取返回值。
  4. 如果含有 @ModelAttribute 注解的方法返回值为 void,则该方法到此为止。
  5. 接下来解析出返回值的参数名,有的小伙伴们说,参数名不就是 @ModelAttribute 注解中配置的 name 属性吗?这当然没错!但是有时候用户没有配置 name 属性,那么这个时候就会对应一套默认的 name 生成方案。默认的名字生成方案是这样的:
    • 如果返回对象前两个字母都是大写,那就原封不动返回,否则首字母小写后返回。
    • 如果返回类型是数组或者集合,则在真实类型后加上 List,例如 List 对象 longList。
  6. 有了 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 即可。


现在大家可以回答文章标题提出的问题了吧!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK