前言
在實現(xiàn)這個功能之前赞庶,我也上網(wǎng)搜索了一下方案。大多數(shù)的解決方法都是定義多個 RestTemplate 設(shè)置不同的超時時間。有沒有更好的方式呢黎棠?帶著這個問題晋渺,我們一起來深入一下 RestTemplate 的源碼
提示:本文包含了大量的源碼分析,如果想直接看筆者是如何實現(xiàn)的脓斩,直接跳到最后的改造思路
版本
SpringBoot:2.3.4.RELEASE
RestTemplate
RestTemplate#doExecute
RestTemplate 發(fā)送請求的方法木西,隨便找一個最后都會走到上圖的 doExecute。
從上圖來看随静,這個方法做的就是這幾件事
- createRequest
- 執(zhí)行 RequestCallback
- 執(zhí)行 Request
- 處理響應八千,將響應轉(zhuǎn)換成用戶聲明的類型
RequestCallback 做了什么
- 根據(jù) RestTemplate 中的定義 HttpMessageConverter 填充 Header Accept(支持的響應類型)
- 通過 HttpMessageConverter 轉(zhuǎn)換 HttpBody
這里我們需要重點關(guān)注的是,createRequest 和 執(zhí)行 Request 部分
createRequest
RestTemplate 中的 Request 是由 RequestFactory 完成創(chuàng)建燎猛。所以我們先來看下獲取 RequestFactory 的邏輯
如果 RestTemplate 配置了 ClientHttpRequestInterceptor(攔截器)的話恋捆,則創(chuàng)建 InterceptingClientHttpRequestFactory,反之則直接獲取 RequestFactory
- 我們可以通過 RestTemplate#setInterceptors 手動添加攔截器扛门;
- 當使用 @LoadBalanced 標記 RestTemplate 時鸠信,RestTemplate 中也會被加入攔截器,具體原理可以參考
org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration
我們先來看下 InterceptingClientHttpRequestFactory 是什么邏輯
InterceptingClientHttpRequestFactory
createRequest 方法直接返回了 InterceptingClientHttpRequest论寨,參考 doExecute 的邏輯星立,接下來會執(zhí)行 InterceptingClientHttpRequest#execute
,其內(nèi)部會執(zhí)行到 InterceptingRequestExecution#execute
這里隨便找一個攔截器的實現(xiàn)配合著來看
邏輯梳理一下:
- InterceptingRequestExecution 會先去執(zhí)行所有的攔截器
- 攔截器在執(zhí)行完邏輯之后葬凳,再次
InterceptingRequestExecution#execute
绰垂。InterceptingRequestExecution 再次調(diào)用下一個攔截器 - 在攔截器邏輯執(zhí)行完之后,會去調(diào)用真正的 RequestFactory 創(chuàng)建請求火焰,然后執(zhí)行請求
在閱讀完 InterceptingRequestExecution#execute 的代碼之后劲装,我們可以發(fā)現(xiàn)。這里僅僅是將 request 的 uri昌简,method占业,header,body 復制到了 delegate 中纯赎。說明攔截器只能對這些屬性進行處理谦疾,并不能在攔截器層面添加 timeout 的相關(guān)處理。
默認情況的 RequestFactory
默認情況下 RestTemplate 會使用 SimpleClientHttpRequestFactory 來創(chuàng)建請求犬金,我們也可以在這個類中看到 setReadTimeout
方法念恍。但是 SimpleClientHttpRequestFactory 并沒有提供可以拓展的點,只能設(shè)置一個針對所有請求的超時時間晚顷。感興趣的同學可以自己閱讀下源碼峰伙,這里就不貼出來了
HttpComponentsClientHttpRequestFactory
在閱讀 HttpComponentsClientHttpRequestFactory 時,發(fā)現(xiàn)了可以擴展的地方该默。每次在創(chuàng)建 Request 的時候瞳氓,都需要在 HttpContext 這個類中設(shè)置 RequestConfig,使用過 apache http client 的同學可能知道 RequestConfig 這個類栓袖,這個類包含了大量的屬性可以定義請求的行為顿膨,這其中有一個屬性 socketTimeout
正是我們所需要的锅锨。
這個類中我們可以擴展的地方就在 createHttpContext
方法中
默認情況下 createHttpContext
返回 null,然后會嘗試從 HttpUriRequest 和 HttpClient 中獲取 RequestConfig 賦值到 HttpContext 中恋沃。
createHttpContext 這個方法我們也來看一下
@Nullable
private BiFunction<HttpMethod, URI, HttpContext> httpContextFactory;
/**
* Configure a factory to pre-create the {@link HttpContext} for each request.
* <p>This may be useful for example in mutual TLS authentication where a
* different {@code RestTemplate} for each client certificate such that
* all calls made through a given {@code RestTemplate} instance as associated
* for the same client identity. {@link HttpClientContext#setUserToken(Object)}
* can be used to specify a fixed user token for all requests.
* @param httpContextFactory the context factory to use
* @since 5.2.7
*/
public void setHttpContextFactory(BiFunction<HttpMethod, URI, HttpContext> httpContextFactory) {
this.httpContextFactory = httpContextFactory;
}
/**
* Template methods that creates a {@link HttpContext} for the given HTTP method and URI.
* <p>The default implementation returns {@code null}.
* @param httpMethod the HTTP method
* @param uri the URI
* @return the http context
*/
@Nullable
protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
return (this.httpContextFactory != null ? this.httpContextFactory.apply(httpMethod, uri) : null);
}
至此,已經(jīng)很清晰了必指。我們可以通過調(diào)用 setHttpContextFactory
來改變 createHttpContext
的結(jié)果囊咏。
改造思路
我們可以開始進行改造了,思路如下
- 默認的超時時間等屬性塔橡,我們可以通過
HttpComponentsClientHttpRequestFactory#setHttpClient
或者HttpComponentsClientHttpRequestFactory#setReadTimeout
來決定 - 在需要自定義 RequsetConfig 的場景梅割,將 RequsetConfig 存儲在 ThreadLocal 中
- 我們自定義的 HttpContextFactory 在讀取到 ThreadLocal 中的 RequsetConfig 后,會生成一個 HttpContext葛家,其他情況返回 null(走原來的邏輯)
代碼如下
public class CustomHttpContextFactory implements BiFunction<HttpMethod, URI, HttpContext> {
@Override
public HttpContext apply(HttpMethod httpMethod, URI uri) {
RequestConfig requestConfig = RequestConfigHolder.get();
if (requestConfig != null) {
HttpContext context = HttpClientContext.create();
context.setAttribute(HttpClientContext.REQUEST_CONFIG, requestConfig);
return context;
}
return null;
}
}
public class RequestConfigHolder {
private static final ThreadLocal<RequestConfig> threadLocal = new ThreadLocal<>();
public static void bind(RequestConfig requestConfig) {
threadLocal.set(requestConfig);
}
public static RequestConfig get() {
return threadLocal.get();
}
public static void clear() {
threadLocal.remove();
}
}
配置類
@Configuration
public class RestTemplateConfiguration {
@Bean("customTimeoutRestTemplate")
public RestTemplate customTimeout() {
RestTemplate restTemplate = new RestTemplate();
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setHttpContextFactory(new CustomHttpContextFactory());
requestFactory.setReadTimeout(3000);
restTemplate.setRequestFactory(requestFactory);
return restTemplate;
}
}
使用案例
@GetMapping("custom/setTimeout")
public String customSetTimeout(Integer timeout) {
RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(timeout).build();
try {
RequestConfigHolder.bind(requestConfig);
customTimeoutRestTemplate.getForObject("https://www.baidu.com", String.class);
} finally {
RequestConfigHolder.clear();
}
return "OK";
}
思路就是這樣户辞,可以將這個使用方式封裝為 注解 + AOP,這樣用起來會更簡單癞谒。
Demo
本文完整 demo:https://github.com/TavenYin/taven-springboot-learning/tree/master/springboot-restTemplate
最后
如果覺得我的文章對你有幫助底燎,動動小手點下關(guān)注或者喜歡,你的支持是對我最大的幫助