Spring RestTemplate 設(shè)置每次請求的 Timeout

前言

在實現(xiàn)這個功能之前赞庶,我也上網(wǎng)搜索了一下方案。大多數(shù)的解決方法都是定義多個 RestTemplate 設(shè)置不同的超時時間。有沒有更好的方式呢黎棠?帶著這個問題晋渺,我們一起來深入一下 RestTemplate 的源碼

提示:本文包含了大量的源碼分析,如果想直接看筆者是如何實現(xiàn)的脓斩,直接跳到最后的改造思路

版本

SpringBoot:2.3.4.RELEASE

RestTemplate

RestTemplate#doExecute

doExecute

RestTemplate 發(fā)送請求的方法木西,隨便找一個最后都會走到上圖的 doExecute。

從上圖來看随静,這個方法做的就是這幾件事

  1. createRequest
  2. 執(zhí)行 RequestCallback
  3. 執(zhí)行 Request
  4. 處理響應八千,將響應轉(zhuǎn)換成用戶聲明的類型

RequestCallback 做了什么

  1. 根據(jù) RestTemplate 中的定義 HttpMessageConverter 填充 Header Accept(支持的響應類型)
  2. 通過 HttpMessageConverter 轉(zhuǎn)換 HttpBody

這里我們需要重點關(guān)注的是,createRequest 和 執(zhí)行 Request 部分

createRequest

RestTemplate 中的 Request 是由 RequestFactory 完成創(chuàng)建燎猛。所以我們先來看下獲取 RequestFactory 的邏輯

getRequestFactory

如果 RestTemplate 配置了 ClientHttpRequestInterceptor(攔截器)的話恋捆,則創(chuàng)建 InterceptingClientHttpRequestFactory,反之則直接獲取 RequestFactory

  1. 我們可以通過 RestTemplate#setInterceptors 手動添加攔截器扛门;
  2. 當使用 @LoadBalanced 標記 RestTemplate 時鸠信,RestTemplate 中也會被加入攔截器,具體原理可以參考 org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration

我們先來看下 InterceptingClientHttpRequestFactory 是什么邏輯

InterceptingClientHttpRequestFactory

InterceptingClientHttpRequestFactory#createRequest

createRequest 方法直接返回了 InterceptingClientHttpRequest论寨,參考 doExecute 的邏輯星立,接下來會執(zhí)行 InterceptingClientHttpRequest#execute,其內(nèi)部會執(zhí)行到 InterceptingRequestExecution#execute

InterceptingRequestExecution#execute

這里隨便找一個攔截器的實現(xiàn)配合著來看

攔截器實現(xiàn)

邏輯梳理一下:

  1. InterceptingRequestExecution 會先去執(zhí)行所有的攔截器
  2. 攔截器在執(zhí)行完邏輯之后葬凳,再次 InterceptingRequestExecution#execute绰垂。InterceptingRequestExecution 再次調(diào)用下一個攔截器
  3. 在攔截器邏輯執(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é)果囊咏。

改造思路

我們可以開始進行改造了,思路如下

  1. 默認的超時時間等屬性塔橡,我們可以通過 HttpComponentsClientHttpRequestFactory#setHttpClient 或者 HttpComponentsClientHttpRequestFactory#setReadTimeout 來決定
  2. 在需要自定義 RequsetConfig 的場景梅割,將 RequsetConfig 存儲在 ThreadLocal 中
  3. 我們自定義的 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)注或者喜歡,你的支持是對我最大的幫助

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弹砚,一起剝皮案震驚了整個濱河市双仍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌桌吃,老刑警劉巖朱沃,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異茅诱,居然都是意外死亡逗物,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門瑟俭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翎卓,“玉大人,你說我怎么就攤上這事尔当×觯” “怎么了?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵椭迎,是天一觀的道長锐帜。 經(jīng)常有香客問我,道長畜号,這世上最難降的妖魔是什么缴阎? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮简软,結(jié)果婚禮上蛮拔,老公的妹妹穿的比我還像新娘述暂。我一直安慰自己,他們只是感情好建炫,可當我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布畦韭。 她就那樣靜靜地躺著,像睡著了一般肛跌。 火紅的嫁衣襯著肌膚如雪艺配。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天衍慎,我揣著相機與錄音转唉,去河邊找鬼。 笑死稳捆,一個胖子當著我的面吹牛赠法,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播乔夯,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼砖织,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了驯嘱?” 一聲冷哼從身側(cè)響起镶苞,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鞠评,沒想到半個月后茂蚓,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡剃幌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年聋涨,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片负乡。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡牍白,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出抖棘,到底是詐尸還是另有隱情茂腥,我是刑警寧澤,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布切省,位于F島的核電站最岗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏朝捆。R本人自食惡果不足惜般渡,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧驯用,春花似錦脸秽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至薇正,卻和暖如春剥扣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背铝穷。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留佳魔,地道東北人曙聂。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像鞠鲜,于是被迫代替她去往敵國和親宁脊。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,697評論 2 351

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