SpringMVC @SessionAttributes 使用詳解以及源碼分析
@sessionattributes
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SessionAttributes {
String[] value() default {};
Class[] types() default {};
}
@sessionattributes注解應用到Controller上面,可以將Model中的屬性同步到session當中。
先看一個最基本的方法
@Controller
@RequestMapping("/Demo.do")
@SessionAttributes(value={"attr1","attr2"})
public class Demo {
@RequestMapping(params="method=index")
public ModelAndView index() {
ModelAndView mav = new ModelAndView("index.jsp");
mav.addObject("attr1", "attr1Value");
mav.addObject("attr2", "attr2Value");
return mav;
}
@RequestMapping(params="method=index2")
public ModelAndView index2(@ModelAttribute("attr1")String attr1, @ModelAttribute("attr2")String attr2) {
ModelAndView mav = new ModelAndView("success.jsp");
return mav;
}
}
index方法返回一個ModelAndView 其中包括視圖index.jsp 和 兩個鍵值放入model當中,在沒有加入@sessionattributes注解的時候,放入model當中的鍵值是request級別的。
現在因為在Controller上面標記了@SessionAttributes(value={"attr1","attr2"}) 那么model中的attr1,attr2會同步到session中,這樣當你訪問index 然后在去訪問index2的時候也會獲取這倆個屬性的值。
當需要清除session當中的值得時候,我們只需要在controller的方法中傳入一個SessionStatus的類型對象 通過調用setComplete方法就可以清除了。
@RequestMapping(params="method=index3")
public ModelAndView index4(SessionStatus status) {
ModelAndView mav = new ModelAndView("success.jsp");
status.setComplete();
return mav;
}
下面就直接分析代碼來看Spring是如可封裝的。
首先我們需要看2個類 DefaultSessionAttributeStore和SessionAttributesHandler
DefaultSessionAttributeStore這個類是用來往WebRequest存取數據的工具類,WebRequest是Spring包裝的HttpServletRequest,大家理解為普通的HttpServletRequest就行了。
public class DefaultSessionAttributeStore implements SessionAttributeStore {
private String attributeNamePrefix = "";
public void setAttributeNamePrefix(String attributeNamePrefix) {
this.attributeNamePrefix = (attributeNamePrefix != null ? attributeNamePrefix : "");
}
public void storeAttribute(WebRequest request, String attributeName, Object attributeValue) {
Assert.notNull(request, "WebRequest must not be null");
Assert.notNull(attributeName, "Attribute name must not be null");
Assert.notNull(attributeValue, "Attribute value must not be null");
String storeAttributeName = getAttributeNameInSession(request, attributeName);
request.setAttribute(storeAttributeName, attributeValue, WebRequest.SCOPE_SESSION);
}
public Object retrieveAttribute(WebRequest request, String attributeName) {
Assert.notNull(request, "WebRequest must not be null");
Assert.notNull(attributeName, "Attribute name must not be null");
String storeAttributeName = getAttributeNameInSession(request, attributeName);
return request.getAttribute(storeAttributeName, WebRequest.SCOPE_SESSION);
}
public void cleanupAttribute(WebRequest request, String attributeName) {
Assert.notNull(request, "WebRequest must not be null");
Assert.notNull(attributeName, "Attribute name must not be null");
String storeAttributeName = getAttributeNameInSession(request, attributeName);
request.removeAttribute(storeAttributeName, WebRequest.SCOPE_SESSION);
}
protected String getAttributeNameInSession(WebRequest request, String attributeName) {
return this.attributeNamePrefix + attributeName;
}
}
Spring會為每一個Controller初始化一個SessionAttributesHandler實例,用來記錄@SessionAttributes(value={"attr1","attr2"})里面的屬性信息,當需要同步model的值時,會先判斷是否在SessionAttributes當中定義。
public class SessionAttributesHandler {
private final Set<String> attributeNames = new HashSet<String>();
private final Set<Class<?>> attributeTypes = new HashSet<Class<?>>();
private final Set<String> knownAttributeNames = Collections.synchronizedSet(new HashSet<String>(4));
private final SessionAttributeStore sessionAttributeStore;
public SessionAttributesHandler(Class<?> handlerType, SessionAttributeStore sessionAttributeStore) {
Assert.notNull(sessionAttributeStore, "SessionAttributeStore may not be null.");
this.sessionAttributeStore = sessionAttributeStore;
SessionAttributes annotation = AnnotationUtils.findAnnotation(handlerType, SessionAttributes.class);
if (annotation != null) {
this.attributeNames.addAll(Arrays.asList(annotation.value()));
this.attributeTypes.addAll(Arrays.<Class<?>>asList(annotation.types()));
}
this.knownAttributeNames.addAll(this.attributeNames);
}
public boolean hasSessionAttributes() {
return ((this.attributeNames.size() > 0) || (this.attributeTypes.size() > 0));
}
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;
}
}
public void storeAttributes(WebRequest request, Map<String, ?> attributes) {
for (String name : attributes.keySet()) {
Object value = attributes.get(name);
Class<?> attrType = (value != null) ? value.getClass() : null;
if (isHandlerSessionAttribute(name, attrType)) {
this.sessionAttributeStore.storeAttribute(request, name, value);
}
}
}
public Map<String, Object> retrieveAttributes(WebRequest request) {
Map<String, Object> attributes = new HashMap<String, Object>();
for (String name : this.knownAttributeNames) {
Object value = this.sessionAttributeStore.retrieveAttribute(request, name);
if (value != null) {
attributes.put(name, value);
}
}
return attributes;
}
public void cleanupAttributes(WebRequest request) {
for (String attributeName : this.knownAttributeNames) {
this.sessionAttributeStore.cleanupAttribute(request, attributeName);
}
}
Object retrieveAttribute(WebRequest request, String attributeName) {
return this.sessionAttributeStore.retrieveAttribute(request, attributeName);
}
}
當我們訪問controller中的一個方法時,會調用RequestMappingHandlerAdapter類當中的invokeHandleMethod的方法。
View Code
我們看這行代碼
modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
跟進去看源碼
View Code
其中這兩句是關鍵的地方,把session的值取出來全部放入到ModelAndViewContainer當中去,這樣我就可以再controller當中的方法中獲取了,當然直接從session中拿也是可以的。
Map<String, ?> attributesInSession = this.sessionAttributesHandler.retrieveAttributes(request);
mavContainer.mergeAttributes(attributesInSession);
在controller中獲取sessionAttributes的只有兩種方式。
一、public ModelAndView index2(@ModelAttribute("attr1")String attr1, @ModelAttribute("attr2")String attr2)
二、public ModelAndView index2(ModelMap map)
這倆種方式的區別是,如果使用@ModelAttribute屬性獲取值,並且@SessionAttributes注解當中還設置了該屬性,當屬性為null時會跑出異常,因為這幾行代碼。
for (String name : findSessionAttributeArguments(handlerMethod)) {
if (!mavContainer.containsAttribute(name)) {
Object value = this.sessionAttributesHandler.retrieveAttribute(request, name);
if (value == null) {
throw new HttpSessionRequiredException("Expected session attribute '" + name + "'");
}
mavContainer.addAttribute(name, value);
}
}
以上分析的都是取值的過程,那么Spring是如何將Model中的數據同步到session當中的呢。
我們看上面invokeHandleMethod方法中最后一句話 return getModelAndView(mavContainer, modelFactory, webRequest); 我們跟進去看源碼。
View Code
有一行代碼是modelFactory.updateModel(webRequest, mavContainer); 我們在跟進去看源碼。
我們看到首先判斷一下ModelAndViewContainer中的SessionStatus是否是完成狀態,如果是TRUE就清除session中的值,反之將ModelAndViewContainer中的model值設置進去。
View Code
看到這里可能還有一個疑問,就是我們的數據時設置到ModelAndView里面的,那么這些數據時如何跑到ModelAndViewContainer當中去的呢,其實是在方法執行完成之后Spring幫我們做的,
具體細節查看 ModelAndViewMethodReturnValueHandler方法中的最后一行代碼mavContainer.addAllAttributes(mav.getModel());
View Code

