作者:京東零售 李文龍
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 = https://www.cnblogs.com/Jcloud/archive/2023/04/28/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可選擇的方案就比較多了,主要有:
-
啟用HiddenHttpMethodFilter,添加對應的引數,保證升級前后過濾器個數與順序一致
-
調整理過濾器A與M的順序,保證M在A之前執行即可,
-
修改過濾器M內部的邏輯,不在初始化的時候決議body,或是在決議body后將引數重新放置到parameterMap中,
此文是筆者按照分析流程進行簡單驗證,分析驗證程序中難免有遺漏之處,如有錯誤遺漏還煩請各位指出共同進步,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/551435.html
標籤:其他
上一篇:springboot專案出現”java: 錯誤: 無效的源發行版:17“問題解決方案
下一篇:返回列表
