在 SpringMVC 中,大家都知道有一个特殊的参数 Model,它的使用方式像下面这样:

@Controller
public class HelloController {
    @GetMapping("/01")
    public String hello(Model model) {
        model.addAttribute("name", "javaboy");
        return "01";
    }
}

这样一个看起来人畜无害的普通参数,里边也会包含你的知识盲区吗?说不定真的包含了,不信你就往下看。

# 1.基本用法

仅仅从使用上来说,Model 有两方面的功能:

  1. 携带参数
  2. 返回参数

先说携带参数:当我们在一个接口中放上 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 即可。


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