ThreadLocal在常見框架中的使用

簡介

ThreadLocal能夠?yàn)楫?dāng)前線程提供存儲和讀取變量的能力瓦灶,提供一個靜態(tài)方法烁登,從而能夠讓若干模塊解耦奶陈;也為多線程并發(fā)提供一個思路捧挺,在ThreadLocal中為當(dāng)前儲存變量,只為當(dāng)前線程所用尿瞭,讓多線程之間不互相干擾。

本文簡單介紹ThreadLocal翅睛,列舉一些常見框架中的使用場景声搁,從而對它有更好的認(rèn)識捕发。

ThreadLocal API

ThreadLocal常用的有三個方法set, get, remove,下面用一小段代碼來看看這三個方法的使用疏旨。

public class ThreadLocalTest {

    final static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    @Test
    public void testThreadLocal() {

        final String valueBeforeSet = threadLocal.get();
        log.info("value before set: {}", valueBeforeSet);

        threadLocal.set("test");
        final String valueAfterSet = threadLocal.get();
        log.info("value after set: {}", valueAfterSet);


        threadLocal.remove();
        final String valueAfterRemove = threadLocal.get();
        log.info("value after remove: {}", valueAfterRemove);

    }
}

輸出:

2018-05-21 20:43:41,390{GMT} INFO  value before set: null
2018-05-21 20:43:41,396{GMT} INFO  value after set: test
2018-05-21 20:43:41,396{GMT} INFO  value after remove: null

上面的代碼中可以清楚看到getset方法的使用檐涝,同時remove也是非常重要的,因?yàn)榫€程池的原因法挨,如果不執(zhí)行remove操作谁榜,這個線程在下次被重復(fù)使用的時候凡纳,存儲在ThreadLocal中的值仍可使用。

在框架中的使用

Spring Security中的使用

如果你使用過Spring Security,那么對SecurityContextHolder肯定不陌生:

SecurityContext securityContext = SecurityContextHolder.getContext();
Authentication authentication = securityContext.getAuthentication();

上面這段代碼經(jīng)常用來獲取當(dāng)前認(rèn)證過的用戶相關(guān)信息荐糜,而這個方法之所以能夠工作巷怜,其中之一就用的ThreadLocal
詳細(xì)代碼可以可見github,這里摘取一些通過ThreadLocal來存儲認(rèn)證信息片段暴氏。

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;

        if (request.getAttribute(FILTER_APPLIED) != null) {
            // ensure that filter is only applied once per request
            chain.doFilter(request, response);
            return;
        }

        final boolean debug = logger.isDebugEnabled();

        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);

        if (forceEagerSessionCreation) {
            HttpSession session = request.getSession();

            if (debug && session.isNew()) {
                logger.debug("Eagerly created session: " + session.getId());
            }
        }

        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
        // 下面摘取HttpSessionSecurityContextRepository代碼來看看這個從哪里獲取
        SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

        try {
            // 調(diào)用set方法存儲
            SecurityContextHolder.setContext(contextBeforeChainExecution);
            // 繼續(xù)調(diào)用其他filter
            chain.doFilter(holder.getRequest(), holder.getResponse());

        } finally {
            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
            // Crucial removal of SecurityContextHolder contents - do this before anything else.
            // finally方法中一定要remove,防止線程被重復(fù)使用关带,變量仍在
            SecurityContextHolder.clearContext();
            repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
            request.removeAttribute(FILTER_APPLIED);

            if (debug) {
                logger.debug("SecurityContextHolder now cleared, as request processing completed");
            }
        }
    }

下面在看看上面如何獲取SecurityContext,這里摘取HttpSessionSecurityContextRepository源碼片段,

public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
        // 從requestResponseHolder中獲取request和response
        HttpServletRequest request = requestResponseHolder.getRequest();
        HttpServletResponse response = requestResponseHolder.getResponse();
        HttpSession httpSession = request.getSession(false);

        // 下面private方法看看如何從session里面獲取context
        SecurityContext context = readSecurityContextFromSession(httpSession);

        if (context == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("No SecurityContext was available from the HttpSession: " + httpSession +". " +
                        "A new one will be created.");
            }
            context = generateNewContext();

        }

        SaveToSessionResponseWrapper wrappedResponse = new SaveToSessionResponseWrapper(response, request, httpSession != null, context);
        requestResponseHolder.setResponse(wrappedResponse);

        if(isServlet3) {
            requestResponseHolder.setRequest(new Servlet3SaveToSessionRequestWrapper(request, wrappedResponse));
        }

        return context;
    }

    /**
     *
     * @param httpSession the session obtained from the request.
     */
private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
        final boolean debug = logger.isDebugEnabled();

        if (httpSession == null) {
            if (debug) {
                logger.debug("No HttpSession currently exists");
            }

            return null;
        }

        // Session exists, so try to obtain a context from it.

        // 原來session里面存儲著一個特殊的屬性key為SPRING_SECURITY_CONTEXT研儒,這個就是context
        Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);

        if (contextFromSession == null) {
            if (debug) {
                logger.debug("HttpSession returned null object for SPRING_SECURITY_CONTEXT");
            }

            return null;
        }

        // We now have the security context object from the session.
        if (!(contextFromSession instanceof SecurityContext)) {
            if (logger.isWarnEnabled()) {
                logger.warn(springSecurityContextKey + " did not contain a SecurityContext but contained: '"
                        + contextFromSession + "'; are you improperly modifying the HttpSession directly "
                        + "(you should always use SecurityContextHolder) or using the HttpSession attribute "
                        + "reserved for this class?");
            }

            return null;
        }

        if (debug) {
            logger.debug("Obtained a valid SecurityContext from " + springSecurityContextKey + ": '" + contextFromSession + "'");
        }

        // Everything OK. The only non-null return from this method.

        return (SecurityContext) contextFromSession;
    }

上面代碼我們就看出了ThreadLocalSecurityContextHolderStrategy是如何工作的了。

Spring MVC中的使用

如果你在Spring MVC中啟用過過org.springframework.web.context.request.RequestContextListener好芭,那么對RequestContextHolder肯定不陌生,通過RequestContextHolder提供的靜態(tài)方法招狸,可以獲取當(dāng)前request對象邻薯。

public static HttpServletRequest getRequest() {
        ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        return  attr.getRequest();
    }
public static HttpSession getSession() {
        ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        return attr.getRequest().getSession(false); // true == allow create
    }

上述方法能夠生效只要看看RequestContextListener源碼就很好理解了

public void requestInitialized(ServletRequestEvent requestEvent) {
        if (!(requestEvent.getServletRequest() instanceof HttpServletRequest)) {
            throw new IllegalArgumentException(
                    "Request is not an HttpServletRequest: " + requestEvent.getServletRequest());
        }
        HttpServletRequest request = (HttpServletRequest) requestEvent.getServletRequest();
        ServletRequestAttributes attributes = new ServletRequestAttributes(request);
        request.setAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE, attributes);
        LocaleContextHolder.setLocale(request.getLocale());
        //存儲
        RequestContextHolder.setRequestAttributes(attributes);
    }

public void requestDestroyed(ServletRequestEvent requestEvent) {
        //銷毀
        ServletRequestAttributes attributes =
                (ServletRequestAttributes) requestEvent.getServletRequest().getAttribute(REQUEST_ATTRIBUTES_ATTRIBUTE);
        ServletRequestAttributes threadAttributes =
                (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (threadAttributes != null) {
            // We're assumably within the original request thread...
            if (attributes == null) {
                attributes = threadAttributes;
            }
            LocaleContextHolder.resetLocaleContext();
            RequestContextHolder.resetRequestAttributes();
        }
        if (attributes != null) {
            attributes.requestCompleted();
        }
    }

多數(shù)據(jù)源切換

在開發(fā)中可能會遇到有多個數(shù)據(jù)源的情況厕诡,例如兩個Datasource之間的切換累榜,配合SpringAOPAbstractRoutingDataSource灵嫌,在加上改變ThreadLocal中的值來完成數(shù)據(jù)源的切換。
實(shí)現(xiàn)AbstractRoutingDataSource時猖凛,實(shí)現(xiàn)determineCurrentLookupKey方法來返回ThreadLocal中的值绪穆,通過其他方法
去改變ThreadLocal中的值,例如對某些方法的切面, 自定義注解等等玖院。

log4j中的使用

如果想在·log4j·中打印一些公共的變量。
例如TrackingId表示每一個請求难菌,可以在filter中先通過org.apache.log4j.MDC.put(String key, Object o)插入一個trackingIdkey(例如TACKING_ID)和value,然后配置log4j的格式%X{TACKING_ID}即可在日志中實(shí)現(xiàn)插入一個值,即使為null也沒有問題耍共。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末猎塞,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子荠耽,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件银亲,死亡現(xiàn)場離奇詭異纽匙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)烛缔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來院喜,“玉大人晕翠,你說我怎么就攤上這事喷舀×苌觯” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長甲棍。 經(jīng)常有香客問我,道長感猛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任颈走,我火速辦了婚禮咱士,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘序厉。我一直安慰自己,他們只是感情好弛房,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著荷逞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪种远。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天筏养,我揣著相機(jī)與錄音常拓,去河邊找鬼。 笑死弄抬,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的掂恕。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼依啰,長吁一口氣:“原來是場噩夢啊……” “哼店枣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鸯两,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤钧唐,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后钝侠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡爬范,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年弱匪,在試婚紗的時候發(fā)現(xiàn)自己被綠了璧亮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片斥难。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖哑诊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情竞阐,我是刑警寧澤暑劝,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站担猛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏先改。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一仇奶、第九天 我趴在偏房一處隱蔽的房頂上張望比驻。 院中可真熱鬧,春花似錦嫁艇、人聲如沸弦撩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽感凤。三九已至悯周,卻和暖如春陪竿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背闰挡。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留长酗,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓之拨,卻偏偏與公主長得像咧叭,于是被迫代替她去往敵國和親蚀乔。 傳聞我的和親對象是個殘疾皇子佳簸,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容