天天看點

springboot更新過程中踩坑定位分析記錄

作者:京東雲開發者

作者:京東零售 李文龍

1.背景

“俗話說:為了修複一個小bug而引入了一個更大bug”

因所負責的系統使用的spring架構版本5.1.5.RELEASE線上上出過一個偶發的小事故,最後定位為spring-context中的一個bug導緻的。

為了修複此bug進行了spring版本的更新,最終定的版本為收銀台團隊使用的版本5.2.12.RELEASE,對應的springboot版本為2.2.12.RELEASE。

選擇這個版本的原因是:

1.有團隊經過了長時間的線上驗證

2.修複了5.1.5.RELEASE對應的bug

2.更新上線

更新相關版本後在預發環境進行了驗證,暫未遇到關于架構的問題。本以為安全更新完成,在上線過程中發現在APP中無法通路,此時還未挂載流量。

日志中分析是某些參數未解析到,後在nginx日志中查到相關請求,使用postman模拟請求可以正常使用。

3.分析驗證定位原因

1.臨時修複

在代碼一緻的情況下,唯一的可能就隻能是線上與預發配置不同,經對比分析得出是某個過濾器的順序線上上未配置,按照預發的配置後可正常使用。我們暫且稱修改的這兩個過濾器為M和A,

其中預設情況下執行順序為M->A,順序修改為A->M後正常,其兩者作用大緻為:

M: 通用過濾器,解析url中的參數至parameterMap中,并初始化讀取了body中的inputstream進行了byte數組的緩存,用于解決重複讀取流問題 A: 特定處理器,先是查詢parameter中的參數,然後邏輯處理後再設定一些特殊參數。

2.為何需要改過濾器順序

經查未更新前過濾器的順序與更新後過濾器順序一緻,為何更新spring架構後需要修改配置。此時猜測可能是spring在更新過程中修改了一部分代碼,

但未有頭緒,隻能先調轉方向分析為什麼postman和浏覽器中的swagger可以正常使用

3.分析nginx日志

前端請求與postman請求的nginx日志進行了分析得出了原因,對比日志如下:

postman:POST /shop/bpaas/floor?client&clientVersion&ip=111.202.149.19&gfid=getShopMainFloor&body= 前端: POST /shop/bpaas/floor HTTP/1.0" 200 634 "-" "api" "0.94" 0.008 0.007 client&clientVersion&ip=111.202.149.17&gfid=getShopMainFloor&body=

經過以上對比發現雖然postman使用了post請求,但資料還是放置在url中,在經過系統的一個内置過濾器M時将url中的參數解析到了parameterMap中,後續過濾器可以使用

request.getParameter擷取到,注意此方法是解決問題的關鍵,此時還未意識到。

4.更新前後架構是否有大的修改

因更新的版本是更新了一個小版本号,是以不好對比更新的buglist,隻能慢慢進行分析,後在分析過濾器時發現更新spring後過濾器個數由11個減少到了10個,減少了那一個為:

org.springframework.web.filter.HiddenHttpMethodFilter           

此過慮器的作用是在浏覽器不支援PUT、DELETE、PATCH等method時,可以在form表單中使用隐藏的_method參數支援這幾種method。好像跟參數解析沒有任何關系,

繼續分析更新版本中 (由2.1.3.RELEASE->2.2.12.RELEASE)是否修改了此過濾器的一些内容,後在2.2.0.M5的release notes中發現HiddenHttpMethodFilter相關的:

“Disable auto-configuration of HiddenHttpMethodFilter by default” github上對應的版本release notes: https://github.com/spring-projects/spring-boot/releases/tag/v2.2.0.M5

也就是說更新後HiddenHttpMethodFilter預設配置由enable修改為了disable,如果再修改回去是不是可以修複參數解析的問題呢?

5.添加過濾器enable配置

因bug修複清單中有對應的issues,是以找到了此過濾器對應的配置:

-Dspring.mvc.hiddenmethod.filter.enabled=true

添加後可以正常使用,證明是此過濾器中在某種條件下不可缺少。

6.未更新spring版本時disable驗證

在确認未更新版本的spring支援此參數的情況下,添加了以上參數,将預設的啟動修改成了禁用,經驗證:在不代碼修改的情況下,無此過濾器時參數無法解析。證明了上步的猜測。

7.深入源碼分析

此時需要分析HiddenHttpMethodFilter過濾器中是否有特殊操作,源碼如下:

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
			throws ServletException, IOException {

		HttpServletRequest requestToUse = request;

		if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
			String paramValue = request.getParameter(this.methodParam);
			if (StringUtils.hasLength(paramValue)) {
				String method = paramValue.toUpperCase(Locale.ENGLISH);
				if (ALLOWED_METHODS.contains(method)) {
					requestToUse = new HttpMethodRequestWrapper(request, method);
				}
			}
		}

		filterChain.doFilter(requestToUse, response);
	}           

分析以上源碼可以發現,有且隻有一種可能,就是request.getParameter可能是解決問題的是關鍵。

8.大膽猜測

分析後源碼猜測,第一步中的修改順序有可能是A中有調用getParameter,是以順序調整為A->M後,相當于間接使用了HiddenHttpMethodFilter。

9.開始驗證

在不使用HiddenHttpMethodFilter的情況下,如果在過濾器原有順序不修改的情況下,隻要在M執行前調用了request.getParameter,理論上可以正常為使用。是以在debug情況下

利用工具在M過濾器調用前先行執行request.getParameter,發現的确可以正常使用。

10.分析過濾器

先前簡述了M的功能,主要是包裝了request,後讀源碼時發現,如果是post請求,讀取body體中的資料後并未解析body中的參數至parameterMap中,而代碼中的其它過濾器都是

通過request.getParameter擷取的資料,重寫後的代碼:

public String getParameter(String name) {
		if ( this.parameterMap.containsKey(name) )
			return this.parameterMap.get(name);
		else {
			return super.getParameter(name);
		}
	}           

在經過request包裝後,先是從paremeterMap中擷取資料,此時map肯定是沒有資料,隻能從父類擷取,而父類擷取時會解析parameter,解析時使用到了inputStream,但M過濾器

的在初始化時解析了輸入流,此時tomcat内部使用内部的request擷取stream時将擷取到空資料,即無法從parameter中擷取到body體中的資料。

而如果在調用M前調用了request.getParameter,tomcat内部将提前于M解析parameter,可以保證後續可擷取到相關參數。

4. 修複方案

既然得出了結論,那麼更新spring版本後修複此bug可選擇的方案就比較多了,主要有:

1.啟用HiddenHttpMethodFilter,添加對應的參數,保證更新前後過濾器個數與順序一緻

2.調整理過濾器A與M的順序,保證M在A之前執行即可。

3.修改過濾器M内部的邏輯,不在初始化的時候解析body,或是在解析body後将參數重新放置到parameterMap中。

此文是筆者按照分析流程進行簡單驗證,分析驗證過程中難免有遺漏之處,如有錯誤遺漏還煩請各位指出共同進步。

繼續閱讀