从原理层面掌握@SessionAttribute的使用【一起学Spring MVC】
每篇一句
不是你当上了火影大家就认可你,而是大家都认可你才能当上火影
前言
该注解顾名思义,作用是将model
中的属性同步到session
会话当中,方便在下一次请求中使用(比如重定向场景~)。
虽然说session
的概念在当下前后端完全分离的场景中已经变得越来越弱化了,但是若为web开发者来说,我仍旧强烈不建议各位扔掉这个知识点,so我自然就建议大家能够熟练使用@sessionattribute
来简化平时的开发,本文带你入坑~
@sessionattribute
这个注解只能标注在类上,用于在多个请求之间传递参数,类似于session
的attribute
。
但不完全一样:一般来说@sessionattribute
设置的参数只用于暂时的传递,而不是长期的保存,长期保存的数据还是要放到session
中。(比如重定向之间暂时传值,用这个注解就很方便)
==官方解释==:当用@sessionattribute
标注的controller
向其模型model添加属性时,将根据该注解指定的名称/类型检查这些属性,若匹配上了就顺带也会放进session里。匹配上的将一直放在sesson
中,直到你调用了sessionstatus.setcomplete()
方法就消失了~~~
// @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
来清除,这个方法只是清除sessionattribute
里的参数,而不会应用于session
中的参数。也就是说使用api自己放进session内和使用@sessionattribute
注解放进去还是有些许差异的~
demo show
下面用一个比较简单的例子演示一下@sessionattribute
它的作用:
@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中,因为注解@sessionattribute中有,所以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 "sessionattribute"; } }
我们只需要访问入口请求/index
就可以直接看到控制台输出如下:
天龙八部;我乔峰是个契丹人;1000.0 天龙八部 api方式手动放进去的:fsx 天龙八部 null api方式手动放进去的:fsx
浏览器如下图:
初识的小伙伴可以认真的观察本例,它佐证了我上面说的理论知识。
@sessionattribute
注解设置的参数有3类方式去使用它:
- 在视图view中(比如jsp页面等)通过
request.getattribute()
或session.getattribute
获取 - 在后面请求返回的视图view中通过
session.getattribute
或者从model中获取(这个也比较常用) - 自动将参数设置到后面请求所对应处理器的
model
类型参数或者有@modelattribute
注释的参数里面(结合@modelattribute
一起使用应该是我们重点关注的)
通过示例知道了它的基本使用,下面从原理层面去分析它的执行过程,实现真正的掌握它。
sessionattributeshandler
见名之意,它是@sessionattributes
处理器,也就是解析这个注解的核心。管理通过@sessionattributes
标注了的特定会话属性,存储最终是委托了sessionattributestore
来实现。
// @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
对@sessionattribute
的处理操作入口,是在modelfactory.initmodel()
方法里会对@sessionattribute
的注解进行解析、处理,然后方法完成之后也会对它进行属性同步。
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里)
总结
这篇文章介绍了@sessionattribute
的核心处理原理,以及也给了一个demo
来介绍它的基本使用,不出意外阅读下来你对它应该是有很好的收获的,希望能帮助到你简化开发~
相关阅读
从原理层面掌握handlermethod、invocablehandlermethod、servletinvocablehandlermethod的使用【一起学spring mvc】
知识交流
==the last:如果觉得本文对你有帮助,不妨点个赞呗。当然分享到你的朋友圈让更多小伙伴看到也是被作者本人许可的~
==
若对技术内容感兴趣可以加入wx群交流:java高工、架构师3群
。
若群二维码失效,请加wx号:fsx641385712
(或者扫描下方wx二维码)。并且备注:"java入群"
字样,会手动邀请入群
若文章
格式混乱
或者图片裂开
,请点击`:
下一篇: 加油送礼