從原理層面掌握@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入群"
字樣,會手動邀請入群
若文章
格式混亂
或者圖片裂開
,請點選`:原文連結-原文連結-原文連結