ThreadLocal系列之——業(yè)務(wù)開發(fā)實踐(一)

寫作目的

以前的工作經(jīng)歷中,筆者本人有深度使用ThreadLocal的經(jīng)驗,它在合適的場景下,是非常好用的一個工具,因此打算分享一二,為各位看官們實際編碼過程中提供多一種選擇,促進大家共同進步

場景

先舉兩個大家熟悉的、Spring中用到的場景

場景一

在Spring管理的Singleton Bean中,如果期望調(diào)用同一個類里被事務(wù)注解的方法(m1調(diào)用m2),且希望事務(wù)能生效,可以考慮的實現(xiàn)如下:

@Service
public class FooService {

    public void m1() {
        // 拿到FooService的代理對象
        FooService self = (FooService)AopContext.currentProxy();
        // 通過代理對象調(diào)用m2方法,事務(wù)切面就能生效
        self.m2();
    }

    @Transactional
    public void m2() {
        // save to db;
    }

}

之所以要通過代理對象去調(diào)用,是因為若在m1中使用原始對象(this),不會進入代理邏輯,因此切面邏輯(事務(wù))是不會生效的。所以問題就轉(zhuǎn)變成了:在一個方法執(zhí)行過程中,如何拿到當(dāng)前方法所屬對象的代理對象?

  1. 在FooService中直接注入自己

  2. 通過調(diào)用AopContext.currentProxy()

為與本文扣題,我們的示例采用的是方式2。那么接下來需要探尋的是,為什么AopContext.currentProxy()就能拿到代理對象呢?

  1. 外部方法調(diào)用FooService#m1的時候,進入了代理邏輯(org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept),與此同時,會把代理對象放到AopContext
image
  1. AopContext#setCurrentProxy會將代理對象(proxy)放到currentProxy中,而currentProxy是一個ThreadLocal
    image
image
  1. 在m1方法中執(zhí)行FooService self = (FooService)AopContext.currentProxy();,本質(zhì)就是從當(dāng)前線程的ThreadLocal中取出上邊1、2步放入的代理對象,接下來就可以用代理對象"搞事情"

    image
  2. 代理對象切面邏輯結(jié)束后,用oldProxy將AopContext還原

    image

如此這般,通過預(yù)先將代理對象放入當(dāng)前線程的ThreadLocal的方式,就可以做到在接下來的流程中,在任意位置都可以很方便獲取到該代理對象,而不需要通過方法參數(shù)一層層透傳下去

在理解上,可以將ThreadLocal理解成為當(dāng)前線程裝東西的"籃子":在線程執(zhí)行任務(wù)時,可以在某節(jié)點(方法)將一些東西放進"籃子",并可在后續(xù)的任意節(jié)點(方法)從"籃子"取出之前放入的東西

image
場景二

大家可能會經(jīng)常使用到Spring @Transactional事務(wù)注解,若不理解原理,可能容易踩坑。一個基本共識是:事務(wù)由連接管理,一個事務(wù)只屬于一個連接,若要使得事務(wù)生效(相關(guān)DB操作同時提交,同時回滾),必須確保是同一個連接

那么就會有如下場景:

@Service
public class FooService {

    @Resource
    private BarService barService;

    @Transactional
    public void foo() {
        barService.bar();

        // save foo
        // ...
    }
}

@Service
public class BarService {

    @Transactional
    public void bar() {
        // save bar
    }
}

FooService#foo調(diào)用BarService#bar進行關(guān)于Bar的數(shù)據(jù)庫操作,之后進行Foo的數(shù)據(jù)庫操作,由于方法foo與方法bar都被@Transactional注解,可以確保事務(wù)操作的同時提交或同時回滾。"確保事務(wù)操作的同時提交或同時回滾"是我們在大量的編程經(jīng)驗中可以輕易得出的結(jié)論,如果追究根因,其實就是共識:這兩個操作處于同一個事務(wù)當(dāng)中,都由同一個連接里的事務(wù)進行管理。因此,重點是需要確保"同一個事務(wù)"以及"同一個連接"。"同一個事務(wù)"是如何實現(xiàn)的,請參考org.springframework.transaction.support.AbstractPlatformTransactionManager#handleExistingTransaction并理解事務(wù)的傳播屬性(不是本文重點,略過)。

那么問題就來了:"同一個連接"又是如何實現(xiàn)的呢?代碼跑在不同類以及不同方法上,Spring如何做到前后兩次獲取的是同一個連接?如果看官們理解了場景一,相信此處應(yīng)該有了結(jié)論 -> ThreadLocal

  1. 外部調(diào)用FooService#foo第一次開啟事務(wù)的時候,將從連接池中取出一個連接,并將連接放到 ConnectionHolder 中,然后將ConnectionHolder綁定到當(dāng)前線程

    // org.springframework.jdbc.datasource.DataSourceTransactionManager#doBegin
    
    protected void doBegin(Object transaction, TransactionDefinition definition) {
     DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
     Connection con = null;
    
     try {
         if (!txObject.hasConnectionHolder() ||
                 txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
             // 從連接池中取出連接,并將連接放到 ConnectionHolder 中
             
             Connection newCon = this.dataSource.getConnection();
             // ...(省略)
             txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
         }
         // ...(省略)
    
         // Bind the connection holder to the thread.
         // 將 ConnectionHolder 綁定到當(dāng)前線程
         if (txObject.isNewConnectionHolder()) {
             TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
         }
     }
     // ...(省略)
    }
    

將value(ConnectionHolder)放到map之后,再放到當(dāng)前線程的ThreadLocal中,以實現(xiàn)與當(dāng)前線程的綁定

// org.springframework.transaction.support.TransactionSynchronizationManager#bindResource

public static void bindResource(Object key, Object value) throws IllegalStateException {
 Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); // 忽略
 
 // resources 是ThreadLocal類型的成員變量
 Map<Object, Object> map = resources.get();
 // set ThreadLocal Map if none found
 if (map == null) {
     map = new HashMap<Object, Object>();
     resources.set(map);
 }
 // 將value(ConnectionHolder)放到map之后,再放到當(dāng)前線程的ThreadLocal中,以實現(xiàn)與當(dāng)前線程的綁定
 Object oldValue = map.put(actualKey, value);
 
 // ...(省略)
}
image
  1. FooService#foo調(diào)用BarService#bar將觸發(fā)第二次開啟事務(wù),發(fā)現(xiàn)傳播屬性為TransactionDefinition.PROPAGATION_REQUIRED(默認(rèn)值),會將第二次事務(wù)直接"融入"第一次事務(wù),能實現(xiàn)的關(guān)鍵點在于前后兩次操作使用同一個連接。如下,是從resources(ThreadLocal)中取出map,并取出第1步中放入的ConnectionHolder,因此就可以確保拿到同一個連接

    // org.springframework.transaction.support.TransactionSynchronizationManager#getResource
    
    public static Object getResource(Object key) {
     Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
     return doGetResource(actualKey);
    }
    
    private static Object doGetResource(Object actualKey) {
     // resources 是ThreadLocal類型的成員變量
     // 從ThreadLocal中取到第1步中放入的ConnectionHolder
     Map<Object, Object> map = resources.get();
     if (map == null) {
         return null;
     }
     Object value = map.get(actualKey);
     // ...(省略)
     
     return value;
    }
    

實戰(zhàn)

以上使用ThreadLocal的場景,都是框架里提供的,是否業(yè)務(wù)開發(fā)中就用不到了呢?非也,業(yè)務(wù)開發(fā)中如果運用妥當(dāng),同樣能省掉很多事,實現(xiàn)精簡代碼的目的。筆者同樣舉兩個業(yè)務(wù)開發(fā)用到的場景,以供大家開闊思路

場景一

該場景比較普遍,幾乎所有公司都用的上,因與業(yè)務(wù)無關(guān),首先介紹,幫助沒有使用過ThreadLocal的看官們找找感覺

所有公司所有業(yè)務(wù)都會有用戶體系,進行業(yè)務(wù)操作都需要登錄、鑒權(quán),以識別某用戶是否有操作某項資源的權(quán)限,因此基本上很多業(yè)務(wù)都需要拿到當(dāng)前請求的用戶信息

一種可以考慮的方式是:每次請求到來時,可否在入口中統(tǒng)一鑒權(quán),然后將鑒權(quán)之后的用戶信息記錄下來,接下來但凡有業(yè)務(wù)要用到,可以直接從保存的地方獲取到用戶信息,不用再一次鑒權(quán),省去一次次的計算消耗;待請求結(jié)束后,就將用戶信息銷毀

  1. 注冊一個HandlerInterceptorFilter,用于在入口處攔截請求,并對當(dāng)前請求進行鑒權(quán),獲取用戶信息,放入UserHolder中,并在請求結(jié)束的時候清理掉該用戶信息(注:請求結(jié)束后清理用戶信息很重要,避免內(nèi)存泄露)
public class LoginCheckInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        // 鑒權(quán)(獲取用戶信息)
        User user = xxx(request);
        if (user != null) {
            // 放到UserHolder這個容器(Holder)中
            UserHolder.putUser(user);
            return true;
        }
        // 鑒權(quán)(獲取用戶信息)失敗,則直接攔截
        return false;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 請求結(jié)束后,清理掉用戶信息(畫重點:很重要)
        UserHolder.clear();
    }
}

UserHolder是一個容器,包裝ThreadLocal,放置當(dāng)次請求中用戶的信息,如下示:

public class UserHolder {
        // 內(nèi)部維護一個ThreadLocal成員變量
    private static ThreadLocal<User> tl = new ThreadLocal<>();

    public static void putUser(User user) {
        tl.set(user);
    }

    public static User getUser() {
        return tl.get();
    }

    public static Long getUid() {
        return Optional.ofNullable(tl.get()).map(User::getId).orElse(null);
    }

    public static void clear() {
        tl.remove();
    }
}
  1. 在需要使用的地方,例如XXXService,直接在任意方法內(nèi)部調(diào)用UserHolder#getUser 即可獲取到當(dāng)前用戶的完整信息。如此這般,就不需要將User從Controller層層傳遞到Service里的某個小方法,避免了"依賴污染",對于[多層次]的業(yè)務(wù)代碼組織尤為有效,省去了多次參數(shù)傳遞的煩惱

反例:明明request、xxx, yyy 都不直接依賴User,只是在最后一層zzz用到User,卻不得不將用戶信息層層傳遞,造成"依賴污染"

// Service接口方法
public void request(User user) {
    xxx(user);
}

private void xxx(User user) {
    yyy(user);
}

private void yyy(User user) {
    zzz(user);
}

private void zzz(User user) {
    String email = user.getEmail();
}

使用ThreadLocal后,簡化如下,只有zzz需要用到User信息,那就在zzz中直接獲取用戶信息,并進行業(yè)務(wù)邏輯

public void request() {
     xxx();
}

public void xxx() {
    yyy();
}

private void yyy() {
    zzz();
}

private void zzz() {
    User user = UserHolder.getUser();
    String email = user.getEmail();
}

看官們可能會有疑問:我們的后端服務(wù)并不需要鑒權(quán),而是由前邊的網(wǎng)關(guān)做好了鑒權(quán),然后通過某些方式(如請求頭)攜帶給后端服務(wù),那還能使用嗎?答案是肯定的,請看場景二

場景二

筆者以前從事廣告相關(guān)業(yè)務(wù),廣告投放邏輯里需要識別是哪個用戶,當(dāng)前手機型號、品牌是什么,操作系統(tǒng)版本號、APP版本號、設(shè)備尺寸等等總共幾十項信息,這些信息都是由請求頭攜帶到后端服務(wù),而廣告投放邏輯里面不同的模塊會使用不同頭字段來做相應(yīng)的業(yè)務(wù)邏輯

image

在實現(xiàn)上,同樣是在入口處,通過自定義攔截器或過濾器的方式攔截請求,獲取到所有的請求頭信息,封裝進HeaderHolder,以便在不同模塊間方便獲取請求頭信息做業(yè)務(wù)邏輯??梢韵胂?,一個業(yè)務(wù)有不同的模塊,不同模塊又分為不同的業(yè)務(wù)抽象層次,而不同層次代碼中可能需要使用到請求頭信息都不一樣,如果是通過函數(shù)傳參的方式將Header層層傳遞,代碼將變得多么糟糕

一般化推論

誠然,使用ThreadLocal會為我們的編程帶來許多好處,同樣的也為代碼的管理,依賴的梳理帶來了一些挑戰(zhàn)。因此真正要把ThreadLocal用好并不簡單,并非任何場景都可以使用的,用好是一個亮點,用不好就是坑:這也是所有技術(shù)選型所要面臨的問題,技術(shù)本身無所謂好壞,沒有任何一種技術(shù)能在所有方面碾壓同類的競品技術(shù),否則一定能將競品取代而不需要面臨trade off,拋開業(yè)務(wù)場景談技術(shù)(架構(gòu))就是耍流氓

結(jié)合筆者本人的實際使用經(jīng)驗,將使用ThreadLocal的場景總結(jié)為一句話:在盡可能早的時機將一些大多數(shù)后續(xù)流程要使用的只讀信息封裝到ThreadLocal里,供后續(xù)流程任意取用

  1. 盡可能早的時機:好事須趁早,越早生成,就越早能使用,可擴展性越高,最好是在與業(yè)務(wù)無關(guān)的入口處
  2. 大多數(shù): 有選擇性地將大多數(shù)后續(xù)都需要用到的信息放到ThreadLocal里,而不是所有信息無腦放入
  3. 只讀信息: 封裝的信息最好"Read Only",意味著一旦生成,將不可再更改,整個后續(xù)流程都只能讀取,該要求是為了讓代碼更可控,倘若信息可以修改,就有可能導(dǎo)致不同層次的代碼產(chǎn)生間接依賴(如下層依賴上層),系統(tǒng)行為將不可控

總結(jié)

本文從Spring使用ThreadLocal的案例為切入點,介紹了ThreadLocal在開源框架中使用的兩個場景;接著又分享了兩個業(yè)務(wù)開發(fā)中能夠用上的場景,幫助看官們更感性地理解ThreadLocal的作用;最后,針對ThreadLocal潛在"可維護性"問題,鮮明的給出筆者本人的觀點:技術(shù)無所謂好壞,謹(jǐn)慎而不盲目使用,并針對如何正確使用ThreadLocal提出了一般化的方法論,只要遵循相應(yīng)原則,基本上對于代碼還是相當(dāng)可控,而不致引入額外的維護成本

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容