Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Spring Frameworkの新標準!? ~ RestClientとHTTPインターフェー...
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
荻原利雄
November 06, 2024
Technology
3k
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
270
実践ArchUnit ~実例による検証パターンの紹介~
ogiwarat
2
1.7k
Spring Boot vs MicroProfile ~クラウドネイティブにおけるフレームワークの比較と選択~
ogiwarat
2
2.7k
第1回 AWSとGitHub勉強会 - キックオフ -
ogiwarat
0
1.2k
第2回 AWSとGitHub勉強会 - CodespacesとHelidonの利用 -
ogiwarat
0
1.2k
第3回 AWSとGitHub勉強会 - GitHub Actionsを使ったCI環境の構築 -
ogiwarat
0
1.2k
第4回 AWSとGitHub勉強会 - GitHub Actionsを使ったCD環境の構築 -
ogiwarat
0
1.2k
第5回 AWSとGitHub勉強会 - AWS EC2環境の構築 -
ogiwarat
0
1.1k
第6回 AWSとGitHub勉強会 - AWS ECS Fargate環境の構築 -
ogiwarat
0
1.4k
Other Decks in Technology
See All in Technology
Genieを崇めよ
kameitomohiro
0
150
[2026-09-11]SREは誰のもの?運用エンジニアが始める 「SRE領域への越境」とチームの進化の軌跡 〜Road to NEXT CRE
tosite
0
290
beyond jj: config & tools ecosystem
indirect
0
490
負債のメタファと2026年 / Debt Metaphor in Agentic Engineering Age 202609 Edition
twada
PRO
8
3.6k
日経電子版を支えていく Kasane Design System/fec_fukuoka
nikkei_engineer_recruiting
0
1.6k
データ_AIの事業の勝敗をわけるもの
nek0128
0
390
ユーザー価値を届け続けるためにウォンテッドリーが大切にしている文化
kotaminato
0
160
顧客に向き合う開発組織へ。リアーキテクチャとフィーチャーチーム化で挑む組織改革
safie
0
2k
アプリログインとWeb認証基盤をつなぐ ASWebAuthenticationSession 作法
shimastripe
1
330
【技術的負債conf】事業成長に伴う技術的負債の説明責任とAIによるモニタリング、認知的負債について
i35_267
3
1.6k
LTのテーマ どうきめてる?〜5つの型と私のやり方〜
yama3133
1
110
例外の正しい扱い方 そのエラー try-catchして大丈夫?
jinwatanabe
3
510
Featured
See All Featured
Principles of Awesome APIs and How to Build Them.
keavy
128
18k
The Cult of Friendly URLs
andyhume
79
7k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
4.8k
jQuery: Nuts, Bolts and Bling
dougneiner
66
8.6k
Amusing Abliteration
ianozsvald
1
300
The State of eCommerce SEO: How to Win in Today's Products SERPs - #SEOweek
aleyda
2
12k
Lightning talk: Run Django tests with GitHub Actions
sabderemane
0
250
Tell your own story through comics
letsgokoyo
1
1.1k
Visual Storytelling: How to be a Superhuman Communicator
reverentgeek
2
660
First, design no harm
axbom
PRO
2
1.3k
The Pragmatic Product Professional
lauravandoore
37
7.4k
Primal Persuasion: How to Engage the Brain for Learning That Lasts
tmiket
0
450
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