ModelFactory
Spring MVC对@SessionAttributes的处理操作入口,是在ModelFactory.initModel()方法里会对@SessionAttributes的注解进行解析、处理,然后方法完成之后也会对它进行属性同步。
ModelFactory是用来维护Model的,具体包含两个功能:
- 处理器执行前,初始化Model
- 处理器执行后,将Model中相应的参数同步更新到SessionAttributes中(不是全量,而是符合条件的那些)
// @since 3.1 public final class ModelFactory { // ModelMethod它是一个私有内部类,持有InvocableHandlerMethod的引用 和方法的dependencies依赖们 private final List<ModelMethod> modelMethods = new ArrayList<>(); private final WebDataBinderFactory dataBinderFactory; private final SessionAttributesHandler sessionAttributesHandler; public ModelFactory(@Nullable List<InvocableHandlerMethod> handlerMethods, WebDataBinderFactory binderFactory, SessionAttributesHandler attributeHandler) { // 把InvocableHandlerMethod转为内部类ModelMethod if (handlerMethods != null) { for (InvocableHandlerMethod handlerMethod : handlerMethods) { this.modelMethods.add(new ModelMethod(handlerMethod)); } } this.dataBinderFactory = binderFactory; this.sessionAttributesHandler = attributeHandler; } // 该方法完成Model的初始化 public void initModel(NativeWebRequest request, ModelAndViewContainer container, HandlerMethod handlerMethod) throws Exception { // 先拿到sessionAttr里所有的属性们(首次进来肯定木有,但同一个session第二次进来就有了) Map<String, ?> sessionAttributes = this.sessionAttributesHandler.retrieveAttributes(request); // 和当前请求中 已经有的model合并属性信息 // 注意:sessionAttributes中只有当前model不存在的属性,它才会放进去 container.mergeAttributes(sessionAttributes); // 此方法重要:调用模型属性方法来填充模型 这里ModelAttribute会生效 // 关于@ModelAttribute的内容 我放到了这里:https://blog.csdn.net/f641385712/article/details/98260361 // 总之:完成这步之后 Model就有值了~~~~ invokeModelAttributeMethods(request, container); // 最后,最后,最后还做了这么一步操作~~~ // findSessionAttributeArguments的作用:把@ModelAttribute的入参也列入SessionAttributes(非常重要) 详细见下文 // 这里一定要掌握:因为使用中的坑坑经常是因为没有理解到这块逻辑 for (String name : findSessionAttributeArguments(handlerMethod)) { // 若ModelAndViewContainer不包含此name的属性 才会进来继续处理 这一点也要注意 if (!container.containsAttribute(name)) { // 去请求域里检索为name的属性,若请求域里没有(也就是sessionAttr里没有),此处会抛出异常的~~~~ Object value = this.sessionAttributesHandler.retrieveAttribute(request, name); if (value == null) { throw new HttpSessionRequiredException("Expected session attribute '" + name + "'", name); } // 把从sessionAttr里检索到的属性也向容器Model内放置一份~ container.addAttribute(name, value); } } } // 把@ModelAttribute标注的入参也列入SessionAttributes 放进sesson里(非常重要) // 这个动作是很多开发者都忽略了的 private List<String> findSessionAttributeArguments(HandlerMethod handlerMethod) { List<String> result = new ArrayList<>(); // 遍历所有的方法参数 for (MethodParameter parameter : handlerMethod.getMethodParameters()) { // 只有参数里标注了@ModelAttribute的才会进入继续解析~~~ if (parameter.hasParameterAnnotation(ModelAttribute.class)) { // 关于getNameForParameter拿到modelKey的方法,这个策略是需要知晓的 String name = getNameForParameter(parameter); Class<?> paramType = parameter.getParameterType(); // 判断isHandlerSessionAttribute为true的 才会把此name合法的添加进来 // (也就是符合@SessionAttribute标注的key或者type的) if (this.sessionAttributesHandler.isHandlerSessionAttribute(name, paramType)) { result.add(name); } } } return result; } // 静态方法:决定了parameter的名字 它是public的,因为ModelAttributeMethodProcessor里也有使用 // 请注意:这里不是MethodParameter.getParameterName()获取到的形参名字,而是有自己的一套规则的 // @ModelAttribute指定了value值就以它为准,否则就是类名的首字母小写(当然不同类型不一样,下面有给范例) public static String getNameForParameter(MethodParameter parameter) { ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class); String name = (ann != null ? ann.value() : null); return (StringUtils.hasText(name) ? name : Conventions.getVariableNameForParameter(parameter)); } // 关于方法这块的处理逻辑,和上差不多,主要是返回类型和实际类型的区分 // 比如List<String>它对应的名是:stringList。即使你的返回类型是Object~~~ public static String getNameForReturnValue(@Nullable Object returnValue, MethodParameter returnType) { ModelAttribute ann = returnType.getMethodAnnotation(ModelAttribute.class); if (ann != null && StringUtils.hasText(ann.value())) { return ann.value(); } else { Method method = returnType.getMethod(); Assert.state(method != null, "No handler method"); Class<?> containingClass = returnType.getContainingClass(); Class<?> resolvedType = GenericTypeResolver.resolveReturnType(method, containingClass); return Conventions.getVariableNameForReturnType(method, resolvedType, returnValue); } } // 将列为@SessionAttributes的模型数据,提升到sessionAttr里 public void updateModel(NativeWebRequest request, ModelAndViewContainer container) throws Exception { ModelMap defaultModel = container.getDefaultModel(); if (container.getSessionStatus().isComplete()){ this.sessionAttributesHandler.cleanupAttributes(request); } else { // 存储到sessionAttr里 this.sessionAttributesHandler.storeAttributes(request, defaultModel); } // 若该request还没有被处理 并且 Model就是默认defaultModel if (!container.isRequestHandled() && container.getModel() == defaultModel) { updateBindingResult(request, defaultModel); } } // 将bindingResult属性添加到需要该属性的模型中。 // isBindingCandidate:给定属性在Model模型中是否需要bindingResult。 private void updateBindingResult(NativeWebRequest request, ModelMap model) throws Exception { List<String> keyNames = new ArrayList<>(model.keySet()); for (String name : keyNames) { Object value = model.get(name); if (value != null && isBindingCandidate(name, value)) { String bindingResultKey = BindingResult.MODEL_KEY_PREFIX + name; if (!model.containsAttribute(bindingResultKey)) { WebDataBinder dataBinder = this.dataBinderFactory.createBinder(request, value, name); model.put(bindingResultKey, dataBinder.getBindingResult()); } } } } // 看看这个静态内部类ModelMethod private static class ModelMethod { // 持有可调用的InvocableHandlerMethod 这个方法 private final InvocableHandlerMethod handlerMethod; // 这字段是搜集该方法标注了@ModelAttribute注解的入参们 private final Set<String> dependencies = new HashSet<>(); public ModelMethod(InvocableHandlerMethod handlerMethod) { this.handlerMethod = handlerMethod; // 把方法入参中所有标注了@ModelAttribute了的Name都搜集进来 for (MethodParameter parameter : handlerMethod.getMethodParameters()) { if (parameter.hasParameterAnnotation(ModelAttribute.class)) { this.dependencies.add(getNameForParameter(parameter)); } } } ... } }
ModelFactory协助在控制器方法调用之前初始化Model模型,并在调用之后对其进行更新。
- 初始化时,通过调用方法上标注有@ModelAttribute的方法,使用临时存储在会话中的属性填充模型。
- 在更新时,模型属性与会话同步,如果缺少,还将添加BindingResult属性。
关于默认名称规则的核心在Conventions.getVariableNameForParameter(parameter)这个方法里,我在上文给了一个范例,介绍常见的各个类型的输出值,大家记忆一下便可。参考:从原理层面掌握HandlerMethod、InvocableHandlerMethod、ServletInvocableHandlerMethod的使用【享学Spring MVC】
将一个参数设置到@SessionAttribute中需要同时满足两个条件:
在@SessionAttribute注解中设置了参数的名字或者类型
在处理器(Controller)中将参数设置到了Model中(这样方法结束后会自动的同步到SessionAttr里)
总结
@SessionAttributes指的是Spring MVC的Session。向其中添加值得时候,同时会向 HttpSession中添加一条。在sessionStatus.setComplete();的时候,会清空Spring MVC
的Session,同时清除对应键的HttpSession内容,但是通过,request.getSession.setAttribute()方式添加的内容不会被清除掉。其他情况下,Spring MVC的Session和HttpSession使用情况相同。
这篇文章介绍了@SessionAttributes的核心处理原理,以及也给了一个Demo来介绍它的基本使用,不出意外阅读下来你对它应该是有很好的收获的,希望能帮助到你简化开发~