多線程調(diào)用如何傳遞請求上下文?簡述ThreadLocal和TaskDecorator

背景

微服務(wù)應(yīng)用大多對外提供RESTful API,需要有相應(yīng)的token才能訪問,我們在聚合服務(wù)中使用Feign Client調(diào)用這些API,順序執(zhí)行往往會(huì)浪費(fèi)大量的IO等待時(shí)間,為了提高查詢速度,我們會(huì)使用異步調(diào)用,Java 8引入了CompletableFuture,結(jié)合Executor框架大大簡化了異步編程的復(fù)雜性。

問題描述

我們的服務(wù)使用Spring Security OAuth2授權(quán),并通過JWT傳遞token,對于用戶側(cè)的請求一律使用用戶token,后端服務(wù)間的調(diào)用使用系統(tǒng)token。但有時(shí)為了簡化開發(fā)(如用戶信息傳遞),服務(wù)間調(diào)用仍然傳遞用戶token。這就帶來一個(gè)問題,在異步請求中子線程會(huì)丟失相應(yīng)的認(rèn)證信息,錯(cuò)誤如下:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.oauth2ClientContext': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; 
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:365)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
    at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:192)
    at com.sun.proxy.$Proxy170.getAccessToken(Unknown Source)
    at org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor.getToken(OAuth2FeignRequestInterceptor.java:126)
    at org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor.extract(OAuth2FeignRequestInterceptor.java:115)
    at org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor.apply(OAuth2FeignRequestInterceptor.java:104)
    at feign.SynchronousMethodHandler.targetRequest(SynchronousMethodHandler.java:169)
    at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:99)
    at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78)
...
Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
    at org.springframework.web.context.request.AbstractRequestAttributesScope.get(AbstractRequestAttributesScope.java:42)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:353)
    ... 76 common frames omitted

問題暴露出來了,該怎么解決呢?在這之前我們先了解兩個(gè)概念,線程的ThreadLocal和TaskDecorator。

ThreadLocal<T>的原理

首先,在Thread類中定義了一個(gè)threadLocals,它是ThreadLocal.ThreadLocalMap對象的引用,默認(rèn)值是null。ThreadLocal.ThreadLocalMap對象表示了一個(gè)以開放地址形式的散列表。當(dāng)我們在線程的run方法中第一次調(diào)用ThreadLocal對象的get方法時(shí),會(huì)為當(dāng)前線程創(chuàng)建一個(gè)ThreadLocalMap對象。也就是每個(gè)線程都各自有一張獨(dú)立的散列表,以ThreadLocal對象作為散列表的key,set方法中的值作為value(第一次調(diào)用get方法時(shí),以initialValue方法的返回值作為value)。顯然我們可以定義多個(gè)ThreadLocal對象,而我們一般將ThreadLocal對象定義為static類型或者外部類中。上面所表達(dá)的意思就是,相同的key在不同的散列表中的值必然是獨(dú)立的,每個(gè)線程都是在各自的散列表中執(zhí)行操作。


ThreadLocal存儲(chǔ)

TaskDecorator簡介

先看官方api文檔說明

public interface TaskDecorator
A callback interface for a decorator to be applied to any Runnable about to be executed.
Note that such a decorator is not necessarily being applied to the user-supplied Runnable/Callable but rather to the actual execution callback (which may be a wrapper around the user-supplied task).
The primary use case is to set some execution context around the task's invocation, or to provide some monitoring/statistics for task execution.

意思就是說這是一個(gè)執(zhí)行回調(diào)方法的裝飾器,主要應(yīng)用于傳遞上下文,或者提供任務(wù)的監(jiān)控/統(tǒng)計(jì)信息。看上去正好可以應(yīng)用于我們這種場景。

解決方案

上文中的錯(cuò)誤信息涉及到RequestAttributes
和SecurityContext,他們都是通過ThreadLocal來保存線程數(shù)據(jù),在同步方法中沒有問題,使用線程池異步調(diào)用時(shí),我們可以通過配合線程池的TaskDecorator裝飾器拷貝上下文傳遞。

注意 線程池中的線程是可復(fù)用的,使用ThreadLocal需要注意內(nèi)存泄露問題,所以線程執(zhí)行完成后需要在finally方法中移除上下文對象。

代碼如下

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import javax.annotation.Nonnull;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Bean("ttlExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 設(shè)置線程池核心容量
        executor.setCorePoolSize(20);
        // 設(shè)置線程池最大容量
        executor.setMaxPoolSize(100);
        // 設(shè)置任務(wù)隊(duì)列長度
        executor.setQueueCapacity(200);
        // 設(shè)置線程超時(shí)時(shí)間
        executor.setKeepAliveSeconds(60);
        // 設(shè)置線程名稱前綴
        executor.setThreadNamePrefix("ttl-executor-");
        // 設(shè)置任務(wù)丟棄后的處理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 設(shè)置任務(wù)的裝飾
        executor.setTaskDecorator(new ContextCopyingDecorator());
        executor.initialize();
        return executor;
    }

    static class ContextCopyingDecorator implements TaskDecorator {
        @Nonnull
        @Override
        public Runnable decorate(@Nonnull Runnable runnable) {
            RequestAttributes context = RequestContextHolder.currentRequestAttributes();
            SecurityContext securityContext = SecurityContextHolder.getContext();
            return () -> {
                try {
                    RequestContextHolder.setRequestAttributes(context);
                    SecurityContextHolder.setContext(securityContext);
                    runnable.run();
                } finally {
                    SecurityContextHolder.clearContext();
                    RequestContextHolder.resetRequestAttributes();
                }
            };
        }
    }
}

擴(kuò)展知識

Spring安全策略可見性分為三個(gè)層級:

  1. MODE_THREADLOCAL 僅當(dāng)前線程(默認(rèn))
  2. MODE_INHERITABLETHREADLOCAL 子線程可見
  3. MODE_GLOBAL 全局可見

可通過啟動(dòng)項(xiàng)參數(shù)進(jìn)行設(shè)置

-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

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