專案開發過程中,有沒有很想定義一個全域性變數,作用域針對於單次 request請求,在整個請求過程中都可以隨時獲取。當使用feign、dubbo等做服務呼叫時,如果該變數的作用域還能傳遞到整個微服務鏈路,那就更好了。這就是本文想實現的效果,剛工作時基於 Oracle ADF 開發,就可以定義基於 RequestScope 作用域的變數。
在前面《微服務的全鏈路日誌(Sleuth+MDC)》文章中,我們實現了日誌的全鏈路,原理是基於 spring cloud sleuth
和 MDC
的框架來實現 traceId 等值的全程傳遞。本文算是姊妹篇,但一些實現的框架有所不同,本文是基於 spring 自帶的 RequestContextHolder
,和 servlet 的 HttpServletRequest
來實現的。
1. 單服務單執行緒實現
如果只希望實現單個服務內的作用域,而且整個API的邏輯內都是單執行緒,那麼最容易想到的方案就是 ThreadLocal
。定義一個 ThreadLocal 變數,在每一個API請求的時候賦值,在請求結束後清除,我們很多框架在AOP中處理這段邏輯。
但現在更容易,Spring框架自帶的 RequestContextHolder
天然支援這麼做。存放變數總要有提供 Getter/Setter
方法的容器吧,下面就介紹 HttpServletRequest
。
1.1. HttpServletRequest
HttpServletRequest 大家應該都不陌生,一次 API 請求中,所有客戶端的請求都被封裝成一個 HttpServletRequest 物件裡。這個物件在 API 請求時建立,響應完成後銷燬,就很適合作為 Request 作用域的容器。
1、Attribute 和 Header、Parameter
而往容器中投放和獲取變數的方法,則可以用 HttpServletRequest 物件的 setAttribute/getAttribute
方法來實現。如今大家可能都對 Attribute
比較陌生,它在早期 JSP 開發時用的比較多,用於 Servlet之間的值傳遞,不過用於當前場景也十分契合。
有人說那為啥不用 Header、Parameter 呢?它們也是 Request 作用域內的容器。簡單有兩點:
Header
、Parameter
設計之初就不是用於做服務端容器的,所以它們通常只能在客戶端賦值,在服務端 HttpServletRequest 也只提供了Getter
介面,而沒有Setter
介面。但Attribute
就同時提供了Getter/Setter
介面。Header
、Parameter
儲存物件的Value
都是String
字串,也是方便客戶端資料基於 HTTP 協議傳輸時方便。但Attribute
儲存物件的Value
是Object
,也就更適合存放各種型別的物件。
那麼在Web開發中,我們日常是如何獲取 HttpServletRequest 物件的呢?
2、獲取 HttpServletRequest 的三種方法
在 Controller 的方法引數上寫上 HttpServletRequest,這樣每次請求過來得到就是對應的 HttpServletRequest。當 Service 等其他層需要用到時,就從 Controller 開始層層傳遞。很明顯,保險,但程式碼看起來不太美觀。
@GetMapping("/req") public void req(HttpServletRequest request) {...}
使用 RequestContextHolder,直接在需要用的地方使用如下方式取HttpServletRequest即可:
public static HttpServletRequest getRequestByContext() { HttpServletRequest request = null; RequestAttributes ra = RequestContextHolder.getRequestAttributes(); if (ra instanceof ServletRequestAttributes) { ServletRequestAttributes sra = (ServletRequestAttributes) ra; request = sra.getRequest(); } return request; }
直接通過 @Autowired 獲取 HttpServletRequest。
@Autowired HttpServletRequest request;
其中,第2、第3種方式的原理是一致的。是因為 Spring框架在動態生成 HttpServletRequest Bean 的原始碼中,也是通過 RequestContextHolder.currentRequestAttributes() 來獲取值,從而可以通過 @Autowired 注入。
下面就詳細介紹一下 RequestContextHolder
。
1.2. RequestContextHolder
1、RequestContextHolder 工具類
我們先來看一下 RequestContextHolder 的原始碼:
public abstract class RequestContextHolder {
private static final boolean jsfPresent = ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");
public RequestContextHolder() {
}
public static void resetRequestAttributes() {
requestAttributesHolder.remove();
inheritableRequestAttributesHolder.remove();
}
public static void setRequestAttributes(@Nullable RequestAttributes attributes) {
setRequestAttributes(attributes, false);
}
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
} else if (inheritable) {
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();
} else {
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
@Nullable
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
if (attributes == null) {
attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
}
return attributes;
}
public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
RequestAttributes attributes = getRequestAttributes();
if (attributes == null) {
if (jsfPresent) {
attributes = RequestContextHolder.FacesRequestAttributesFactory.getFacesRequestAttributes();
}
if (attributes == null) {
throw new 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.");
}
}
return attributes;
}
private static class FacesRequestAttributesFactory {
private FacesRequestAttributesFactory() {
}
@Nullable
public static RequestAttributes getFacesRequestAttributes() {
FacesContext facesContext = FacesContext.getCurrentInstance();
return facesContext != null ? new FacesRequestAttributes(facesContext) : null;
}
}
}
可以關注到兩個重點:
RequestContextHolder
也是基於ThreadLocal
實現的,基於本地執行緒提供了Getter/Setter
方法,但如果跨執行緒則丟失變數值。RequestContextHolder
可以基於InheritableThreadLocal
實現,從而實現也可以從子執行緒中獲取當前執行緒的值。
這和上一篇文章中講的 MDC
很像。RequestContextHolder 的工具類很簡單,那麼 Spring 框架是在哪裡存放 RequestContextHolder 值,又在哪裡銷燬的呢?
2、Spring MVC 實現
我們看下 FrameworkServlet 這個類,裡面有個 processRequest 方法,根據方法名稱我們也可以大概瞭解到這個是方法用於處理請求的。
FrameworkServlet.java
protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = this.buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
this.initContextHolders(request, localeContext, requestAttributes);
try {
this.doService(request, response);
} catch (IOException | ServletException var16) {
failureCause = var16;
throw var16;
} catch (Throwable var17) {
failureCause = var17;
throw new NestedServletException("Request processing failed", var17);
} finally {
this.resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
this.logResult(request, response, (Throwable)failureCause, asyncManager);
this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
}
}
this.doService(request, response);
是執行具體的業務邏輯,而我們關注的兩個點則在這個方法的前後:
設定當前請求 RequestContextHolder 值,
this.initContextHolders(request, localeContext, requestAttributes);
對應方法程式碼如下:private void initContextHolders(HttpServletRequest request, @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) { if (localeContext != null) { LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable); } if (requestAttributes != null) { RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable); } }
當執行完成或丟擲異常,則需要重置 RequestContextHolder 值,即清除掉當前 RequestContextHolder 值,設定為以前的值,
this.resetContextHolders(request, previousLocaleContext, previousAttributes);
對應方法程式碼如下:private void resetContextHolders(HttpServletRequest request, @Nullable LocaleContext prevLocaleContext, @Nullable RequestAttributes previousAttributes) { LocaleContextHolder.setLocaleContext(prevLocaleContext, this.threadContextInheritable); RequestContextHolder.setRequestAttributes(previousAttributes, this.threadContextInheritable); }
2. 單服務多執行緒實現
單個服務內,當我們有多執行緒的開發,如果希望在子執行緒內依然可以通過 RequestContextHolder 來獲取 HttpServletRequest 該怎麼辦呢?
有人講,前面 RequestContextHolder 的工具類中,不就提供 InheritableThreadLocal 的實現方式嗎,不就可以實現需求了嘛。
這裡明確不建議使用 InheritableThreadLocal 的實現方式,其實在上一篇文章中,也就提到過不建議用 InheritableThreadLocal 實現 MDC 的多執行緒傳遞。這裡也一樣,建議還是用 執行緒池的裝飾器模式
來替代 InheritableThreadLocal
。下面做一下對比,說明原因。
1、InheritableThreadLocal 的侷限性
ThreadLocal 的侷限性,就是不能在父子執行緒之間傳遞。 即在子執行緒中無法訪問在父執行緒中設定的本地執行緒變數。 後來為了解決這個問題,引入了一個新的類 InheritableThreadLocal。
使用該方法後,子執行緒可以訪問在 建立子執行緒時 父執行緒當時的本地執行緒變數,其實現原理就是在父執行緒建立子執行緒時將父執行緒當前存在的本地執行緒變數拷貝到子執行緒的本地執行緒變數中。
大家關注上文中加粗的幾個字 “建立子執行緒時”,這就是 InheritableThreadLocal
的侷限性。
眾所周知,執行緒池的一大特點就是執行緒在建立後可回收,重複使用。這就意味著如果使用執行緒池建立執行緒,當使用 InheritableThreadLocal 時,只有新建立的執行緒可以正確的繼承父執行緒的值,而後續重複使用的執行緒則不會更新值。
2、執行緒池的裝飾模式
ThreadPoolTaskExecutor 類的 setTaskDecorator(TaskDecorator taskDecorator)
方法則沒有上述的問題,因為它本身不是和執行緒 Thread
掛鉤的,而是和 Runnable
掛鉤。方法的官方註釋是:
Specify a custom TaskDecorator to be applied to any Runnable about to be executed.
因此,對於想實現單服務多執行緒的傳遞時,建議仿照下列方式自定義執行緒池(還結合了 MDC的上下文繼承):
@Bean("customExecutor")
public Executor getAsyncExecutor() {
final RejectedExecutionHandler rejectedHandler = new ThreadPoolExecutor.CallerRunsPolicy() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
log.warn("LOG:執行緒池容量不夠,考慮增加執行緒數量,但更推薦將執行緒消耗數量大的程式使用單獨的執行緒池");
super.rejectedExecution(r, e);
}
};
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setCorePoolSize(7);
threadPoolTaskExecutor.setMaxPoolSize(42);
threadPoolTaskExecutor.setQueueCapacity(11);
threadPoolTaskExecutor.setRejectedExecutionHandler(rejectedHandler);
threadPoolTaskExecutor.setThreadNamePrefix("Custom Executor-");
threadPoolTaskExecutor.setTaskDecorator(runnable -> {
try {
Optional<RequestAttributes> requestAttributesOptional = ofNullable(RequestContextHolder.getRequestAttributes());
Optional<Map<String, String>> contextMapOptional = ofNullable(MDC.getCopyOfContextMap());
return () -> {
try {
requestAttributesOptional.ifPresent(RequestContextHolder::setRequestAttributes);
contextMapOptional.ifPresent(MDC::setContextMap);
runnable.run();
} finally {
MDC.clear();
RequestContextHolder.resetRequestAttributes();
}
};
} catch (Exception e) {
return runnable;
}
});
return threadPoolTaskExecutor;
}
3、sleuth 的 LazyTraceThreadPoolTaskExecutor 是否也會傳遞執行緒值
還記得上篇文章中 MDC 的子執行緒傳遞,當引入 sleuth 框架後,Spring 預設的執行緒池被替換為 LazyTraceThreadPoolTaskExecutor
。此時不需要做上述裝飾器的操作,預設執行緒池中的子執行緒就能繼承 MDC 中 traceId 等值。
那麼 LazyTraceThreadPoolTaskExecutor
能不能也讓子執行緒繼承父執行緒 RequestContextHolder 的值呢?
親身試驗過,不能!
3. 全鏈路多執行緒實現
全鏈路是針對微服務呼叫的場景,雖然原則上來講,HttpServletRequest
應該只針對單次服務的請求到響應。但是由於現在微服務的流行,一次服務請求的鏈路往往會橫跨多個服務。
基於上面的方法,我們是否可以實現請求作用域的變數,跨微服務傳播?
1、傳遞方式探討
但這裡就有個矛盾,我們前面拿 Attribute
和 Header、Parameter
做比較時就說過。前者適合在服務端容器內部傳遞值(Setter/Getter)。而後兩者應該在客戶端存放值(Setter),而在服務端獲取(Getter)。
所以我的理解是:如果需要實現服務間的資料傳遞,建議資料量小的字串可以通過Header
傳遞(如:traceId等)。實際的資料,還是應該通過常規的 API 引數 Parameter
或請求體 Body
傳遞。
2、Header 傳遞的例子
這邊有一個 Header 通過 Feign 攔截器傳遞的例子。Feign 支援自定義攔截器配置,可以在該配置類中讀取上一個請求的值,然後再塞到下一個請求中。
HelloFeign.java
@FeignClient(
name = "${hello-service.name}",
url = "${hello-service.url}",
path = "${hello-service.path}",
configuration = {FeignRequestInterceptor.class}
)
public interface HelloFeign { ... }
FeignRequestInterceptor.java
@ConditionalOnClass({RequestInterceptor.class})
public class FeignRequestInterceptor implements RequestInterceptor {
private static final String[] HEADER_KEYS = new String[]{"demo-key-1", "demo-key-2", "demo-key-3"};
@Override
public void apply(RequestTemplate requestTemplate) {
ofNullable(this.getRequestByContext())
.ifPresent(request -> {
for (int i = 0; i < HEADER_KEYS.length; i++) {
String key = HEADER_KEYS[i];
String value = request.getHeader(key);
if (!Objects.isNull(value)) {
requestTemplate.header(key, new String[]{value});
}
}
});
}
private HttpServletRequest getRequestByContext() {
HttpServletRequest request = null;
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
if (ra instanceof ServletRequestAttributes) {
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
request = sra.getRequest();
}
return request;
}
}