執行緒池中使用ThreadLocal方案
人工手打,翻譯自:https://moelholm.com/2017/07/24/spring-4-3-using-a-taskdecorator-to-copy-mdc-data-to-async-threads 本來想自己寫一篇關於執行緒池threadlocal的,偶然看到這篇文章覺得挺好的,便直接翻譯了
尊重外國人寫文章的習慣,如果你初次看到此類翻譯可能會造成不愉悅,但如果你曾經看到過,那你一定明白我在說什麼,有的地方加上我自己的理解和註釋
在這篇文章裡,我們將會演示如何從web執行緒裡複製MDC資料到@Async註解的執行緒裡,我們將會使用一個全新的 Spring Framework 4.3的特性: ThreadPoolTaskExecutor#setTaskDecorator() [set-task-decorator].
下面是最終結果:
注意到倒數第二行和第三行:在這個log級別上輸出了[userId:Duke],倒數第三行是在一個web執行緒裡(一個使用@RestController註解的類)發出的,倒數第二行是在一個用了@Async註解的非同步執行緒裡發出的。本質上,MDC資料從web執行緒中複製到了使用@Async註解的非同步執行緒裡中了(這就是最酷的部分,:smirk:)
繼續閱讀吧,少年,去看看這是怎麼實現的。這篇文章的所有程式碼都可以在GitGub上的示例中找到。如果有需要的話,可以去看看細節。
關於示例專案
這個示例專案基於Spring Boot 2。日誌API這裡用的是SLF4J和Logback(用了Logger, LoggerFactory和MDC) 如果你去看了那個示例專案,你將會發現這個@RestController註解的Controler
@RestController public class MessageRestController { private final Logger logger = LoggerFactory.getLogger(getClass()); private final MessageRepository messageRepository; MessageRestController(MessageRepository messageRepository) { this.messageRepository = messageRepository; } @GetMapping List<String> list() throws Exception { logger.info("RestController in action"); return messageRepository.findAll().get(); } }
注意到它輸出了日誌:RestController in action,同時注意到它有一個古怪的呼叫:messageRepository.findAll().get(),這是因為它執行了一個非同步的方法,接收了一個Future物件,並且呼叫了get()方法來等待結果返回,所以這是一個在web執行緒裡呼叫使用@Async註解的非同步方法。這是一個很顯然的人為的為了演示而寫的示例(我猜你在工作中的一些場景中會明智的呼叫此類非同步方法)
下面是那個repository類:
@Repository
class MessageRepository {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Async
Future<List<String>> findAll() {
logger.info("Repository in action");
return new AsyncResult<>(Arrays.asList("Hello World", "Spring Boot is awesome"));
}
}
注意到findAll方法裡列印了日誌:Repository in action。
為了完整起見,讓我向你展示如何在web執行緒裡設定MDC資料的:
@Component
public class MdcFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
try {
MDC.put("mdcData", "[userId:Duke]");
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
如果我們什麼也不做,我們可以在web執行緒裡很輕鬆的拿到正確配置的MDC資料,但是當一個web請求進入了@Async註解的非同步方法呼叫裡,我們卻不能跟蹤它:MDC資料裡的ThreadLocal資料不會簡單的自動複製過來,好訊息是這個超級簡單解決
解決方案第一步: 配置@Async執行緒池
首先,定製化你的非同步功能,我是這樣做的:
@EnableAsync(proxyTargetClass = true)
@SpringBootApplication
public class Application extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(new MdcTaskDecorator());
executor.initialize();
return executor;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
有意思的地方是我們擴充套件了AsyncConfigurerSupport,好讓我們可以自定義執行緒池
更精確的說:祕密在於executor.setTaskDecorator(new MdcTaskDecorator())。就是這行程式碼使我們可以自定義TaskDecorator
解決方案第二步: 實現TaskDecorator
現在到了說明自定義的TaskDecorator:
class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
// Right now: Web thread context !
// (Grab the current thread MDC data)
Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
// Right now: @Async thread context !
// (Restore the Web thread context's MDC data)
MDC.setContextMap(contextMap);
runnable.run();
} finally {
MDC.clear();
}
};
}
}
decorate()方法的引數是一個Runnable物件,返回結果也是另一個Runnable物件
這裡,我只是把原始的Runnable物件包裝了一下,首先取得MDC資料,然後把它放到了委託的run方法裡(Here, I basically wrap the original Runnable and maintain the MDC data around a delegation to its run() method.英文原文是這樣,太難翻譯了,囧)
總結
從web執行緒裡複製MDC資料到非同步執行緒是如此的容易,這裡展示的技巧不侷限於複製MDC資料,你也可以使用它來複制其他ThreadLocal資料(MDC內部就是使用ThreadLocal),或者你可以使用TaskDecorator做一些其他完全不同的事情:記錄日誌,度量方法執行的時間,吞掉異常,退出JVM等等,只要你喜歡
牆裂感謝Joris Kuipers (@jkuipers)提醒我這個牛逼的Spring Framework 4.3新功能, An awesome tip :hugging:(這一句怎麼翻譯?).
參考
[set-task-decorator] ThreadPoolTaskExecutor#setTaskDecorator() (Spring’s JavaDoc) https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.html#setTaskDecorator-org.springframework.core.task.TaskDecorator-
以下自己的總結:
- 使用ThreadLocal,不會在子執行緒中(包括new Thread和new執行緒池)獲取到
- 使用InheritableThreadLocal,可以在子執行緒中(包括new Thread和new執行緒池)獲取到,但是如果用的是執行緒池,一般不會每次使用的時候重新建立,而他的賦值只能在首次建立的時候可以(Thread類的inheritableThreadLocals變數),後面執行緒池中的執行緒重複使用時,一開始賦值的那個變數將會一直存在,你可能會得到錯誤的結果或者理解為這也是一種記憶體洩漏
- 在spring中,一般通過xml或者@Configuration來配置執行緒池,那麼在專案啟動的時候,執行緒池就完成建立了,根本沒有機會給你設定變數,所以最佳實踐就是,寫一個代理類,線上程池提交任務的時候(execute和submit方法),把當前執行緒的threadlocal變數儲存起來,重寫run方法或者call方法,並且在呼叫實際的run方法前,儲存在剛才儲存起來的變數,一般也是放到threadlocal裡面,這樣在實際的run方法裡,就可以方便的通過threadlocal獲取到了
- 除了上述的方法,ali提供了一個transmittable-thread-local,原理就是上面3所講的,不過個人覺得它實現有點繞,用起來還算簡單,可以用下
關於threadlocal的程式碼細節,見我的另外一篇文章:再看ThreadLocal