初期不習慣,久了也慢慢覺的好用,直到有一天,資料庫裡的資料暴走了,久思不得其解,最後 PM 點出,問題在使用者同時開啟多個頁籤編輯。
以前的 FormController 的作法是:
- GET request進來,用 uid 到資料庫取 model 資料,然後再準備一些表單需要用到的資料,再將這些資料全部放到request裡。
- 回到 JSP 後,從request裡取出資料顯示。
- 使用者 submit 後,POST request進來,再用 uid 到資料庫取 model 資料,然後進行 data binding 與 validation,最後存到資料庫。
Annotation 以後的作法是:
- GET request進來,用 uid 到資料庫取 model 資料,然後再準備一些表單需要用到的資料,再將這些資料全部放到request裡,另外將 model 資料放到session裡。
- 回到 JSP 後,從request裡取出資料顯示。
- 使用者 submit 後,POST request進來,直接到session裡取出 model 資料,然後進行 data binding 與 validation,最後存到資料庫。
因為同一個controller的@SessionAttributes只會用同一個名稱,也就是第一個頁籤取出約翰用user存到session,第二個頁籤取出比爾還是用user存到session。
在這個當下,session裡只有一個叫做比爾的user,約翰被蓋掉了
然後使用者編輯完約翰送出,噹噹,比爾變成約翰了。
目前官方沒有奉送解答,但有提到怎麼解,說穿了很簡單,就是每個 form 都用不同的@SessionAttributes名稱就好了。
怎麼做呢?這才是問題的開始。
首先,在org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter有一個sessionAttributeStore屬性可以客制。
不要以為<mvc:annotation-driven />是用org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter這個HandlerAdapter,我被唬弄了好久,才發現已經改用org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter。
目前官方沒有奉送解答,但有提到怎麼解,說穿了很簡單,就是每個 form 都用不同的@SessionAttributes名稱就好了。
怎麼做呢?這才是問題的開始。
首先,在org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter有一個sessionAttributeStore屬性可以客制。
不要以為<mvc:annotation-driven />是用org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter這個HandlerAdapter,我被唬弄了好久,才發現已經改用org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter。
sessionAttributeStore實作如下,重點只有getAttributeNameInSession(),但另外三個xxxAttribute()一定要有,是從DefaultSessionAttributeStore複製來的,如果沒有這三個xxxAttribute()的話,那神奇的Java機制會去呼叫DefaultSessionAttributeStore的getAttributeNameInSession(),還是回傳固定的名稱。
這裡的作法很簡單,就是在原本的名稱後面加上一個時間戳記,然後放到request裡,讓稍後的JSP可以放到form裡,等到submit時,又會回來sessionAttributeStore,然後就可以從session裡取回剛剛寄放獨一無二的約翰了。
public class ConversationSessionAttributeStore extends DefaultSessionAttributeStore { @Override 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, true); request.setAttribute(storeAttributeName, attributeValue, WebRequest.SCOPE_SESSION); } @Override 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, false); return request.getAttribute(storeAttributeName, WebRequest.SCOPE_SESSION); } @Override 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, false); request.removeAttribute(storeAttributeName, WebRequest.SCOPE_SESSION); } private String getAttributeNameInSession(WebRequest request, String attributeName, boolean isNew) { String a = super.getAttributeNameInSession(request, attributeName); if (isNew) { String c = String.valueOf(new Date().getTime()); request.setAttribute("conversationKey", c, RequestAttributes.SCOPE_REQUEST); a += "_" + c; } else { String c = request.getParameter("conversationKey"); if (StringUtils.isNotBlank(c)) { a += "_" + c; } } return a; } }但在這個年代,大家都是用<mvc:annotation-driven />一槍打掉所有的設定,怎麼客制RequestMappingHandlerAdapter呢?
很抱歉,<mvc:annotation-driven />裡有很多客制的屬性,就是沒有sessionAttributeStore。
那就先以我淺薄的知識蠻幹吧!
<bean id="conversationSessionAttributeStore" class="idv.neil.web.ConversationSessionAttributeStore" /> <bean name="handlerAdapter" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> <property name="sessionAttributeStore"> <ref bean="conversationSessionAttributeStore" /> </property> </bean> <mvc:annotation-driven />哇咧,居然就可以用,我出運了,收工關電腦下班閃人。
當然事情沒這麼簡單,過不久電話就響了,除了這功能好了,其他功能全掛了!
也沒這麼誇張了,但原本前台透過 Ajax 向後台撈資料的$.getJSON(),都死死去了。
測試第一步,當然先把<bean name="handlerAdapter" ... />拿掉看看,哇,好了,這下賽了,我的sessionAttributeStore又飛走了。
好吧,面對現實,<mvc:annotation-driven />到底做了什麼?
從http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd追到org.springframework.web.servlet.config.annotation.EnableWebMvc,再追到org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport,終於找到requestMappingHandlerAdapter()。
@Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { ConfigurableWebBindingInitializer webBindingInitializer = new ConfigurableWebBindingInitializer(); webBindingInitializer.setConversionService(mvcConversionService()); webBindingInitializer.setValidator(mvcValidator()); List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<HandlerMethodArgumentResolver>(); addArgumentResolvers(argumentResolvers); List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<HandlerMethodReturnValueHandler>(); addReturnValueHandlers(returnValueHandlers); RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter(); adapter.setMessageConverters(getMessageConverters()); adapter.setWebBindingInitializer(webBindingInitializer); adapter.setCustomArgumentResolvers(argumentResolvers); adapter.setCustomReturnValueHandlers(returnValueHandlers); return adapter; }這裡就是<mvc:annotation-driven />預設出產HandlerAdapter的地方,逐行翻成XML之後,得到以下的結果。
<bean name="handlerAdapter" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> <property name="sessionAttributeStore"> <ref bean="conversationSessionAttributeStore" /> </property> <property name="webBindingInitializer"> <bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer"> <property name="conversionService"> <bean class="org.springframework.format.support.DefaultFormattingConversionService" /> </property> <property name="validator"> <bean class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean" /> </property> </bean> </property> <property name="customArgumentResolvers"> <list></list> </property> <property name="customReturnValueHandlers"> <list></list> </property> <property name="messageConverters"> <list> <bean class="org.springframework.http.converter.ByteArrayHttpMessageConverter" /> <bean class="org.springframework.http.converter.StringHttpMessageConverter"> <property name="writeAcceptCharset" value="false" /> </bean> <bean class="org.springframework.http.converter.ResourceHttpMessageConverter" /> <bean class="org.springframework.http.converter.xml.SourceHttpMessageConverter" /> <bean class="org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter" /> <bean class="org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter" /> <bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" /> <!-- <bean class="org.springframework.http.converter.feed.AtomFeedHttpMessageConverter" /> <bean class="org.springframework.http.converter.feed.RssChannelHttpMessageConverter" /> --> </list> </property> </bean> <mvc:annotation-driven />這下真的可以下班了。
應該吧!
---
---
---
沒有留言:
張貼留言