动态

详情 返回 返回

CR被批“寫得像坨屎”,我三句話讓他當場閉嘴 - 动态 详情

這坨屎山,我接了

大家好,我是老A。

我想很多程序員有過這種經歷,新接手一個項目,打開工程一看,妥妥的一大坨🤦,內心OS:好嘛,又要“屎山雕花”了。。。

我這兩年在做電商業務,所以業務上經常會搞大促,3天一小促,5天一大促,作為技術早就習慣了這種研發節奏(倒排)。今年6月是我們業務年中的一次大型大促,所以5月份的需求爆炸多,基本都是倒排,業務天天拿着大喇叭在我們屁股後喊📢:這個需求不做就會死。。。

在需求集中爆發的這個月,我們團隊接到一個關於“虛擬庫存預佔”的緊急需求,必須在一個祖傳的核心訂單模塊(屎山)裏實現。這個模塊是出了名的“shit”,代碼三年沒人敢動,文檔約等於沒有,最後一任維護者已經退隱江湖了。。。

PRD評審會上,技術這邊一片寂靜,眼神裏充滿了“you can you up,我家還有事”的謙讓。

很不幸,由於另外幾個大神都有不接這個需求的“合理”理由,所以這個需求順理成章的落在了我的頭上,shit!!!

技術調研階段我整整花了兩天時間,在屎山裏深潛、考古、通靈,最後終於被我摸清了它的基本脈絡。過程中發現了一個被濫用多年的ThreadLocalContextHolder,可謂是這坨屎山的精髓所在,這東西就像個幽靈,把user id在幾十個方法調用之間“隔空投送”,代碼的可讀性和可測性約等於零。

此刻我面臨一個兩難抉擇:

  1. 理想主義方案:花兩週時間,徹底重構整個模塊,廢棄ThreadLocal。這樣做最正確,但業務方和產品肯定會提刀來見。
  2. 務實主義方案:搞一個“臨時止血”方案,只寫好我自己的增量代碼,在與舊模塊交互時,臨時兼容一次那個ThreadLocal,確保3天內能上線。

經過深思熟慮後我選了方案2。我知道這個選擇,在CR評審會上肯定會受到質疑和挑戰,甚至可能會上演一場好戲,但是依然這麼還是這麼選擇了。\
最終我提交的CR,核心增量代碼如下(僅截取部分調用了那個ContextHolder的核心代碼):

@Service
public class StockValidationService {

    @Autowired
    private SkuRepository skuRepository;

    /**
     * 6月大促新增的核心功能:校驗預佔庫存的合法性。
     * 
     * @param skuId 商品ID
     * @param quantity 預佔數量
     * @return 是否校驗通過
     */
    public boolean validatePreOccupation(String skuId, Integer quantity) {
        if (StringUtils.isBlank(skuId) || Objects.isNull(quantity) || quantity <= 0) {
            throw new BizException(INVENTORY_PARAM_ILLEGAL.getCode, INVENTORY_PARAM_ILLEGAL.getDesc);
        }

        // ...此處省略10行業務邏輯,比如風控校驗、活動規則校驗等...

        // 為了獲取當前操作的用户信息,進行風險等級判斷不得不調用了這個祖傳的ContextHolder
        Long currentUserId = ContextHolder.getCurrentUserId();
        if (isHighRiskUser(currentUserId)) {
            // ...執行高風險用户校驗邏輯...
        }

        Integer stock = skuRepository.getStock(skuId);
        return stock >= quantity;
    }

    private boolean isHighRiskUser(Long userId) {
        // ...根據用户ID判斷是否為高風險用户的邏輯...
        return false;
    }
}

第二幕:CR評審會上的“公開處刑”

我們需求上線前都要經過嚴格的代碼評審,所以在上線前一週我就已經預約好了架構組和主管們的時間,在上線前3天的一個上午開始了代碼評審會,會議室裏,技術主管和主管的主管都列席旁聽。輪到我負責的部分,那位以代碼潔癖聞名、眼裏容不得沙子的架構師大B哥,開啓了他的挑戰。

他沒有看我的增量代碼細節,而是順着那一行ContextHolder.getCurrentUserId()的調用,直接點開了ContextHolder這個祖傳類。

他冷笑一聲,沒有看我,而是轉向在場的技術主管們説:“領導,我有點不明白。都2025年了,我們團隊的代碼庫裏,為什麼還會允許這種反模式存在?

然後,他才把頭轉向我,音量不大,但整個會議室都聽得見:

“小A,你這次提交的代碼,就像是在一鍋餿了的湯裏,加了一勺松茸。湯還是餿的,松茸也被污染了。 我説的不是你寫的這幾行業務邏輯,我説的是你選擇與這坨屎共存的這個決策本身,這個決策,就很屎!\
並且你這個方案太髒了!一個合格的設計,至少應該做到單一職責和依賴倒置!你應該把風險校驗抽成一個RiskService,把活動規則抽成一個RuleEngine,把輸入輸出都用DTO封裝起來,像這樣……”
接着大B就開始在白板上大展鴻圖,畫了一個大致的類圖。

image.png

我能感覺到腎上腺素瞬間飆升,臉頰發燙,雖然早有預料到會被質疑和挑戰,但是這個B説的話是真難聽啊,不愧是大B。


第三幕:B面反擊——“三板斧,讓佈道師迴歸現實”

第一招:釜底抽薪——“您説的都對,但這坨屎,是三年前的”

面對這種“公開處刑”,我沒有慌亂,因為這種情況也算是預料之中的,我整理了一下思緒,點了點頭:“B哥,您對ThreadLocal濫用的所有批評,我100%同意。事實上,這坨屎比您説的還臭。它的可測試性幾乎為0,在異步場景下就是P0級故障的定時炸彈。

我又看了看白板上那張近乎“完美”的架構圖,平靜地反問:“B哥,您這套方案,非常完美,但為了實現它,我們需要新增8個類,修改5個模塊,聯調3個下游,還需要一週的完整迴歸測試。而我這次任務的工期,只有3天。”

“所以,我們今天討論的,是如何用最短的時間最小的代價完成業務訴求,我的臨時方案,雖然醜但是能保證業務活下去。而您畫的這張圖,是極其完美主義,所有程序員都向往的存在,如果給我足夠的時間,我十分願意這樣去實現”

接着我在投屏打開了SonarQube的掃描報告頁面,話鋒一轉:“B哥,您可以看下SonarQube報告,你之前説的這些問題,其實我在做需求的時候,就已經全部瞭解到了,可是我們沒時間去重構啊。大家可以看看這個類的最後修改時間,是2022年。我這次的任務,是在3天內把這個大促業務需求完成,這個工程本身就像一棟馬上要塌的危樓,我的目標是加一個消防通道,確保大促的業務能活下去。我是在救火,不是在裝修。”

image.png

第二招:靈魂拷問——“如果讓你來選,是‘死’還是‘髒’?”

我不給大B喘息的機會,直接把問題拋給他:“所以,當時我面臨一個選擇:\
**A,花兩週時間徹底重構,讓代碼變得優雅,但業務延期,大促功能上不了線。\
B,用一個‘臨時止血’方案,兼容這坨屎,3天上線,保證業務成功。** ”

我看着B哥,真誠地提問:“B哥,我想請教一下,如果這個決策是您來做,您會選擇讓業務‘優雅地死’,還是‘骯髒地活’?
image.png

第三招:更進一步——“我不僅想到了,我還驗證過了”

在對方陷入沉默時,我繼續輸出:“當然,我不是隻想骯髒地活。作為一個工程師,我也有潔癖。在評估臨時方案的同時,我也在思考,重構這塊到底需要多大代價?會不會有什麼我們沒想到的坑?所以,我利用下班時間,針對最核心的邏輯,做了一個技術驗證原型

這個POC證明了,採用顯式參數傳遞的方案是完全可行的,並且對下游的改造成本是可控的。我的結論是,完整的重構,預估需要80人日。只要這次大促的需求上線,我隨時可以帶人去拆彈。\
我的CR,結束了。”

重構後的代碼
@Service
public class StockValidationService {

    @Autowired
    private SkuRepository skuRepository;

    /**
     * 重構後的版本:所有依賴都通過“顯式參數傳遞”,方法變成了一個純粹、可預測的函數
     *
     * @param skuId 商品ID
     * @param quantity 預佔數量
     * @param currentUserId  核心變化-當前用户ID被作為參數明確地傳遞進來
     * @return 是否校驗通過
     */
    public boolean validatePreOccupation(String skuId, Integer quantity, Long currentUserId) {
        if (StringUtils.isBlank(skuId) || Objects.isNull(quantity) || quantity <= 0) {
            log.wanrn("xxx");
            throw new BizException(INVENTORY_PARAM_ILLEGAL.getCode, INVENTORY_PARAM_ILLEGAL.getDesc);
        }
        
        if (StringUtils.isBlank(currentUserId)) {
            log.wanrn("xxx");
            throw new BizException(USER_STATUS_ILLEGAL.getCode, USER_STATUS_ILLEGAL.getDesc);
        }

        // ...同樣的業務邏輯...

        if (isHighRiskUser(currentUserId)) {
            // ...執行高風險用户校驗邏輯...
        }

        Integer stock = skuRepository.getStock(skuId);
        return stock >= quantity;
    }

    private boolean isHighRiskUser(Long userId) {
        // ...根據用户ID判斷是否為高風險用户的邏輯...
        return false;
    }
}

B哥笑了笑説:“改個ThreadLocal而已,能有多大成本?你這是在為自己的妥協找藉口罷了。”

“B哥,如果只是單純改代碼,確實很快。但在做這個POC時我也對整個重構的成本,做了一次完整的評估。這是我的評估報告,我邊説邊將投屏切換到了我的成本分析頁:

影響面分析與方案設計 (10 人/天)\
依賴分析:ContextHolder.getCurrentUserId()這個“幽靈”到底飄散在多少個類、多少個方法裏?我們需要用IDE的靜態分析工具,完整地畫出一張“依賴地圖”。

鏈路梳理:這些被調用的方法,又被多少上游的業務方(Controller, Job, MQ Consumer)所依賴?整個調用鏈路有多深?

方案評審:設計出的新版接口和參數傳遞方案,需要組織多個相關團隊進行評審,達成共識。

代碼重構與單測修復 (20 人/天)\
代碼修改:這才是真正的“體力活”,修改幾十上百個文件的方法簽名和調用。

單元測試重構:所有依賴了ContextHolder的舊單元測試,現在全部要重寫。因為依賴了“全局狀態”,這些測試本身可能就很脆弱,修復成本極高。

全鏈路迴歸測試 (40 人/天)

自動化測試:需要QA同學配合,修改所有覆蓋到相關鏈路的自動化測試用例。

手動測試:由於改動的是核心模塊,可能會引發意想不到的“蝴蝶效應”。QA團隊必須對所有相關業務場景,進行一次徹底的、地毯式的“人工迴歸測試”,確保沒有產生新的Bug。這個過程極其耗時。

灰度發佈與線上觀察 (10 人/天)

發佈策略:這種核心改動,絕不可能“一鍵全量上線”。必須制定周密的灰度發佈計劃(比如先上1%的機器,再到10%,再到50%),並準備好隨時可以回滾的預案。

線上觀察:在灰度發佈期間,需要有專人(開發、QA、SRE)24小時盯着監控大盤,觀察業務指標、系統性能是否有異常波動。

所以真正的成本大頭,在測試和發佈這兩個環節。要動這個核心模塊,我們至少需要協調3個上下游團隊,並需要測試團隊投入至少一個月的迴歸測試資源。這個成本,不是我一個人利用下班時間就能覆蓋的。”

重構的理論支撐

1⃣️《Effective Java》的告誡\
Joshua Bloch 在《Effective Java》Item 57 中強調:“Minimize the scope of local variables”(最小化局部變量的作用域)。\
解讀:ThreadLocal本質上是將一個變量的作用域,從“方法內”擴大到了“整個線程的生命週期”,這是一種“作用域濫用”。一個優秀的API設計,應儘可能減少這種對外部隱式狀態的依賴。

2⃣️ 框架之魂:Spring Framework 的設計哲學\
Spring框架的核心,是“依賴注入”(Dependency Injection)。\
解讀:Spring花了20年時間,教育了全世界的Java開發者——“你需要什麼,就通過構造函數或方法參數明確地告訴我,不要讓我去一個全局的地方自己找”。ContextHolder這種做法,完全違背了IoC和DI的初衷。

3⃣️ 未來之勢:異步/虛擬線程的“天敵”\
Oracle官方在Java 21虛擬線程(Project Loom)的JEP 444文檔中明確指出:
“Avoid thread-local variables... thread-local variables are a significant obstacle to scalability.”(避免使用線程局部變量...它是可伸縮性的巨大障礙)\
解讀:在未來的虛擬線程模型下,一個請求可能會在多個不同的底層OS線程上執行。ThreadLocal這種強依賴物理線程的模式,會徹底失效,並帶來難以追蹤的Bug。繼續使用它,等於是在為未來的技術升級埋下最深的雷。


曲終人散——CR,過

會議室裏鴉雀無聲。B哥看了看我,又看了看各位主管,最後點點頭,説:“優化這塊進7月迭代排期池吧。這次CR,過。”

會議結束後,我腦子裏盤旋着一個想法:Code Review的最高境界,不是找茬,而是共識。一個優秀的工程師,不僅要能寫出乾淨的代碼,更要有勇氣和智慧,去推動團隊償還應用的歷史債務。

這,可能就是“B面”工作的常態吧。


福利時間

為了感謝大家的支持,我把這兩年在一線大廠面試和帶團隊的過程中,沉澱下來的所有的私房筆記,整理成了一份《大廠碼農老A的B面真話手冊》。\
裏面包含了我這麼多年在職場摸爬滾打總結下來的職場成長經驗、潛規則和避坑指南,當然也有我總結的很多求職經驗、各類經典題目以及我的獨門心得。\
關注我的同名公眾號【大廠碼農老A】,後台回覆“B面”你會發現新大陸。
最後,如果覺得內容還行,也希望能點個贊、點個在看,讓更多需要它的兄弟看到。感謝大家!

user avatar cyzf 头像 zaotalk 头像 nihaojob 头像 sofastack 头像 razyliang 头像 leexiaohui1997 头像 huajianketang 头像 debuginn 头像 zero_dev 头像 u_16769727 头像 u_11365552 头像 jiangyi 头像
点赞 197 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.