題記:昨天加班打車回家,看見前面有輛路虎在高速上開的巨慢,擋住了我坐的出租車的路,因而就跟司機吐槽了一句:「前面這車怎麼這麼面啊?」,司機沉默了大概3秒,說了一句富含哲理性的話:「沒有面車,只有麪人」。借用這句話套在軟件開發上就是:「沒有面代碼,只有面的程序猿」。只不過此次我就是那個面的程序猿。(面:是一個方言,大意就是優柔寡斷,反應遲緩,沒有主見,好欺負之類的)。html
背景是這樣的,最近項目要作國際化,主要是基於spring的i18來作,經過攔截器攔截request過來的url中是否包含locale參數,若是locale參數指定了語言類型,則頁面上顯示的信息按指定的類型來獲取,如content這個字段,若是locale=zh_CN,則content=「你好」,若是locale=en_US,則content=「hello」;返回頁面的時候,一樣根據locale,在返回頁面的路徑前加上具體目錄,如返回sayHi.html,locale=zh_CN時返回cn/sayHi.html,locale=en_US時返回us/sayHi.html,這樣能夠顯示不一樣內容的歡迎頁面,通常國際化也是這樣作的,同一套代碼,不一樣配置就能夠完成國際化。web
因而開始的時候個人攔截器代碼時這樣寫的:spring
public class LocaleHandleInterceptor extends LocaleChangeInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException { String newLocale = request.getParameter(this.getParamName()); if (newLocale != null) { LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request); if (localeResolver == null) { throw new IllegalStateException("No LocaleResolver found,may be not config localeResolver bean!"); } Locale locale = StringUtils.parseLocaleString(newLocale); localeResolver.setLocale(request, response, locale); request.getSession().setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, locale); } else { Object localObject = request.getSession().getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME); if (null == localObject) { Locale locale = RequestContextUtils.getLocale(request); locale = (null == locale) ? Locale.getDefault() : locale; request.getSession().setAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME, locale); } } // Proceed in any case. return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { if (null != modelAndView) { // Locale locale = // (Locale)request.getSession().getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME); Locale locale = (Locale) WebUtils.getSessionAttribute(request, SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME); modelAndView.setViewName(locale.toString() + "/" + modelAndView.getViewName()); } } }
寫完後跑了幾個例子,換了一個locale參數設置都沒有問題,這件事情就算完成了。json
過了一段時間後進行上線前的壓力測試,用loadrunner 500用戶併發壓測tomcat(jvm Xmx=1024M)的時候發現曲線比較奇怪,剛開始表現正常,但大約30秒後tps急劇降低,一分鐘後tps趨於0,可是tomcat並無報OOM內存溢出,用瀏覽器訪問某個url仍是能有返回,用gcutil查看內存回收狀況,發現每秒都在作full gc,因而用jmap把當前tomcat的堆棧打出來,看到不少concurrentHashMap對象,對象裏有不少locale對象,聯想到這塊代碼最近只加過國際化的攔截器,因而懷疑到是否是session致使的,一開始沒有改代碼,而是在tomcat的conf下找到web.xml,調整了session-timeout 爲1分鐘,從新壓測問題仍是復現,從新審視代碼,發現是這段代碼引發的問題,把它註釋掉就正常:瀏覽器
// Object localObject = request.getSession().getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME);
找到問題後,對request.getSession()進行分析,此方法內部調用的是request.getSession(true),即沒有session的時候會建立,壓力測試下大併發訪問致使過多的session對象建立用於存儲locale,在必定mina gc後都移到jvm old區,引發頻繁的full gc,拖慢整個響應。那爲何沒有報OOM呢?我猜想是響應慢後拖慢了壓測機打過來的請求,full gc每次都能清理出一些空間出來,避免了OOM。不知道是否是這樣?基於這種session的方式自己是能夠作國際化的,只是我調用的方式不對,因而參考spring的LocaleChangeInterceptor和SessionLocaleResolver類的源碼將咱們的攔截器代碼修改以下:tomcat
public class LocaleHandleInterceptor extends LocaleChangeInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException { String localName = request.getParameter(this.getParamName()); LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request); if (localeResolver == null) { throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?"); } Locale locale = localeResolver.resolveLocale(request); if(!(null ==localName || locale.toString().equalsIgnoreCase(localName))){ localeResolver.setLocale(request, response, (null == localName?Locale.getDefault():StringUtils.parseLocaleString(localName))); } // Proceed in any case. return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { //Locale locale = (Locale)request.getSession().getAttribute(SessionLocaleResolver.LOCALE_SESSION_ATTRIBUTE_NAME); if(null != modelAndView){ // 過濾json返回的狀況 Locale locale = RequestContextUtils.getLocaleResolver(request).resolveLocale(request); modelAndView.setViewName(locale.toString()+"/"+modelAndView.getViewName()); } } }
以後再壓測就沒問題了。session
後記:不通過壓測的代碼不足以談優秀,出了任何問題,先拋開以前的先入爲主根深蒂固的觀念,從源頭分析,大膽猜想,當心求證,並保持學習的心態,繞開問題很容易,可是直面問題須要勇氣和耐心,踏過了它,你就會前進一步,而技術上的前進是技術人員真正的財富。併發