Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Spring Frameworkの新標準!? ~ RestClientとHTTPインターフェー...
Search
荻原利雄
November 06, 2024
Technology
2.9k
4
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Spring Frameworkの新標準!? ~ RestClientとHTTPインターフェース入門 ~
荻原利雄
November 06, 2024
More Decks by 荻原利雄
See All by 荻原利雄
先取りMaven4 ~16年ぶりのメジャーアップデート、その進化とは?~
ogiwarat
0
180
実践ArchUnit ~実例による検証パターンの紹介~
ogiwarat
2
1.4k
Spring Boot vs MicroProfile ~クラウドネイティブにおけるフレームワークの比較と選択~
ogiwarat
2
2.6k
第1回 AWSとGitHub勉強会 - キックオフ -
ogiwarat
0
1.1k
第2回 AWSとGitHub勉強会 - CodespacesとHelidonの利用 -
ogiwarat
0
1.2k
第3回 AWSとGitHub勉強会 - GitHub Actionsを使ったCI環境の構築 -
ogiwarat
0
1.1k
第4回 AWSとGitHub勉強会 - GitHub Actionsを使ったCD環境の構築 -
ogiwarat
0
1.1k
第5回 AWSとGitHub勉強会 - AWS EC2環境の構築 -
ogiwarat
0
1.1k
第6回 AWSとGitHub勉強会 - AWS ECS Fargate環境の構築 -
ogiwarat
0
1.3k
Other Decks in Technology
See All in Technology
2026 TECHFRESH 畢業分享會 - AI-Native 重塑軟體工程與虛擬講師
line_developers_tw
PRO
0
1.2k
攻撃者視点で考えるDetection Engineering
cryptopeg
3
2k
日本 Fintech 未来予測レポート 2027〜2028年(オリジナル版)
8maki
0
2.3k
自律型AIエージェントは何を破壊するのか
kojira
0
160
【NRUG vol.18】なぜ多くのオブザーバビリティ導入は失敗するのか
nrug_member
0
190
Flow 不死:AI 時代 DevOps 的不變本質
cheng_wei_chen
2
200
200個のGitHubリポジトリを横断調査したかった
icck
0
130
2026TECHFRESH畢業分享會 - 原生還是跨平台? App 開發踩坑實錄
line_developers_tw
PRO
0
1.3k
LayerXにおけるセキュリティ管理の現在地と次の一手
tosho
0
240
手塩にかけりゃいいってもんじゃない
ming_ayami
0
600
2026TECHFRESH畢業分享會 - Lightning Talk - 資料也要 CI/CD? 用 Airbyte 自動化資料同步
line_developers_tw
PRO
0
1.2k
ザ・データベース、MySQL ~ OSC 2026 Sendai ~
sakaik
0
120
Featured
See All Featured
Optimising Largest Contentful Paint
csswizardry
37
3.7k
Google's AI Overviews - The New Search
badams
0
1k
"I'm Feeling Lucky" - Building Great Search Experiences for Today's Users (#IAC19)
danielanewman
230
23k
How to Grow Your eCommerce with AI & Automation
katarinadahlin
PRO
1
210
Navigating the moral maze — ethical principles for Al-driven product design
skipperchong
2
390
DevOps and Value Stream Thinking: Enabling flow, efficiency and business value
helenjbeal
1
240
The World Runs on Bad Software
bkeepers
PRO
72
12k
Lessons Learnt from Crawling 1000+ Websites
charlesmeaden
PRO
1
1.3k
Are puppies a ranking factor?
jonoalderson
1
3.6k
Highjacked: Video Game Concept Design
rkendrick25
PRO
1
390
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
49
3.5k
Large-scale JavaScript Application Architecture
addyosmani
515
110k
Transcript
Spring Frameworkの新標準!? ~ RestClientとHTTPインターフェース入門 ~ 株式会社 豆蔵 ビジネスソリューション事業部 主幹ソフトウェアエンジニア 荻原
利雄 2024/11/6 JSUG 勉強会:その2
2 荻原 利雄(オギワラ トシオ) • 所属 / 職種 - 株式会社豆蔵
- ビジネスソリューション事業部 - 主幹ソフトウェアエンジニア • プロフィール - オブジェクト指向とともにエンタープライズな Javaアプリを作りつづけて25年のアラフィフエ ンジニア - ここ数年は大規模基幹システムを支える JakartaEEフルスタックなフレームワークの開発 を行っている - 昨年度からはSpring Bootを使った共通機能の開 発も行っている extact-io 豆蔵デベロッパーサイト 豆蔵デベロッパーサイトで連載中! Springの小話
3 © 2024 Mamezou Inc. All rights reserved. 豆蔵 採用
会社紹介 日本におけるSpringの先駆的企業 (だたし今となってはみなさん元豆蔵) 1粒の知性が ソフトウェアエンジニアリングを変える。 豆蔵では共に高め合う仲間を募集しています! 豆蔵は、オブジェクト指向技術含めた ソフトウェア工学を産業、企業に浸透 させるべく1999年に創業しました 創業以来、ソフトウェア工学を基軸に ロボティクス、AI/IoTによる工場のデ ジタル化、車載向けECU統合化、 ERP/Open Sourceによる基幹系シス テム刷新等に取り組んでいます
Agenda 1. RestTemplateからみたRestClientとのコンセプトや機能の違い 2. RestClientの拡張ポイントとその実装例 3. HTTPインターフェースの機能とその利用例 4. RestClient+HTTPインターフェースの活用 5.
まとめ 4
お断り • 本資料はSpringバージョンはSpring Bootは現時点(2024/11/6)のGA最新の v3.3.5をベースに確認・作成しています • RestClientはSpring Boot 3.1(Spring Framework
6.1)で正式リリースされ、現時点で そこから大きく変わったところはありません。よってSpring Boot 3.1以降であれば本 資料の内容を参考にしていただけると思います • 説明の中で前提としているSpring Bootの機能はAuto Configurationのみ(と思ってい る)ため、Spring Framework単独でも参考にしていただけると思います 5 本日の説明で利用したソースコードは一式以下にアップしています。 https://github.com/extact-io/jsug-2024-restclient テストコードも含めRestClientとHTTPInterfaceのまとまったサンプルとしても使えるので見てみね♪
6 1. RestTemplateからみたRestClientとのコンセプトや機能の違い 2. RestClientの拡張ポイントとその実装例 3. HTTPインターフェースの機能とその利用例 4. RestClient+HTTPインターフェースの活用 5.
まとめ
まずはRestTemplateのおさらい • その前に本日の説明で使用する例の紹介 7 同じお題をもとにそれぞ れの実装をみていくよ!
RestTemplateのコードを眺めてみる(1/7) 8 // RestTemplateの作り方 String baseUrl = "http://localhost:8080"; DefaultUriBuilderFactory uriFactory
= new DefaultUriBuilderFactory(baseUrl); RestTemplate restTemplate = new RestTemplate(); 作り方はいくつかあるが、都 度指定するのは面倒なので baseUrlは指定してしておく。 これを前提に以降のコードを 眺める
RestTemplateのコードを眺めてみる(2/7) 9 // IDを指定して本を取得する public Optional<Book> get(int id) { BookResponse
book = restTemplate.getForObject("/books/{id}", BookResponse.class, id); return Optional.ofNullable(book) .map(BookResponse::toModel); } // 本を全件取得する public List<Book> getAll() { List<BookResponse> bookResponses = restTemplate.exchange( "/books", HttpMethod.GET, null, new ParameterizedTypeReference<List<BookResponse>>() { }) .getBody(); return bookResponses.stream() .map(BookResponse::toModel) .toList(); } getForObjectとgetForEntity ではBodyの取得型にList(正し くはジェネリック指定)を使え ないのでexchangeメソッドを 使う必要がある
RestTemplateのコードを眺めてみる(3/7) 10 // 条件に合う本を検索する public List<Book> findByCondition(Map<String, String> queryParams) {
// RestTemplateからbaseUriの部分を取得 UriBuilder builder; if (restTemplate.getUriTemplateHandler() instanceof UriBuilderFactory factory) { builder = factory.builder(); } else { throw new IllegalStateException("unknwon type =>" +… ) } builder.path("/books/search"); // Mapの内容をUriComponentsBuilderを使ってクエリーストリングに変換 queryParams.forEach((key, value) -> builder.queryParam(key, value)); URI uri = builder.build(); List<BookResponse> bookResponses = restTemplate.exchange( uri, HttpMethod.GET, null, new ParameterizedTypeReference<List<BookResponse>>() { }) .getBody(); return bookResponses.stream() .map(BookResponse::toModel) .toList(); } クエリーストリングは UriBuilderを使って設定して あげる(URLエンコードとかあ るし) そして今度もexchange!
RestTemplateのコードを眺めてみる(4/7) 11 // 著者名が前方一致する本を検索する public List<Book> findByAuthorStartingWith(String prefix) { List<BookResponse>
bookResponses = restTemplate.exchange( "/books/author?prefix={prefix}", HttpMethod.GET, null, new ParameterizedTypeReference<List<BookResponse>>() { }, prefix) .getBody(); return bookResponses.stream() .map(BookResponse::toModel) .toList(); } 動的に変わる部分を {prefix}としてバインドパ ラメータで指定 そして今度もexchange! さらに今度は今までになかった第4パラ メータが登場!これは{prefix}に対す るバインド値です
RestTemplateのコードを眺めてみる(5/7) 12 // 本を追加する public Book add(String title, String author,
LocalDate published) throws DuplicateException { AddRequest request = new AddRequest(title, author, published); BookResponse bookResponse = restTemplate.postForObject( "/books", request, BookResponse.class); return bookResponse.toModel(); } xxxForObject, xxxForEntity のxxxは使用するHTTPメソッド によって使い分ける
RestTemplateのコードを眺めてみる(6/7) 13 // 本を更新する public Book update(Book updateBook) throws NotFoundException
{ UpdateRequest request = UpdateRequest.from(updateBook); // BookResponse bookResponse = restTemplate.exchange( "/books", HttpMethod.PUT, new HttpEntity<>(request), BookResponse.class) .getBody(); return bookResponse.toModel(); } 戻り値はListではないのでputForObjectを使 いたいが、HTTPプロトコルの原理主義ではPUT のレスポンスボディを使うのはよくないので、 putForObjectはない!(putForEntityも同様) なので、PUTで戻りを取得したい場合は exchangeを使う必要あり
RestTemplateのコードを眺めてみる(7/7) 14 // 本を削除する @Override public void delete(int id) throws
NotFoundException { restTemplate.delete("/books/{id}", id); } RestTemplate#deleteはHTTPのDELETEメソッドの送信 の意味 やることが単純なのでこのメソッドはシンプル・・
RestTemplateのAPIって、、 (昔はよかったのかもしれないが、今となっては) • やりたいことは同じでもgetForObject, getForEntity, exchangeと3つもある • それぞれ引数をいっぱい取るので、どこになにを渡せばいいの かとても分かりづらい •
加えてオーバーロードメソッドが沢山あるので、どれを使えば いいのか更に分かりづらい • もっというとラムダが使えないのでサクッと書けない&記述が 冗長になりがち(昨今のAPIスタイルと比べて) 15 そこで RestClientの誕生
RestClientで実装してみる(1/5) 16 // RestTemplateの作り方 String baseUri = "http://localhost:8080"; DefaultUriBuilderFactory uriFactory
= new DefaultUriBuilderFactory(baseUrl); RestTemplate restTemplate = new RestTemplate(); // RestClientの作り方 RestClient restClient = RestClient.builder() .baseUrl("http://localhost:8080") .build(); Builderパターンが採用され分かりやすく スッキリ。Builderで他にも沢山設定が可 能になっている(後述) RestTemplate RestClient
RestClientで実装してみる(2/5) 17 // IDを指定して本を取得する public Optional<Book> get(int id) { BookResponse
book = restTemplate.getForObject("/books/{id}", BookResponse.class, id); return Optional.ofNullable(book) .map(BookResponse::toModel); } // IDを指定して本を取得する public Optional<Book> get(int id) { BookResponse book = client .get() // 1. GETで .uri("/books/{id}", id) // 2. このURI(パス)に引数をバインドして .retrieve() // 3. 送信を実行して .body(BookResponse.class); // 4. レスポンスボディはBookResponseで返してね♪ return Optional.ofNullable(book) .map(BookResponse::toModel); } まとめて引数で渡すのではなく、 必要な「もの」や「こと」を渡 していくfluentなAPIスタイルへ RestTemplate RestClient
RestClientで実装してみる(3/5) 18 // 本を全件取得する public List<Book> getAll() { List<BookResponse> bookResponses
= restTemplate.exchange( "/books", HttpMethod.GET, null, new ParameterizedTypeReference<List<BookResponse>>() { }) .getBody(); return bookResponses.stream() .map(BookResponse::toModel) .toList(); } // 本を全件取得する public List<Book> getAll() { List<BookResponse> bookResponses= client .get() .uri("/books") .retrieve() .body(new ParameterizedTypeReference<List<BookResponse>>(){}); } fluentな呼び出し方は先ほどと 同じ。かつListでGenricで型指 定した戻り値の取得も可能 getForObjectとgetForEntity ではBodyの取得型にList(正し くはジェネリック指定)を使え ないのでexchangeメソッドを 使う必要がある RestTemplate RestClient
RestClientで実装してみる(4/5) 19 // 著者名が前方一致する本を検索する public List<Book> findByAuthorStartingWith(String prefix) { List<BookResponse>
bookResponses = client .get() .uri("/books/author", builder -> builder.queryParam("prefix", prefix).build()) .retrieve() .body(new ParameterizedTypeReference<>(){}); … } 値を設定する個所でラムダが使 えるので簡潔に指定できること が多くなる // 著者名が前方一致する本を検索する public List<Book> findByAuthorStartingWith(String prefix) { List<BookResponse> bookResponses = restTemplate.exchange( "/books/author?prefix={prefix}", HttpMethod.GET, null, new ParameterizedTypeReference<List<BookResponse>>() { }, prefix) .getBody(); … } 動的に変わる部分を {prefix}としてバインドパ ラメータで指定 今までになかった第4パラメータ が登場!これは{prefix}に対する バインド値です RestTemplate RestClient
RestClientで実装してみる(5/5) 20 // 本を追加する @Override public Book add(String title, String
author, LocalDate published) throws … { AddRequest request = new AddRequest(title, author, published); BookResponse bookResponse = client .post() .uri("/books") .body(request) .retrieve() .body(BookResponse.class); return bookResponse.toModel(); } // 本を追加する public Book add(String title, String author, LocalDate published) throws … { AddRequest request = new AddRequest(title, author, published); BookResponse bookResponse = restTemplate.postForObject( "/books", request, BookResponse.class); return bookResponse.toModel(); } 他のHTTPメソッド呼び出しにな てもfluentな呼び出しスタイル に変化なし。HTTPメソッド指定 の個所を変えるだけ // 本を更新する BookResponse bookResponse = client .put() .uri("/books") .body(request) .retrieve() .body(BookResponse.class); return bookResponse.toModel(); // 本を削除する client .delete() .uri("/books/{id}", id) .retrieve() .toBodilessEntity(); xxxForObject, xxxForEntityの xxxは使用するHTTPメソッドに よって使い分ける RestTemplate RestClient
ここまでのまとめ • RestTemplateからみたRestClientのコンセプトや機能の違いは? • コンセプトの違い ➢ APIのスタイルがオーバーロードからfluentへ • 機能の違い ➢
極論すると、いや極論しなくても機能は同じ • つまり、既存のAPIをリファクタリングしただけ • ではなぜ今さら? • (Springの)利用者の視点 ➢ APIがわかりづらい、使いづらい、もう勘弁して(個人の感想です) • (Springの)開発者の視点 ➢ オーバーロード地獄で、機能追加が大変(、もう勘弁して) そして、RestClientの爆誕へ 21
RestTemplateはオワコンか? 22 <中の人の回答をみると>https://github.com/spring-projects/spring-framework/issues/32016 から Google翻訳したもの を記載しています 回答
結局どうなのか • RestTemplateが今後なくなるや削除されることいったことはな いと想定される • 「メンテナンスモード」の記載は今では削除し、現時点で RestTemplateの使用は(オフィシャルに)非推奨というもので はない • しかし、今後RestTemplateの都合であらな機能を追加すること
はない 23 https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/web/client/RestTemplate.htmlより RestTemplate と RestClient は同じインフラストラクチャ (つまり、リクエストファクトリ、リクエストインターセプタおよびイニ シャライザー、メッセージコンバーターなど) を共有するため、そこで行われた改善も同様に共有されます。ただし、RestClient は新 しい高レベルの機能に焦点を当てています。 今後はRestClientがSpringにおける(同期呼び出しの)RESTクライア ント機能の標準になると考えるのが妥当
24 1. RestTemplateからみたRestClientとのコンセプトや機能の違い 2. RestClientの拡張ポイントとその実装例 3. HTTPインターフェースの機能とその利用例 4. RestClient+HTTPインターフェースの活用 5.
まとめ
RestClientインスタンスの作り方 • Builderで色々な拡張指定が可能 25 RestClient customClient = RestClient.builder() .requestFactory(new HttpComponentsClientHttpRequestFactory())
.messageConverters(converters -> converters.add(new MyCustomMessageConverter())) .baseUrl("https://example.com") .defaultUriVariables(Map.of("variable", "foo")) .defaultHeader("My-Header", "Foo") .requestInterceptor(myCustomInterceptor) .requestInitializer(myCustomInitializer) .build(); https://spring.pleiades.io/spring-framework/reference/integration/rest-clients.html 以降、それぞれの拡張ポイントを利用例とともに説明していきます RestClient
ある値をヘッダーに常に設定してリクエストを送 信したい! • BuilderのdefaultHeaderで設定するだけ 26 RestClient restClient = RestClient.builder() .defaultHeader("Sender-Name",
BookClientApplication.class.getSimpleName()) . … .build(); このBuilderで生成されたRestClientから送信されるリクエストヘッダには常に Sender-Name: BookClientApplication が設定されるようになる defaultHeaderメソッドはConsumer<HttpHeaders>の引数も取るので処理を渡すことも可能だが、ゴ リゴリやる場合は後述のClientHttpRequestInitializerの実装クラスを作成した方がよいと思う RestClient
あるクエリーパラメータをに常に設定してリクエ ストを送信したい! • Builderで構成済みのUriBuilderを渡すだけ 27 // 常に送信したいクエリーパラメータを設定したUriBuilderを作成する UriComponentsBuilder uriBuilder =
UriComponentsBuilder .fromHttpUrl("http://localhost:8080") .queryParam("Sender-Name", BookClientApplication.class.getSimpleName()); // 作成したUriBuilderを設定したRestClientを生成する RestClient restClient = RestClient.builder() .uriBuilderFactory(uriFactory) . … .build(); このBuilderで生成されたRestClientから送信されるリクエストのクエリーストリングには常に Sender-Name=BookClientApplication が設定されるようになる RestClient
自分で設定したObjectMapperを使いたい • 自分で設定したObjectMapperを内包したHttpMessageConverterを Builderで設定する 28 // 自分で設定したObjectMapperを作成する(例はLocalDateオブジェクトを”yyyyMMdd”の書式に従って) // (デ)シリアライズするようにする例 ObjectMapper
mapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addSerializer(LocalDate.class, new ConfigurableLocalDateSerializer("yyyyMMdd")); module.addDeserializer(LocalDate.class, new ConfigurableLocalDateDeserializer("yyyyMMdd")); mapper.registerModule(module); // JSONの(デ)シリアライズにオレオレのObjectMapperが使われるようにSpringの // MappingJackson2HttpMessageConverterを被せる HttpMessageConverter<Object> converter = new MappingJackson2HttpMessageConverter(mapper); // 作成したメッセージコンバーターが常に優先されるようにリストの先頭に追加する RestClient restClient = RestClient.builder() .messageConverters(converters -> converters.addFirst(converter)) . … .build(); リクエスト・レスポンスのJSONに含まれるLocalDateオブジェクトに対する(デ)シリアライズは常に “yyyyMMdd”の書式に従って行われるようになる RestClient
動的にリクエストヘッダに値を設定したい • 行いたいヘッダー処理を実装したClientHttpRequestInitializerを Builderに渡す 29 // 認証情報をユーザIDとロールをヘッダに設定する実装例 public class PropagateUserContextInitializer
implements ClientHttpRequestInitializer { @Override public void initialize(ClientHttpRequest request) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated()) { String userId = String.valueOf(auth.getPrincipal()); String roles = auth.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(",")); request.getHeaders().add("User-ID", userId); request.getHeaders().add("Roles", roles); } } } // 作成したClientHttRequestInitializerを設定したRestClientを生成する RestClient restClient = RestClient.builder() .requestInitializer(new PropagateUserContextInitializer()) . … .build(); 認証情報が存在する場合、こ のBuilderから生成された RestClientから送信するリク エストのヘッダには常に認証 情報が設定されるようになる RestClient
リクエスト・レスポンスの送受信の前後に処理を 割り込ませたい • 割り込んだときに行いたい処理を実装した ClientHttpRequestInterceptorをBuilderに渡す 30 // リクエスト送信時、レスポンスの受信時にそれぞれのヘッダー情報をログ出力する実装例 @Slf4j public
class LoggingInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { // リクエストのヘッダーとボディをログ出力 logRequestDetails(request, body); // リクエストを実行し、レスポンスを取得 ClientHttpResponse response = execution.execute(request, body); // レスポンスのヘッダーをログ出力 logResponseDetails(response); return response; } private void logRequestDetails(HttpRequest request, byte[] body) { log.info("Request URI: " + request.getURI()); … // リクエストボディのログ出力 log.info("Request Body: " + new String(body, StandardCharsets.UTF_8)); } private void logResponseDetails(ClientHttpResponse response) throws IOException { log.info("Response Status Code: " + response.getStatusCode()); log.info("Response Headers: " + response.getHeaders()); … } } // 作成したInterceptorをBuilderに設定する RestClient restClient = RestClient.builder() .requestInterceptor(new LoggingInterceptor()) . … .build(); RestClient
ClientHttpRequestInterceptorに注意点 31 @Slf4j public class LoggingInterceptor implements ClientHttpRequestInterceptor { @Override
public ClientHttpResponse intercept( HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { ここは注意! • リクエストのメッセージボティはInterceporが呼び出された時点で 全量byte配列化(文字列化)されメモリに展開される • Interceptorが使われていない場合、JSONシリアライズされた結果 やファイルリソースは通信路(OutputStream)に繋げて処理されるた め、ボティの内容が余分にメモリに載ることはない • なので添付ファイルや大きなJSONデータを送る場合はOOMに注意す る必要がある RestClient
レスポンスエラーハンドリグをまとめて行いたい • エラー処理を実装したResponseErrorHandlerをBuilderに渡す 32 // 400(BAD_REQUEST), 404(NOT_FOUND), 409(CONFLICT)応答を受信したらそれぞれの対応する例外を送出する実装例 @Slf4j public
class BookResponseErrorHandler implements ResponseErrorHandler { private static final List<Integer> HANDLE_STATUS = List.of( HttpStatus.CONFLICT.value(), HttpStatus.NOT_FOUND.value(), HttpStatus.BAD_REQUEST.value()); @Override public boolean hasError(ClientHttpResponse res) throws IOException { return HANDLE_STATUS.contains(res.getStatusCode().value()); } @Override public void handleError(ClientHttpResponse response) throws IOException { ErrorMessage message = readBody(response); HttpStatus status = HttpStatus.resolve(response.getStatusCode().value()); switch (status) { case HttpStatus.CONFLICT -> throw new DuplicateException(message); case HttpStatus.NOT_FOUND -> throw new NotFoundException(message); case HttpStatus.BAD_REQUEST -> throw new ValidationException(message); default -> throw new IllegalArgumentException("Unexpected value: " + response.getStatusCode()); } } } // 作成したErrorHandlerをBuilderに設定する RestClient restClient = RestClient.builder() . defaultStatusHandler( new BookResponseErrorHandler()) . … .build(); RestClient
HttpClientの実装を変えたい/接続設定を細かく した(1/2) • 利用可能なClientHttpRequestFactory実装 • HttpComponentsClientHttpRequestFactory → Apache HTTPClient •
JettyClientHttpRequestFactory → Jetty • JdkClientHttpRequestFactory → JDK HttpClient (Java11以上) • SimpleClientHttpRequestFactory → JDK HttpURLConnection (Java11未満) • JDKのクラスを利用するものがデフォルト。利用する実装を変えたい 場合は対応するFactoryをBuilderに渡せばよい • Spring Bootの場合はクラスパスを基にApache HTTPClient→Jetty→JDKの優 先度でAutoConfigureされる • HTTP接続に関する細かい設定を行いたい場合は自分で構成した ClientHttpRequestFactoryをBuilderに渡す 33
HttpClientの実装を変えたい/接続設定を細かく した(2/2) • Apache HTTPClientに対して細かい設定を行う例 34 // 接続タイムアウト、リードタイムアウト、最大リダイレクト回数を設定する例 ConnectionConfig connectionConfig
= ConnectionConfig.custom() .setConnectTimeout(Timeout.ofSeconds(5)) // 接続タイムアウト(default:3sec) .build(); PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setDefaultConnectionConfig(connectionConfig); RequestConfig requestConfig = RequestConfig.custom() .setResponseTimeout(Timeout.ofSeconds(30)) // 読み取りタイムアウト(default:none) .setMaxRedirects(3) // 最大リダイレクト回数(default:50) .build(); CloseableHttpClient httpClient = HttpClients.custom() .setConnectionManager(connectionManager) .setDefaultRequestConfig(requestConfig) .build(); ClientHttpRequestFactory = HttpComponentsClientHttpRequestFactory(httpClient); // 作成したFactoryをBuilderに設定する RestClient restClient = RestClient.builder() .requestFactory(requestFactory) . … .build(); RestClient
35 1. RestTemplateからみたRestClientとのコンセプトや機能の違い 2. RestClientの拡張ポイントとその実装例 3. HTTPインターフェースの機能とその利用例 4. RestClient+HTTPインターフェースの活用 5.
まとめ
HTTPインターフェースってどんなもの • エンドポイントやパラメータ名などのREST API仕様をインターフェース で定義すると、それがそのままRESTクライアントの実装として使える • たとえるとインターフェースだけでデータソースへのアクセスが行える Spring DataのRESTクライアント版(MicroProfileには遥か昔から存在) 36
@HttpExchange("/books") public interface BookClientApi { @GetExchange("/{id}") BookResponse get(@PathVariable int id); @GetExchange List<BookResponse> getAll(); @GetExchange("/search") List<BookResponse> findByCondition(@RequestParam Map<String, String> queryParams); @GetExchange("/author") List<BookResponse> findByAuthorStartingWith(@RequestParam("prefix") String prefix); @PostExchange BookResponse add(@RequestBody AddRequest request); … } @RequestMappingではなく @HttpExchange @GetMappingではな く@GetExchange @PostMappingではなく @PostExchangeでPUTもDELETEも 同じネーミングルール インターフェースの定義は @RestControllerのXXXMappingの アノテーション名が違うだけで 他はすべて同じ。ここで定義さ れた内容に従ってリクエストの 送受信が行われる。
HTTPInterfaceインスタンスの作り方 • リクエスト送信に使うRestClientをAdapterに被せるだけ 37 // リクエストの送受信に使うRestClientの生成(構成) RestClient restClient = RestClient.builder()
... .build(); // 生成したRestClientを使うProxyインスタンスを生成するファクトリの生成 RestClientAdapter adapter = RestClientAdapter.create(restClient); HttpServiceProxyFactory factory = HttpServiceProxyFactory .builderFor(adapter) .conversionService(conversionService) .build(); // BookClientApiインタフェースに対するProxyインスタンスの生成 BookClientApi api = factory.createClient(BookClientApi.class); Proxyの生成に使われたRestClientを使って実際の リクエストの送受信は行われます ココのインター フェースを作るだけ
HTTPInterfaceで実装してみる(1/4) 38 // IDを指定して本を取得する public Optional<Book> get(int id) { BookResponse
book = client .get() // 1. GETで .uri("/books/{id}", id) // 2. このURI(パス)に引数をバインドして .retrieve() // 3. 送信を実行して .body(BookResponse.class); // 4. レスポンスボディはBookResponseで返してね♪ return Optional.ofNullable(book) .map(BookResponse::toModel); } // IDを指定して本を取得する public Optional<Book> get(int id) { BookResponse book = client.get(id); return Optional.ofNullable(book) .map(BookResponse::toModel); } RestClientに対する処理はインター フェース定義に従いProxyがやってく れるため、処理はスッキリ! @GetExchange("/{id}") BookResponse get(@PathVariable int id); <使っているインターフェース> パラメータバインドも勝手にやっても らえる RestClient HTTPInterface
HTTPInterfaceで実装してみる(2/4) 39 // 本を全件取得する public List<Book> getAll() { List<BookResponse> bookResponses=
client .get() .uri("/books") .retrieve() .body(new ParameterizedTypeReference<List<BookResponse>>(){}); } // 本を全件取得する public List<Book> getAll() { return client.getAll().stream() .map(BookResponse::toModel) .toList(); } @GetExchange List<BookResponse> getAll(); <使っているインターフェース> RestClient HTTPInterface
HTTPInterfaceで実装してみる(3/4) 40 // 著者名が前方一致する本を検索する public List<Book> findByAuthorStartingWith(String prefix) { return
client.findByAuthorStartingWith(prefix).stream() .map(BookResponse::toModel) .toList(); } パラメータバインドもインタ フェースに従って自動 // 著者名が前方一致する本を検索する public List<Book> findByAuthorStartingWith(String prefix) { List<BookResponse> bookResponses = client .get() .uri("/books/author", builder -> builder.queryParam("prefix", prefix).build()) .retrieve() .body(new ParameterizedTypeReference<>(){}); … } @GetExchange("/author") List<BookResponse> findByAuthorStartingWith(@RequestParam("prefix") String prefix); <使っているインターフェース> RestClient HTTPInterface
HTTPInterfaceで実装してみる(4/4) 41 // 本を追加する public Book add(String title, String author,
LocalDate published) throws DuplicateException { AddRequest request = new AddRequest(title, author, published); return client.add(request).toModel(); } // 本を追加する @Override public Book add(String title, String author, LocalDate published) throws … { AddRequest request = new AddRequest(title, author, published); BookResponse bookResponse = client .post() .uri("/books") .body(request) .retrieve() .body(BookResponse.class); return bookResponse.toModel(); } @PostExchange BookResponse add(@RequestBody AddRequest request); <使っているインターフェース> RestClient HTTPInterface
42 1. RestTemplateからみたRestClientとのコンセプトや機能の違い 2. RestClientの拡張ポイントとその実装例 3. HTTPインターフェースの機能とその利用例 4. RestClient+HTTPインターフェースの活用 5.
まとめ
ドメイン層に対するインタフェースは別に出しま しょう(設計のお話)(1/2) • インターフェースだからとしてHTTPInterfaceをドメイン層(ビ ジネス層)で直接使うのはNG! 43 このような設計は一見DIP(依存性逆転の原則)になってるが、インフラ層の都合や 影響がドメイン層に直撃 NG
なぜか:HTTPInterfaceで実装でみてみる (ファイルアップロード編) 44 // ファイルのアップロード public String upload(String resourceName) {
MultiValueMap<String, Resource> parts = new LinkedMultiValueMap<>(); parts.add("file", new ClassPathResource(resourceName)); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); return client.upload(headers, parts); } ヘッダーの指定が必要 // ファイルのアップロード public String upload(String resourceName) { MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>(); parts.add("file", new ClassPathResource(resourceName)); return client .post() .uri("/books/upload") .contentType(MediaType.MULTIPART_FORM_DATA) .body(parts) .retrieve() .body(String.class); } @PostExchange("/upload") String upload(@RequestHeader MultiValueMap<String, String> headers, @RequestPart MultiValueMap<String, Resource> parts); <使っているインターフェース> RestClient HTTPInterface 引数にアノテーション を付ける
ドメイン層に対するインタフェースは別に出しま しょう(設計のお話)(2/2) • uploadを見てもらうと分かるけど、HTTPIntercafeを呼ぶ前にゴニョゴニヨしないといけないこと は少なからずある • 対向システムとの送受信に利用するデータモデルは異なる。現状一致しているとしても対向システ ムから変更を要求された場合、その影響をドメイン層でもろに受ける 45 ビジネス層向けのインタフェースとHTTPIntercaは分けて間に緩衝材となるAdapterを入れておくのが吉
OK HTTPリクエストやHTTPレスポンスに対 する処理やデータ構造の変換はココで 行う
HTTPInterfaceは単体テストでも便利に使える • 実装コストがホボないので@RestControllerクラスを呼び出す テストドライバとしてとっても便利!(賛否はあると思います が) 46 実際のテストコードをみてみる (時間があれば)
47 1. RestTemplateからみたRestClientとのコンセプトや機能の違い 2. RestClientの拡張ポイントとその実装例 3. HTTPインターフェースの機能とその利用例 4. RestClient+HTTPインターフェースの活用 5.
まとめ
Spring Frameworkのこれからの新標準 • 「どこに」「なにを」「どうやって」のAPI仕様は HTTPInterfaceで宣言 • HTTPプロトコルの詳細はRestClientで局所化 48 こらからはコレだ!
49 ご清聴ありがとうございました
おまけ 50
MicroProfile Rest Client-機能概要 • インタフェースをもとにREST APIの呼び出しを可能にする機能 • Spring DataやMy Batisのインタフェースによる動的実行と同じイメージ
51 <動作イメージ>
MicroProfile RestClient-APIの利用(1/3) 52 public interface PersonResource { @GET @Path("/{id}") @Produces(MediaType.APPLICATION_JSON)
Person get(@PathParam("id")long id); @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) Person add(Person person); @GET @Produces(MediaType.APPLICATION_JSON) List<Person> findByName(@QueryParam("name") String name); } <Rest Clientで呼び出そうとしているREST API> ▪ PersonResourceインタフェース
MicroProfile RestClient-APIの利用(2/3) 53 <先ほどのREST APIを呼び出すに必要な実装(素のJAX-RS ClientAPI)> public interface PersonResource {
@GET @Path("/{id}") @Produces(MediaType.APPLICATION_JSON) Person get(@PathParam("id")long id); @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) Person add(Person person); @GET @Produces(MediaType.APPLICATION_JSON) List<Person> findByName(@QueryParam("name") String name); } ▪ PersonResourceインタフェース(サーバ側と全く同じ) @ApplicationScoped public class JaxrsPersonService implements PersonService { private Client client; private static final String BASE_URL = "http://localhost:7001/api/persons"; @PostConstruct public void init() { client = ClientBuilder.newClient(); } @Override public Person getPerson(long id) { // リクエスト送信 var response = client .target(BASE_URL) .path("{id}") .resolveTemplate("id", id) .request() .get(); // 結果該当なし if (response.getStatus() == Status.NOT_FOUND.getStatusCode()) { throw new PersonClientException(ClientError.NOT_FOUND); } // 結果取得 return response.readEntity(Person.class); } @Override public Person addPerson(Person person) { // リクエスト送信 var response = client .target(BASE_URL) .request() .post(Entity.json(person)); // nameの値重複 if (response.getStatus() == Status.CONFLICT.getStatusCode()) { throw new PersonClientException(ClientError.NAME_DEPULICATE); } // 結果取得 return response.readEntity(Person.class); } … } ▪ PersonResourceインタフェースの実装 @ApplicationScoped public class RestClientPersonService { private PersonClient personClient; @Inject public RestClientPersonService(PersonClient personClient) { this.personClient = personClient; } @Override public Person getPerson(long id) { return personClient.get(id); } … } ▪ PersonResourceインタフェースの利用
MicroProfile RestClient-APIの利用(3/3) 54 <先ほどのREST APIを呼び出すに必要な実装(MicroProfile RestClientの利用)> ▪ PersonResourceインタフェース(サーバ側+アノテーション) @ApplicationScoped public
class RestClientPersonService { private PersonClient personClient; @Inject public RestClientPersonService(@RestClient PersonClient personClient) { this.personClient = personClient; } @Override public Person getPerson(long id) { return personClient.get(id); } … } ▪ PersonResourceインタフェースの利用 @RegisterRestClient(baseUri = "http://localhost:7001/api") @Path("persons") public interface PersonClient { @GET @Path("/{id}") @Produces(MediaType.APPLICATION_JSON) Person get(@PathParam("id")long id); @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) Person add(Person person); @GET @Produces(MediaType.APPLICATION_JSON) List<Person> findByName(@QueryParam("name") String name); } Qualifier RestClientにす るおまじない これだけでOK