RestTemplate非推奨化に備えよう!RestClient入門、そしてリニューアルされたSpring Retryでリトライして、WireMockでテストする。 #jjug_ccc

-- Views

May 30, 26

スライド概要

JJUG CCC 2026 Springの資料です。

profile-image

Java、Spring、IntelliJ IDEA

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

ダウンロード

関連スライド

各ページのテキスト
1.

RestTemplate非推奨化に備えよう! RestClient入門、 そしてリニューアルされた Spring Retryでリトライして、 WireMockでテストする。 JJUG CCC 2026 Spring 2026年5月30日 多田真敏 1

2.

このセッションについて ◆RestTemplateは、Springで外部Web APIにアクセスする際に よく使われてきたクラスです。 ◆しかしSpring公式から、RestTemplateはSpring 7.1で非推奨化 ・Spring 8.0で削除されると発表されました。 ◆このセッションでは、RestTemplateの代替であるRestClientの使い方を 紹介します。 ◆加えて、Spring Framework 7でリニューアルされたSpring Retryを利用して、 RestClientと組み合わせてHTTPリクエストのリトライする方法を紹介します。 ◆最後に、WireMockでモックサーバーを作成して、RestClientやリトライを どのようにテストするかご紹介します。 2

3.

必要な前提知識 ◆このセッションを理解するには、以下の前提知識が必要です ◆Spring DI・Spring MVCの利用経験 3

4.

前提知識を学べる資料 ハッシュタグ 書きました! #つくまなBoot 6月発売予定 4

5.

自己紹介 ◆多田真敏(@suke_masa) ◆JJUG・JSUGスタッフ ◆クレジットカード会社で 社内システムの内製化+AWS化 ◆OSSドキュメントの和訳 ◆Thymeleaf・Resilience4j ◆櫻坂46、北海道日本ハムファイターズ、 DB.スターマン、ゴジラ、英語学習 5

6.

対象バージョン ◆JDK 25 ◆Spring Boot 4.1 バージョンが異なる場合、 この資料の説明が正しくない 可能性があります ◆Spring Boot利用前提で解説しています ◆Spring Bootを利用していない場合、 この資料では解説していない設定を追加する必要があります 6

7.

目次 ① RestClientの使い方 ② Spring Retryによるリトライ ③ WireMockを利用したRestClientのテスト ④ お知らせ 7

8.

目次 ① RestClientの使い方 ② Spring Retryによるリトライ ③ WireMockを利用したRestClientのテスト ④ お知らせ 8

9.

質問:HTTPクライアントは何を使ってますか? ◆RestTemplate? ◆RestClient? ◆WebClient? ◆その他? 9

10.

RestTemplateは将来的に非推奨+削除 Spring 7.1で非推奨化 Spring 8で削除 https://spring.io/blog/2025/09/30/the-state-of-http-clients-in-spring 10

11.

サンプルWeb API ◆複数件検索 GET /api/todos?keyword=a 200 OK [{ "id": 2, "description": "Example 2", "completed": false, "deadline": "2025-10-02T12:00:00", "createdAt": "2025-09-02T12:00:00" }, { "id": 1, "description": "Example 1", "completed": true, "deadline": "2025-10-01T12:00:00", "createdAt": "2025-09-01T12:00:00" }] ◆単一検索 GET /api/todos/1 200 OK { "id": 1, "description": "Example 1", "completed": true, "deadline": "2025-10-01T12:00:00", "createdAt": "2025-09-01T12:00:00" } 11

12.

サンプルWeb API ◆追加 ◆更新 POST /api/todos { "description": "Example 1", "deadline": "2025-10-01T12:00:00", } PUT /api/todos/1 { "description": "Example 1", "completed": true, "deadline": "2025-10-01T12:00:00", } 201 Created Location: /api/todos/4 200 OK ◆完了 ◆削除 PATCH /api/todos/1/done DELETE /api/todos/1 200 OK 204 No Content 12

13.
[beta]
RestClientの生成
@Component
public class TodoClient {
private final RestClient restClient;

ビルダーが
Auto Configurationで
Bean定義済み
(スコープはprototype)

public RestClientTodoClient(
RestClient.Builder restClientBuilder,
@Value("${todo-service.base-url}") String baseUrl
) {
this.restClient = restClientBuilder.baseUrl(baseUrl)
.build();
}

13

14.

application.properties # 接続タイムアウト spring.http.clients.connect-timeout=1s # 読み取りタイムアウト spring.http.clients.read-timeout=1s # ClientHttpRequestFactory実装(後述) spring.http.clients.imperative.factory=jdk # リダイレクトに付いていくか否か spring.http.clients.redirects=follow # Cookieの扱い方法 spring.http.clients.cookie-handling=enable # 利用するSSLバンドル spring.http.clients.ssl.bundle=xxx 特にタイムアウト設定は 忘れずに 14

15.

GET(単一検索) public TodoResponse getById(Integer id) { TodoResponse todoResponse = restClient.get() .uri("/api/todos/" + id) .retrieve() .body(TodoResponse.class); return todoResponse; } 15

16.
[beta]
GET(複数件検索)
public List<TodoResponse> getByKeyword(String keyword) {
List<TodoResponse> todoResponseList = restClient.get()
.uri("/api/todos?keyword=" + keyword)
.retrieve()
.body(new ParameterizedTypeReference<>(){});
return todoResponseList;
}

16

17.
[beta]
POST
public URI register(TodoRequest request) {
ResponseEntity<Void> responseEntity = restClient.post()
.uri("/api/todos")
.body(request)
.retrieve()
.toBodilessEntity();
URI location = responseEntity.getHeaders().getLocation();
return location;
}

17

18.

PUT public void update(Integer id, TodoRequest request) { restClient.put() .uri("/api/todos/" + id) .body(request) .retrieve() .toBodilessEntity(); } 18

19.

DELETE public void delete(Integer id) { restClient.delete() .uri("/api/todos/" + id) .retrieve() .toBodilessEntity(); } 19

20.

PATCH public void patch(Integer id) { restClient.patch() .uri("/api/todos/" + id + "/done") .retrieve() .toBodilessEntity(); } 20

21.
[beta]
レスポンスが4xx・5xxの場合
◆デフォルトでは即座に例外

◆各ステータスコードでの挙動はdefaultStatusHandler()で
変更可能
// 4xx・5xxでも例外を発生させない例
this.restClient = restClientBuilder.baseUrl(baseUrl)
.defaultStatusHandler(
status -> true,
(request, response) -> {
// 何もしない
}
).build();
21

22.

RestClientで発生する例外 タイムアウト等 RestClientException ResourceAccess Exception RestClientResponse Exception HttpStatusCodeException 4xx 例外 HttpClientErrorException NotFound TooMany Requests ・・・ 5xx 例外 UnknownContentType Exception UnknownHttpStatusCodeException HttpServerErrorException InternalServerError ・・・ 22

23.

ResClientの中を見てみよう ◆RestClientはインタフェース → 実装クラスはDefaultRestClient ◆https://github.com/spring-projects/springframework/blob/main/spring- web/src/main/java/org/springframework/web/client/DefaultR estClient.java 23

24.

RestClientの登場人物 ◆ClientHttpRequest / ClientHttpResponse ◆リクエスト・レスポンスを表すインタフェース ◆ClientHttpRequestFactory ◆ClientHttpRequestを生成するインタフェース ◆HttpMessageConverter ◆クラスとHTTPボディ(JSON・XMLなど)の相互変換を行う インタフェース 24

25.

ざっくりとしたシーケンス図 外部システム 25

26.

ClientHttpRequestFactory実装一覧 ◆JdkClientHttpRequestFactory(デフォルト) ◆java.net.http.HttpClientを利用 ◆HttpComponentsClientHttpRequestFactory ◆Apache HttpComponentsを利用 ◆JettyClientHttpRequestFactory ◆Jettyを利用 ◆ReactorClientHttpRequestFactory ◆Reactor Nettyを利用 ◆SimpleClientHttpRequestFactory ◆java.net.HttpURLConnectionを利用(PATCHができない制限あり) 26

27.

ClientHttpRequestFactory実装の指定 ◆application.propertiesで指定 ◆ライブラリの依存関係でHttpComponentsなどが含まれる 可能性もあるため、明示的に指定したほうが安全 27

28.

まとめ ◆RestTemplateがSpring 7.1で非推奨化、 Spring 8.0で削除予定 → RestClientに移行しましょう! ◆RestClientはメソッドチェーンで書く! ◆例外や内部実装などをしっかり押さえよう! 28

29.

目次 ① RestClientの使い方 ② Spring Retryによるリトライ ③ WireMockを利用したRestClientのテスト ④ お知らせ 29

30.

リトライの重要性 ◆ネットワーク越しに別システムにアクセスする際は、 様々な理由でタイムアウトが起こったりする ◆クラウド環境では、瞬間的なネットワークの不調が たまに起こったりする ◆クラウドのオートスケールの力で、数秒後には相手システムが 復旧しているかもしれない 同じリクエストをもう一度送信(=リトライ)すれば、 問題が解決しているかもしれない 30

31.

Spring Retry ◆Spring Framework本体に含まれている、 リトライのためのAPI ◆かつては別のライブラリだったが、Spring Framework 7から 本体に再実装された ◆APIは微妙に変わっているので注意 31

32.

Spring Retryによるリトライ方法 ① RetryTemplate ◆ラムダ式を利用してリトライを記述する 個人的には こちらがおすすめ ◆Beanでなくてもリトライ可能 ② AOP ◆AOPによる割り込み処理でリトライする ◆AOPを使うので、Beanに対してのみリトライ可能 ◆RetryListenerを使えない制限あり 32

33.

①RetryTemplate ◆RetryTemplateのインスタンス化 RetryPolicy retryPolicy = RetryPolicy.builder() .delay(Duration.ofSeconds(1)) // リトライ間隔時間 .multiplier(1.0) // リトライ間隔時間を何倍ずつ増やすか .maxRetries(2) // 最大リトライ回数 .includes( // リトライ対象の例外 HttpServerErrorException.class, HttpClientErrorException.TooManyRequests.class) .build(); RetryTemplate retryTemplate = new RetryTemplate(retryPolicy); 33

34.
[beta]
①RetryTemplate
◆RetryListenerの作成
public class LoggingRetryListener implements RetryListener {
/// 毎回の失敗した試行の後に呼び出されます。
@Override
public void onRetryFailure(RetryPolicy p, Retryable<?> r, Throwable th) {
logger.warn("試行に失敗しました。例外={}", th.getMessage());
}
/// 全試行が上限に達した場合に呼び出されます。
@Override
public void onRetryPolicyExhaustion(
RetryPolicy p, Retryable<?> r, RetryException e) {
logger.error("試行が上限に達しました。例外={}",
e.getLastException().getMessage());
}

}

34

35.

①RetryTemplate ◆RetryListenerの設定 retryTemplate.setRetryListener(new LoggingRetryListener()); 35

36.
[beta]
①RetryTemplate
◆リトライの実装

リトライしたい処理を
引数のラムダ式で記述

try {
TodoResponse todoResponse = retryTemplate.execute(() ->
restClient.get().uri("/api/todos/" + id)
.retrieve().body(TodoResponse.class));
return todoResponse;
} catch (RetryException e) {
Throwable lastException = e.getLastException();
最大リトライ回数
if (lastException instanceof RestClientException rce) {
でも失敗すると
throw rce;
RetryException
} else {
最後にスローされた
(チェック例外)
throw new RuntimeException(e);
例外を取得
}
}
36

37.

②AOP @Configuration @EnableResilientMethods public class AppConfig { ... } リトライしたいメソッドに @Component アノテーションを付加 public class クラス名 { @Retryable( delay = 1_000, multiplier = 1.0, maxRetries = 2, includes = {HogeException.class, FugaException.class} ) public 戻り値の型 メソッド名() { // 処理 } } 37

38.

リトライする前に考えるべきこと ①リトライで問題が解決するのか? ◆リトライで解決する可能性がある異常 ◆タイムアウト ◆ステータスコード5xx ◆ステータスコード429 Too Many Requests ◆リトライでは解決しない異常 ◆429以外のステータスコード4xx 38

39.

リトライする前に考えるべきこと ②クライアントをどれだけ待たせられるか? ◆リトライするとクライアントの待ち時間が長くなる ◆場合によってはリトライしないことも考慮に入れる ◆リトライ間の待ち時間(=バックオフ)を決める ◆固定バックオフ、指数関数的バックオフ、ランダムバックオフなどが ある ◆最大リトライ回数を決める 39

40.

リトライする前に考えるべきこと ③リトライして問題にならないか? ◆GET・PUT・DELETEなどは冪等 → 考慮は不要 ◆POST・PATCHは冪等でない → Idempotency-KeyリクエストヘッダーにUUIDなどを 設定することで、同一のリクエストかどうかを判断して データの2重登録などを防ぐ ◆詳細はMDNを参照 → https://developer.mozilla.org/enUS/docs/Web/HTTP/Reference/Headers/Idempotency-Key 40

41.

まとめ ◆Spring Retryでリトライを実装できる! ◆リトライする前にいろいろ考えることがあります 41

42.

目次 ① RestClientの使い方 ② Spring Retryによるリトライ ③ WireMockを利用したRestClientのテスト ④ お知らせ 42

43.

テスト用モックサーバー、どれ使う? ◆MockRestServiceServer ◆Spring謹製。実際にサーバーは起動しない ◆WireMock ◆OSSのモックサーバー ◆MockServer ◆ここしばらく開発が止まっている → 2026年5月から活動再開 43

44.

WireMockを使う <dependencies> ... <dependency> <groupId>org.wiremock</groupId> <artifactId>wiremock-standalone</artifactId> <version>3.13.2</version> <scope>test</scope> </dependency> </dependencies> 44

45.

WireMockでテスト @SpringBootTest public class TodoClientTest { @Autowired TodoClient todoClient; @RegisterExtension static WireMockExtension wireMock = WireMockExtension.newInstance() .options(wireMockConfig() .http2PlainDisabled(true) HTTP2無効化+ポート番号指定 .port(9999)) .build(); // 次のスライドへ 45

46.

調査したけどよく分かっていないこと ◆HTTP2を有効化していると、なぜか一部のテストが落ちる ◆@ExtendWith(WireMockExtension.class)だと、 なぜか一部のテストが落ちる ◆見識をお持ちの方、ぜひ教えてください・・・ 46

47.
[beta]
WireMockでテスト

①正常時

@Test @DisplayName("IDを指定すると、該当するTODOを取得できる")
void success() {
// WireMockの設定
wireMock.stubFor(get("/api/todos/1")
.willReturn(okJson("""
{"id": 1, "description": "Example 1","completed": true,
"deadline": "2025-10-01T12:00:00",
"createdAt": "2025-09-01T12:00:00"}""")));
// テストの実行
TodoResponse actual = todoClient.getById(1);
// 結果の検証
assertEquals(
new TodoResponse(1, "Example 1", true,
LocalDateTime.parse("2025-10-01T12:00:00"),
LocalDateTime.parse("2025-09-01T12:00:00")
), actual);
}
47

48.
[beta]
WireMockでテスト

②異常時

@Test @DisplayName("該当するIDのTODOがない場合は、例外が発生する")
void empty() {
// WireMockの設定
wireMock.stubFor(get("/api/todos/999")
.willReturn(notFound().withBody("""
{
"type": "about:blank",
"status": 404,
"title": "Not Found",
"detail": "該当するTODOが見つかりません。",
"instance": "/api/todos/999",
}
""")));
// テストの実行と例外の検証
assertThrows(HttpClientErrorException.NotFound.class,
() -> todoClient.getById(999)
);
}

48

49.
[beta]
WireMockでテスト

③リトライ

@Test
@DisplayName("500->500->200とレスポンスされると、指定したID該当するTODOを取得できる")
void retrySuccess() {
// 1回目は500
wireMock.stubFor(get("/api/todos/1")
.inScenario("TODO").whenScenarioStateIs(Scenario.STARTED)
.willReturn(serverError().withBody("<エラー時のレスポンスボディ>"))
.willSetStateTo("SECOND"));
// 2回目も500
wireMock.stubFor(get("/api/todos/1")
.inScenario("TODO").whenScenarioStateIs("SECOND")
.willReturn(serverError().withBody("<エラー時のレスポンスボディ>"))
.willSetStateTo("THIRD"));
// 3回目は200
wireMock.stubFor(get("/api/todos/1")
.inScenario("TODO").whenScenarioStateIs("THIRD")
.willReturn(okJson("<正常なレスポンスボディ>")));
// 次頁に続く

49

50.

WireMockでテスト ③リトライ // 時間の計測開始(org.springframework.util.StopWatchクラス) StopWatch stopWatch = new StopWatch(); stopWatch.start(); // テストの実行 TodoResponse actual = todoClient.getById(1); // 時間の計測終了 stopWatch.stop(); int seconds = (int) stopWatch.getTotalTimeSeconds(); // 結果の検証 assertAll( () -> assertEquals(new TodoResponse(...), actual), () -> assertEquals(2, seconds) リトライした分だけの時間がかかっているか ); も } 検証したほうがいい (たまにちゃんとリトライできてないことが ある) 50

51.

まとめ ◆Wiremockてテスト用モックサーバーを作れる! ◆リトライのテストもできる! 51

52.

目次 ① RestClientの使い方 ② Spring Retryによるリトライ ③ WireMockを利用したRestClientのテスト ④ お知らせ 52

53.

Jakarta EEの勉強会やります! ◆日時: 2026-06-22(月) 19:00-21:00 ◆場所: 恵比寿のRed Hat様セミナールーム ◆テーマ ◆Jakarta EE 12 が変えるデータアクセスの新仕様:Data, NoSQL, Query 解説(by 景井さん) ◆ついにMVCがJakarta EEに。その歴史・機能・アーキテクチャを知ろ う (by 多田) 53

54.

頑張って書いたので買ってください ハッシュタグ 書きました! #つくまなBoot 6月発売予定 54

55.

ご清聴ありがとうございました! ◆よいSpringライフを! 55