初期不習慣,久了也慢慢覺的好用,直到有一天,資料庫裡的資料暴走了,久思不得其解,最後 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 />
這下真的可以下班了。應該吧!
---
---
---
沒有留言:
張貼留言