背景
微服務(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í)行操作。
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è)層級:
- MODE_THREADLOCAL 僅當(dāng)前線程(默認(rèn))
- MODE_INHERITABLETHREADLOCAL 子線程可見
- MODE_GLOBAL 全局可見
可通過啟動(dòng)項(xiàng)參數(shù)進(jìn)行設(shè)置
-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL