從原理層面掌握@SessionAttribute的使用【一起學Spring MVC】


每篇一句

不是你當上了火影大家就認可你,而是大家都認可你才能當上火影

前言

該注解顧名思義,作用是將Model中的屬性同步到session會話當中,方便在下一次請求中使用(比如重定向場景~)。
雖然說Session的概念在當下前后端完全分離的場景中已經變得越來越弱化了,但是若為web開發者來說,我仍舊強烈不建議各位扔掉這個知識點,so我自然就建議大家能夠熟練使用@SessionAttribute來簡化平時的開發,本文帶你入坑~

@SessionAttribute

這個注解只能標注在類上,用於在多個請求之間傳遞參數,類似於SessionAttribute
但不完全一樣:一般來說@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類方式去使用它:

  1. 在視圖view中(比如jsp頁面等)通過request.getAttribute()session.getAttribute獲取
  2. 在后面請求返回的視圖view中通過session.getAttribute或者從model中獲取(這個也比較常用)
  3. 自動將參數設置到后面請求所對應處理器的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中需要同時滿足兩個條件:

  1. @SessionAttribute注解中設置了參數的名字或者類型
  2. 在處理器(Controller)中將參數設置到了Model中(這樣方法結束后會自動的同步到SessionAttr里)

總結

這篇文章介紹了@SessionAttribute的核心處理原理,以及也給了一個Demo來介紹它的基本使用,不出意外閱讀下來你對它應該是有很好的收獲的,希望能幫助到你簡化開發~

相關閱讀

從原理層面掌握HandlerMethod、InvocableHandlerMethod、ServletInvocableHandlerMethod的使用【一起學Spring MVC】

知識交流

The last:如果覺得本文對你有幫助,不妨點個贊唄。當然分享到你的朋友圈讓更多小伙伴看到也是被作者本人許可的~

若對技術內容感興趣可以加入wx群交流:Java高工、架構師3群
若群二維碼失效,請加wx號:fsx641385712(或者掃描下方wx二維碼)。並且備注:"java入群" 字樣,會手動邀請入群

若文章格式混亂或者圖片裂開,請點擊`:原文鏈接-原文鏈接-原文鏈接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM