Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Spring Frameworkの新標準!? ~ RestClientとHTTPインターフェー...

荻原利雄
November 06, 2024

Spring Frameworkの新標準!? ~ RestClientとHTTPインターフェース入門 ~

荻原利雄

November 06, 2024
Tweet

More Decks by 荻原利雄

Other Decks in Technology

Transcript

  1. 2 荻原 利雄(オギワラ トシオ) • 所属 / 職種 - 株式会社豆蔵

    - ビジネスソリューション事業部 - 主幹ソフトウェアエンジニア • プロフィール - オブジェクト指向とともにエンタープライズな Javaアプリを作りつづけて25年のアラフィフエ ンジニア - ここ数年は大規模基幹システムを支える JakartaEEフルスタックなフレームワークの開発 を行っている - 昨年度からはSpring Bootを使った共通機能の開 発も行っている extact-io 豆蔵デベロッパーサイト 豆蔵デベロッパーサイトで連載中! Springの小話
  2. 3 © 2024 Mamezou Inc. All rights reserved. 豆蔵 採用

    会社紹介 日本におけるSpringの先駆的企業 (だたし今となってはみなさん元豆蔵) 1粒の知性が ソフトウェアエンジニアリングを変える。 豆蔵では共に高め合う仲間を募集しています! 豆蔵は、オブジェクト指向技術含めた ソフトウェア工学を産業、企業に浸透 させるべく1999年に創業しました 創業以来、ソフトウェア工学を基軸に ロボティクス、AI/IoTによる工場のデ ジタル化、車載向けECU統合化、 ERP/Open Sourceによる基幹系シス テム刷新等に取り組んでいます
  3. お断り • 本資料は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のまとまったサンプルとしても使えるので見てみね♪
  4. RestTemplateのコードを眺めてみる(1/7) 8 // RestTemplateの作り方 String baseUrl = "http://localhost:8080"; DefaultUriBuilderFactory uriFactory

    = new DefaultUriBuilderFactory(baseUrl); RestTemplate restTemplate = new RestTemplate(); 作り方はいくつかあるが、都 度指定するのは面倒なので baseUrlは指定してしておく。 これを前提に以降のコードを 眺める
  5. 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メソッドを 使う必要がある
  6. 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!
  7. 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}に対す るバインド値です
  8. 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メソッド によって使い分ける
  9. 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を使う必要あり
  10. RestTemplateのコードを眺めてみる(7/7) 14 // 本を削除する @Override public void delete(int id) throws

    NotFoundException { restTemplate.delete("/books/{id}", id); } RestTemplate#deleteはHTTPのDELETEメソッドの送信 の意味 やることが単純なのでこのメソッドはシンプル・・
  11. RestTemplateのAPIって、、 (昔はよかったのかもしれないが、今となっては) • やりたいことは同じでもgetForObject, getForEntity, exchangeと3つもある • それぞれ引数をいっぱい取るので、どこになにを渡せばいいの かとても分かりづらい •

    加えてオーバーロードメソッドが沢山あるので、どれを使えば いいのか更に分かりづらい • もっというとラムダが使えないのでサクッと書けない&記述が 冗長になりがち(昨今のAPIスタイルと比べて) 15 そこで RestClientの誕生
  12. 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
  13. 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
  14. 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
  15. 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
  16. 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
  17. ここまでのまとめ • RestTemplateからみたRestClientのコンセプトや機能の違いは? • コンセプトの違い ➢ APIのスタイルがオーバーロードからfluentへ • 機能の違い ➢

    極論すると、いや極論しなくても機能は同じ • つまり、既存のAPIをリファクタリングしただけ • ではなぜ今さら? • (Springの)利用者の視点 ➢ APIがわかりづらい、使いづらい、もう勘弁して(個人の感想です) • (Springの)開発者の視点 ➢ オーバーロード地獄で、機能追加が大変(、もう勘弁して) そして、RestClientの爆誕へ 21
  18. 結局どうなのか • 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クライア ント機能の標準になると考えるのが妥当
  19. 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
  20. ある値をヘッダーに常に設定してリクエストを送 信したい! • BuilderのdefaultHeaderで設定するだけ 26 RestClient restClient = RestClient.builder() .defaultHeader("Sender-Name",

    BookClientApplication.class.getSimpleName()) . … .build(); このBuilderで生成されたRestClientから送信されるリクエストヘッダには常に Sender-Name: BookClientApplication が設定されるようになる defaultHeaderメソッドはConsumer<HttpHeaders>の引数も取るので処理を渡すことも可能だが、ゴ リゴリやる場合は後述のClientHttpRequestInitializerの実装クラスを作成した方がよいと思う RestClient
  21. あるクエリーパラメータをに常に設定してリクエ ストを送信したい! • 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
  22. 自分で設定した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
  23. 動的にリクエストヘッダに値を設定したい • 行いたいヘッダー処理を実装した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
  24. リクエスト・レスポンスの送受信の前後に処理を 割り込ませたい • 割り込んだときに行いたい処理を実装した 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
  25. 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
  26. レスポンスエラーハンドリグをまとめて行いたい • エラー処理を実装した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
  27. 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
  28. 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
  29. 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の アノテーション名が違うだけで 他はすべて同じ。ここで定義さ れた内容に従ってリクエストの 送受信が行われる。
  30. 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を使って実際の リクエストの送受信は行われます ココのインター フェースを作るだけ
  31. 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
  32. 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
  33. 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
  34. 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
  35. なぜか: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 引数にアノテーション を付ける
  36. 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インタフェース
  37. 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インタフェースの利用
  38. 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