前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从原理层面掌握@SessionAttributes的使用【享学Spring MVC】

从原理层面掌握@SessionAttributes的使用【享学Spring MVC】

作者头像
YourBatman
发布2019-09-03 16:26:48
2K0
发布2019-09-03 16:26:48
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦
前言

该注解顾名思义,作用是将Model中的属性同步到session会话当中,方便在下一次请求中使用(比如重定向场景~)。

虽然说Session的概念在当下前后端完全分离的场景中已经变得越来越弱化了,但是若为web开发者来说,我仍旧强烈不建议各位扔掉这个知识点,so我自然就建议大家能够熟练使用@SessionAttributes来简化平时的开发,本文带你入坑~

@SessionAttributes

我把这行字放在最前面:本文讲解的是org.springframework.web.bind.annotation.SessionAttributes而非org.springframework.web.bind.annotation.SessionAttribute,它两可完全不是一个概念哦~ 关于@SessionAttribute的使用在这里

这个注解只能标注在类上,用于在多个请求之间传递参数,类似于SessionAttribute

但不完全一样:一般来说@SessionAttributes设置的参数只用于暂时的传递,而不是长期的保存,长期保存的数据还是要放到Session中。(比如重定向之间暂时传值,用这个注解就很方便)

官方解释:当用@SessionAttributes标注的Controller向其模型Model添加属性时,将根据该注解指定的名称/类型检查这些属性,若匹配上了就顺带也会放进Session里。匹配上的将一直放在Sesson中,直到你调用了SessionStatus.setComplete()方法就消失了~~~

代码语言:javascript
复制
// @since 2.5   它只能标注在类上
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SessionAttributes {

	// 只有名称匹配上了的  Model上的属性会向session里放置一份~~~
	@AliasFor("names")
	String[] value() default {};
	@AliasFor("value")
	String[] names() default {};

	// 也可以拿类型来约束
	Class<?>[] types() default {};
}

注意理解这句话:用户可以调用SessionStatus.setComplete来清除,这个方法只是清除**@SessionAttributes**里的参数,而不会应用于**Session**中的参数。也就是说使用API自己放进Session内和使用@SessionAttributes注解放进去还是有些许差异的~

Demo Show

下面用一个比较简单的例子演示一下@SessionAttributes它的作用:

代码语言:javascript
复制
@Controller
@RequestMapping("/sessionattr/demo")
@SessionAttributes(value = {"book", "description"}, types = {Double.class})
public class RedirectController {

    @RequestMapping("/index")
    public String index(Model model, HttpSession httpSession) {
        model.addAttribute("book", "天龙八部");
        model.addAttribute("description", "我乔峰是个契丹人");
        model.addAttribute("price", new Double("1000.00"));

        // 通过Sesson API手动放一个进去
        httpSession.setAttribute("hero", "fsx");

        //跳转之前将数据保存到Model中,因为注解@SessionAttributes中有,所以book和description应该都会保存到SessionAttributes里(注意:不是session里)
        return "redirect:get";
    }

    // 关于@ModelAttribute 下文会讲
    @RequestMapping("/get")
    public String get(@ModelAttribute("book") String book, ModelMap model, HttpSession httpSession, SessionStatus sessionStatus) {
        //可以从model中获得book、description和price的参数
        System.out.println(model.get("book") + ";" + model.get("description") + ";" + model.get("price"));

        // 从sesson中也能拿到值
        System.out.println(httpSession.getAttribute("book"));
        System.out.println("API方式手动放进去的:" + httpSession.getAttribute("hero"));
        // 使用@ModelAttribute也能拿到值
        System.out.println(book);

        // 手动清除SessionAttributes
        sessionStatus.setComplete();
        return "redirect:complete";
    }

    @RequestMapping("/complete")
    @ResponseBody
    public String complete(ModelMap modelMap, HttpSession httpSession) {
        //已经被清除,无法获取book的值
        System.out.println(modelMap.get("book"));
        System.out.println("API方式手动放进去的:" + httpSession.getAttribute("hero"));
        return "sessionAttributes";
    }

}

我们只需要访问入口请求/index就可以直接看到控制台输出如下:

代码语言:javascript
复制
天龙八部;我乔峰是个契丹人;1000.0
天龙八部
API方式手动放进去的:fsx
天龙八部
null
API方式手动放进去的:fsx

浏览器如下图:

初识的小伙伴可以认真的观察本例,它佐证了我上面说的理论知识。

@SessionAttributes注解设置的参数有3类方式去使用它:

  1. 在视图view中(比如jsp页面等)通过request.getAttribute()session.getAttribute获取
  2. 在后面请求返回的视图view中通过session.getAttribute或者从model中获取(这个也比较常用)
  3. 自动将参数设置到后面请求所对应处理器的Model类型参数或者有@ModelAttribute注释的参数里面(结合@ModelAttribute一起使用应该是我们重点关注的)

通过示例知道了它的基本使用,下面从原理层面去分析它的执行过程,实现真正的掌握它。

SessionAttributesHandler

见名之意,它是@SessionAttributes处理器,也就是解析这个注解的核心。管理通过@SessionAttributes标注了的特定会话属性,存储最终是委托了SessionAttributeStore来实现。

代码语言:javascript
复制
// @since 3.1
public class SessionAttributesHandler {

	private final Set<String> attributeNames = new HashSet<>();
	private final Set<Class<?>> attributeTypes = new HashSet<>();

	// 注意这个重要性:它是注解方式放入session和API方式放入session的关键(它只会记录注解方式放进去的session属性~~)
	private final Set<String> knownAttributeNames = Collections.newSetFromMap(new ConcurrentHashMap<>(4));
	// sessonAttr存储器:它最终存储到的是WebRequest的session域里面去(对httpSession是进行了包装的)
	// 因为有WebRequest的处理,所以达到我们上面看到的效果。complete只会清楚注解放进去的,并不清除API放进去的~~~
	// 它的唯一实现类DefaultSessionAttributeStore实现也简单。(特点:能够制定特殊的前缀,这个有时候还是有用的)
	// 前缀attributeNamePrefix在构造器里传入进来  默认是“”
	private final SessionAttributeStore sessionAttributeStore;

	// 唯一的构造器 handlerType:控制器类型  SessionAttributeStore 是由调用者上层传进来的
	public SessionAttributesHandler(Class<?> handlerType, SessionAttributeStore sessionAttributeStore) {
		Assert.notNull(sessionAttributeStore, "SessionAttributeStore may not be null");
		this.sessionAttributeStore = sessionAttributeStore;

		// 父类上、接口上、注解上的注解标注了这个注解都算
		SessionAttributes ann = AnnotatedElementUtils.findMergedAnnotation(handlerType, SessionAttributes.class);
		if (ann != null) {
			Collections.addAll(this.attributeNames, ann.names());
			Collections.addAll(this.attributeTypes, ann.types());
		}
		this.knownAttributeNames.addAll(this.attributeNames);
	}

	// 既没有指定Name 也没有指定type  这个注解标上了也没啥用
	public boolean hasSessionAttributes() {
		return (!this.attributeNames.isEmpty() || !this.attributeTypes.isEmpty());
	}

	// 看看指定的attributeName或者type是否在包含里面
	// 请注意:name和type都是或者的关系,只要有一个符合条件就成
	public boolean isHandlerSessionAttribute(String attributeName, Class<?> attributeType) {
		Assert.notNull(attributeName, "Attribute name must not be null");
		if (this.attributeNames.contains(attributeName) || this.attributeTypes.contains(attributeType)) {
			this.knownAttributeNames.add(attributeName);
			return true;
		} else {
			return false;
		}
	}

	// 把attributes属性们存储起来  进到WebRequest 里
	public void storeAttributes(WebRequest request, Map<String, ?> attributes) {
		attributes.forEach((name, value) -> {
			if (value != null && isHandlerSessionAttribute(name, value.getClass())) {
				this.sessionAttributeStore.storeAttribute(request, name, value);
			}
		});
	}

	// 检索所有的属性们  用的是knownAttributeNames哦~~~~
	// 也就是说手动API放进Session的 此处不会被检索出来的
	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
	public void cleanupAttributes(WebRequest request) {
		for (String attributeName : this.knownAttributeNames) {
			this.sessionAttributeStore.cleanupAttribute(request, attributeName);
		}
	}


	// 对底层sessionAttributeStore的一个传递调用~~~~~
	// 毕竟可以拼比一下sessionAttributeStore的实现~~~~
	@Nullable
	Object retrieveAttribute(WebRequest request, String attributeName) {
		return this.sessionAttributeStore.retrieveAttribute(request, attributeName);
	}
}

这个类是对SessionAttribute这些属性的核心处理能力:包括了所谓的增删改查。因为要进一步理解到它的原理,所以要说到它的处理入口,那就要来到ModelFactory了~

ModelFactory

Spring MVC@SessionAttributes的处理操作入口,是在ModelFactory.initModel()方法里会对@SessionAttributes的注解进行解析、处理,然后方法完成之后也会对它进行属性同步。

ModelFactory**是用来维护Model的,具体包含两个功能:**

  • 处理器执行前,初始化Model
  • 处理器执行后,将Model中相应的参数同步更新到SessionAttributes中(不是全量,而是符合条件的那些)
代码语言:javascript
复制
// @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**中需要同时满足两个条件:

  1. @SessionAttribute注解中设置了参数的名字或者类型
  2. 在处理器(Controller)中将参数设置到了Model中(这样方法结束后会自动的同步到SessionAttr里)
总结

@SessionAttributes指的是Spring MVCSession。向其中添加值得时候,同时会向 HttpSession中添加一条。在sessionStatus.setComplete();的时候,会清空Spring MVC

Session,同时清除对应键的HttpSession内容,但是通过,**request.getSession.setAttribute()**方式添加的内容不会被清除掉。其他情况下,Spring MVCSessionHttpSession使用情况相同。

这篇文章介绍了@SessionAttributes的核心处理原理,以及也给了一个Demo来介绍它的基本使用,不出意外阅读下来你对它应该是有很好的收获的,希望能帮助到你简化开发~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年08月04日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • @SessionAttributes
    • Demo Show
      • SessionAttributesHandler
        • ModelFactory
          • 总结
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档