當前位置: 妍妍網 > 碼農

工作六年,看到這樣的程式碼,內心五味雜陳...

2024-02-21碼農

在金塊看到一篇文章,讓我產生了想要分享的欲望。

講述的是面對同一個需求,一個工作經驗不到兩年的小鮮肉和一個工作六年的老司機給出的兩個不同技術方案的實作落地。

先是小鮮肉寫了一版實作,然後老司機在審查程式碼的時候覺得應該有更優雅的落地解決方案,於是又按照自己的思路重構了一版。

一、歷史背景

那天下午,看到了令我終生難忘的程式碼,那一刻破防了......

故事還得從半年前數據隔離的那個事情說起......

1.1 數據隔離

預發,灰度,線上環境共用一個資料庫。

每一張表有一個 env 欄位,環境不同值不同。

特別說明:env 欄位即環境欄位。

如下圖所示:

1.2 隔離之前

插曲:一開始只有 1 個核心表有 env 欄位,其他表均無該欄位;有一天預發環境的操作影響到客戶線上的數據。為了徹底隔離,剩余的二十幾個表均要添加上環境隔離欄位。

當時二十幾張表已經大量生產數據,隔離需要做好相容過渡,保障數據安全。

1.3 隔離改造

其他表歷史數據很難做區分,於是新增加的欄位 env 初始化 all ,表示預發線上都能存取。以此達到歷史數據的相容。

每一個環境都有一個自己獨立標誌;從 application.properties 中讀該欄位;最終到資料庫執行的語句如下:

SELECT XXX FROM tableName WHERE env = ${環境欄位值} and ${condition}

1.4 隔離方案

最拉胯的做法:每一張表涉及到的 DO、Mapper、XML等挨個添加 env 欄位。但我指定不能這麽幹!!!

具體方案:自訂 mybatis 攔截器進行統一處理。

透過這個方案可以解決以下幾個問題:

  • 業務程式碼不用修改,包括 DO、Mapper、XML等。只修改 mybatis 攔截的邏輯。

  • 挨個添加補充欄位,工程量很多,出錯機率極高

  • 後續擴充套件容易

  • 1.5 最終落地

    在 mybatis 攔截器中, 透過覆寫 SQL。新增時填充環境欄位值,查詢時添加環境欄位條件,真正實作改一處即可。

    考慮歷史數據過渡,將 env = {當前環境},'all')

    SELECT xxx FROM ${tableName} WHERE env in (${當前環境},'all'AND ${其他條件}

    具體實作邏輯如下圖所示:

    1.其中 env 欄位是從 application.properties 配置獲取,全域唯一,只要環境不同,env 值不同

    借助 JSqlParser 開源工具,覆寫 sql 語句,修改重新填充、查詢拼接條件即可。

    https://github.com/JSQLParser/JSqlParser

    思路:自訂攔截器,填充環境參數,修改 sql 語句,下面是部份程式碼範例:

    @Intercepts(
    {@Signature(type = Executor. classmethod"update", args = {MappedStatement. classObject. class})}
    )
    @Component
    public classEnvIsolationInterceptorimplementsInterceptor
    {
    ......
    @Override
    public Object intercept(Invocation invocation)throws Throwable {
    ......
    if (SqlCommandType.INSERT == sqlCommandType) {
    try {
    // 重寫 sql 執行語句,填充環境參數等
    insertMethodProcess(invocation, boundSql);
    catch (Exception exception) {
    log.error("parser insert sql exception, boundSql is:" + JSON.toJSONString(boundSql), exception);
    throw exception;
    }
    }
    return invocation.proceed();
    }
    }

    一氣呵成,完美上線。

    二、發展演變

    2.1 業務需求

    隨著業務發展,出現了以下需求:

    1.上下遊合作,我們的 PRC 介面在匹配環境上與他們有差異,需要改造

    SELECT * FROM ${tableName} WHERE bizId = ${bizId} and env in (?,'all')

    2.有一些環境的數據相互相共享,比如預發和灰度等

    3.開發人員的部份後面,希望在預發能糾正線上數據等

    2.2 初步溝通

    這個需求的落地交給了來了快兩年的小鮮肉。

    在開始做之前,他也問我該怎麽做。我簡單說了一些想法,比如可以跳過環境欄位檢查,不拼接條件;或者拼接所有條件,這樣都能查詢;亦或者看一下能不能註解來標誌特定方法,你想一想如何實作......

    年紀大了需要給年輕人機會。

    2.3 勤勞能幹

    小鮮肉,沒多久就實作了。

    不過有一天下午他遇到了麻煩。他填充的環境欄位取出來為 null,看來很久沒找到原因,讓我幫他看看。

    但是不久前也還教過他 Arthas 如何使用呢,這種問題應該不在話下吧?

    2.4 具體實作

    大致邏輯:在需要跳過環境條件判斷的方法前後做寫死處理,同環切面邏輯, 一加一刪。

    填充顏色部份為小鮮肉的改造邏輯。

    大概邏輯就是:將 env 欄位填充所有環境。條件過濾的忽略的目的。

    SELECT * FROM ${tableName} WHERE env in ('pre','gray','online','all'AND ${其他條件}

    2.5 錯誤原因

    經過排查是因為 API 裏面有多處對 threadLoal 進行處理的邏輯,方法之間存在呼叫。

    簡化舉例:A 和 B 方法都是獨立的方法, A 在呼叫 B 的過程,B 結束時把上下文環境欄位刪除, A 在獲取時得到 null。

    具體如下:

    2.6 五味雜陳

    當我看到程式碼的一瞬間,徹底破防了......

    queryProject 方法裏面呼叫 findProjectWithOutEnv, 在兩個方法中,都有填充處理 env 的程式碼。

    2.7 遍地開花

    然而,這三行程式碼,隨處可見,在業務程式碼中遍地開花.......

    // 1. 變量保存 oriFilterEnv
    String oriFilterEnv = UserHolder.getUser().getFilterEnv();
    // 2. 設定值到套用上下文
    UserHolder.getUser().setFilterEnv(globalConfigDTO.getAllEnv());
    //....... 業務程式碼 ....
    // 3. 結束復原
    UserHolder.getUser().setFilterEnv(oriFilterEnv);

    改了個遍,很勤勞......

    2.8 靈魂開問

    難道真的就只能這麽做嗎,當然還有......

  • 開閉原則符合了嗎

  • 改漏了應該辦呢

  • 其他人遇到跳過的檢查的場景也加這樣的程式碼嗎

  • 業務程式碼和功能代分碼離了嗎

  • 填充到套用上下文物件 user 合適嗎

  • .......

  • 大量魔法值,單行字元超 500,方法長度拖幾個螢幕也都睜一眼閉一只眼了,但整這一出,還是破防......

    內心湧動,我覺得要重構一下。

    三、重構一下

    3.1 困難之處

    在 mybatis intercept 中不能直接精準地獲取到 service 層的介面呼叫。只能透過棧幀查詢到呼叫鏈。

    3.2 問題列表

  • 盡量不要修改已有方法,保證不影響原有邏輯;

  • 盡量不要在業務方法中修改功能程式碼;關註點分離;

  • 盡量最小改動,修改一處即可實作邏輯;

  • 改造後復用能力,而不是依葫蘆畫瓢地添加這種程式碼

  • 3.3 實作分析

  • 用獨立的 ThreadLocal,不與當前使用者資訊上下文混合使用

  • 註解+AOP,透過註解參數解析,達到目標功能

  • 對於方法之間的呼叫或者迴圈呼叫,要考慮最佳化

  • 同一份程式碼,在多個環境執行,不管如何,一定要考慮線上數據安全性。

    3.4 使用案例

    采用了自訂註解的方式:@InvokeChainSkipEnvRule

    其使用案例如下:

    @InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})

    案例說明:project 表在預發環境校驗跳過。

    使用的方式就是在呼叫入口處添加該註解:

    @SneakyThrows
    @GetMapping("/importSignedUserData")
    @InvokeChainSkipEnvRule(skipEnvList = {"pre"}, skipTableList = {"project"})
    publicvoidimportSignedUserData(
    ......
    HttpServletRequest request,
    HttpServletResponse response)
    {
    ......
    }

    3.5 具體實作

  • 1.方法上標記註解, 註解參數定義規則

  • 2.切面讀取方法上面的註解規則,並傳遞到套用上下文

  • 3.攔截器從套用上下文讀取規則進行規則判斷

  • @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public@interface InvokeChainSkipEnvRule {
    /**
    * 是否跳過環境。 預設 true,不推薦設定 false
    *
    @return
    */

    booleanisKip()defaulttrue;
    /**
    * 賦值則判斷規則,否則不判斷
    *
    @return
    */

    String[] skipEnvList() default {};
    /**
    * 賦值則判斷規則,否則不判斷
    *
    @return
    */

    String[] skipTableList() default {};
    }

    3.6 不足之處

  • 1.整個鏈路上的這個表操作都會跳過,顆粒度還是比較粗

  • 2.註解只能在入口處使用,公共方法呼叫盡量避免

  • 那還要不要完善一下,還有什麽沒有考慮到的點呢?

    拿起手機看到快 12 點的那一刻,我還是選擇先回家了......

    四、總結思考

    4.1 隔離總結

    這是一個很好參考案例:在套用中既做了數據隔離,也做了數據共享。透過自訂攔截器做數據隔離,透過自定註解切面實作數據共享。

    4.2 編碼總結

    同樣的程式碼寫兩次就應該考慮重構了

  • 盡量修改一個地方,不要寫這種邊邊角角的程式碼

  • 善用自訂註解,解決這種通用邏輯

  • 可以妥協,但是要有底線

  • ......

  • 4.3 場景總結

    簡單梳理,自訂註解 + AOP 的場景

    自訂註解很靈活,套用場景廣泛,可以多多挖掘。

    4.4 反思總結

  • 如果一開始就做好技術方案或者直接使用不同的資料庫

  • 是否可以拒絕那個所謂的需求

  • 先有設計再有編碼,別瞎搞

  • 4.5 最後感想

    在這個只講業務結果,不講技術氛圍的環境裏,突然有一些傷感。身體已經開始吃不消了,好像也過了那個對技術較真死摳的年紀。

    突然一想,這麽做的意義又有多大呢?

    來源:uzong | juejin.cn/post/7294844864020430902

    >>

    END

    精品資料,超贊福利,免費領

    微信掃碼/長按辨識 添加【技術交流群

    群內每天分享精品學習資料

    最近開發整理了一個用於速刷面試題的小程式;其中收錄了上千道常見面試題及答案(包含基礎並行JVMMySQLRedisSpringSpringMVCSpringBootSpringCloud訊息佇列等多個型別),歡迎您的使用。

    👇👇

    👇點選"閱讀原文",獲取更多資料(持續更新中