日々常々

ふつうのプログラマがあたりまえにしたいこと。

SpringBootでのRestTemplateのタイムアウト設定

  • spring-boot:2.1.7.RELEASE
  • spring-web:5.1.9.RELEASE

Short Answer

RestTemplateBuilder restTemplateBuilder = ...

RestTemplate restTemplate = restTemplateBuilder
                .setConnectTimeout(Duration.ofSeconds(3))
                .setReadTimeout(Duration.ofSeconds(3))
                .build();

タイムアウトが設定できない時は実行時例外が出ます。

以下は中身に興味のある人向け。

登場人物

f:id:irof:20190817195852p:plain

RestTemplateとかRestTemplateBuilderとか

RestTemplateは実際の通信をClientHttpRequestFactoryに任せます。 この子はインタフェースなので実際の通信は実装クラスがすることになります。 そのため通信に関わることは ClientRequestFactoryの実装が使用するHTTPクライアントに依存することになります。 今回のタイムアウトなどはこのHTTPクライアントが担うものになります。

RestTemplateBuilderはSpringBoot1.4で導入されたRestTemplate設定用のクラスです。 RestTemplateBuilderがあるのならRestTemplateへの設定は全部任せたいところですが、タイムアウトは前述のようにRestTemplateに対する設定ではないのです。 これどうやってんだっけ、ってのがこれ書いた動機。

タイムアウトを実際に設定するところ

タイムアウトはHTTP通信に依存したものという判断か、RestTemplateにはタイムアウトを設定するようなメソッドはありません。 そのためClientRequestFactoryの実装か、使用されるHTTPクライアントに設定することになります。

デフォルトで使用されるSimpleClientHttpRequestFactorysetConnectTimeout/setReadTimeoutを持っており、これらの値がHttpURLConnectionに渡されます。他のクライアントでも同様だと思います。

という事で、タイムアウトを設定したClientRequestFactoryを使用するようにRestTemplateに設定します。これにはRestTemplateのコンストラクタかsetRequestFactoryメソッドを使用します。

SpringBootを使用しない場合はこんな感じ。

SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(3_000);
requestFactory.setReadTimeout(3_000);
RestTemplate restTemplate = new RestTemplate(requestFactory);

SpringBootを使用する場合も最終的に前述の設定をする事には変わりありません。なのでRestTemplateBuilderがよくわからなくても次のような方法でいいといえばいいわけです。

  • いままでどおりやる。
  • buildしたRestTemplateに自分で生成したClientHttpRequestFactorysetRequestFactoryする。

でもタイムアウトを設定したいのであってSimpleClientHttpRequestFactoryを使いたいわけではないわけで。

SpringBootでのClientHttpRequestFactoryの決まり方

という事でClientRequestFactoryの実装にタイムアウトを設定したいのですが、まずはClientHttpRequestFactoryがどう決まるかを見ていきましょう。

RestTemplateBuilderRestTemplateに対して次のようにClientHttpRequestFactoryを設定しています。(ソース

  • Supplier<ClientHttpRequestFactory>が指定されていればそれを使用してClientHttpRequestFactoryを生成する。
  • detectRequestFactoryが有効ならClientHttpRequestFactorySupplierを使用してClientHttpRequestFactoryを生成する。
  • どちらも無効なら何もしない。(RestTemplateのデフォルトになる。)

RestTemplateBuilderはクラスパスにあるHTTPクライアントを自動で使用するようになっており、SpringBoot2.1.7では ApacheHttpClient -> OkHttp3 -> JDK となっているようです。これはClientHttpRequestFactorySupplierで実現されます。(ソース

先に「SimpleClientHttpRequestFactoryClientHttpRequestFactoryのデフォルトになる」というような事を書きましたが、SpringBootを使わない場合はRestTemplate(というか基底のHttpAccessor)のデフォルトであり、SpringBootを使う場合はRestTemplateBuilderClientHttpRequestFactorySupplierのデフォルトであります。場合によってはSpringBootを使う場合と使わない場合でClientHttpRequestFactoryのデフォルトが違うなんてことも起こらないとは言わない。

タイムアウトを設定したClientHttpRequestFactoryインスタンスを返すSupplier<ClientHttpRequestFactory>を使うようにすればRestTemplatesetRequestFactoryをしなくてもよくなります。これでもいいですね。

RestTemplateBuilderでのタイムアウト設定

RestTemplateBuilderを使用するならClientRequestFactoryの実装を気にせずタイムアウトを設定したいものです。 具体的にはSimpleClientHttpRequestFactoryのような実装クラスを目にしたくない。いや別に恨みがあるとかではないですが。

ここで冒頭の設定方法にようやく辿りつきます。長かった。

RestTemplateBuildersetConnectTimeout/setReadTimeoutClientRequestFactoryインスタンスに対するリフレクションで実現されています。(ソース) 該当するメソッドがない場合は実行時に例外が出るので安心ですね。 安心じゃねーよ。とはいえ無視されてタイムアウト効かずに動くよりはマシか……

しかしこれなんでClientRequestFactoryにメソッド追加……は他の実装に配慮した(defaultメソッドはJava7以前の互換で使えないし)として、新しいインタフェース作ったりしなかったんだろう。まあインタフェースにしても実行時例外には変わらないけど。

寄り道: MockRestServiceServerの話

spring-testMockRestServiceServerRestTemplateClientHttpRequestFactoryを差し替えることで実現されています。 つまり、ClientHttpRequestFactoryの実装に依存するテストは行えません。 こちらを動作させたい場合はMockServerなどのテストダブルをHTTPの先に置くことになります。

spring-boot-test@AutoConfigureMockRestServiceServer を使うと RestTemplateBuilder で生成された RestTemplate に自動でバインドされます。これは RestTemplateCustomizerの実装であるMockServerRestTemplateCustomizerRestTemplateBuilderに設定されるから。便利なんですが、RestTemplateBuilderで複数のRestTemplateを作ってるとちょっと使いづらくなるのが残念なところ。

長々書いたけど

実際のとこRestTemplateBuilderに対してIDE経由でお伺いをたてたら一瞬で見つかります。

f:id:irof:20190817193315p:plain

あと、最新の master だとここで見たコードも結構変わってたりてて面白かった。