きっかけ
web apiを作ったのだが、ユニットテストで毎回難儀していたので、楽にテストが書けると聞いたRestClientを使うことに。
実装
今回は、RestClientを生成するConfigクラスと、実際にAPIとして使う場所を分けてる。
あと、余談だが、使っているweb apiは、NewsAPIっていう海外のニュース情報が取得できるサイト。
無料アカウントなら、アクセスできる回数に制限があるけど自由に使えるので、情報収集がてら使ってみることに。
前提
やるにしても環境情報載せないとね。。。
一部抜粋なので、適時読み替えて。
必要そうなものだけ載せてる。※抜けてたらすまぬ
plugins { id 'org.springframework.boot' version '3.3.3' } repositories { mavenCentral() gradlePluginPortal() } allprojects { ext { springVersion = "3.3.3" } } dependencies { implementation "org.springframework.boot:spring-boot-starter-web:$springVersion" implementation 'org.apache.httpcomponents.client5:httpclient5' }
RestClientを生成しているConfigクラス。
参考サイトを元に、とりあえず真似てみる。
最低限であれば、HttpClientBuilder.create()
あたりからメソッドの最後まであればいいと思う。
package com.galewings.config; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.DefaultResponseErrorHandler; import org.springframework.web.client.RestClient; import java.util.concurrent.TimeUnit; @Configuration public class NewsApiConfig { @Value("${newsapi.url}") private String baseUrl; @Bean RestClient customRestClient() { var connectionConfig = ConnectionConfig.custom() // TTLを設定 .setTimeToLive(TimeValue.of(59, TimeUnit.SECONDS)) // コネクションタイムアウト値を設定 .setConnectTimeout(Timeout.of(1, TimeUnit.SECONDS)) // ソケットタイムアウト値を設定(レスポンスタイムアウトと同義) .setSocketTimeout(Timeout.of(5, TimeUnit.SECONDS)) .build(); // PoolingHttpClientConnectionManagerを使うことでコネクションがプールされて // リクエストごとにコネクションを確立する必要がなくなる var connectionManager = PoolingHttpClientConnectionManagerBuilder.create() .setDefaultConnectionConfig(connectionConfig) // 全ルート合算の最大接続数 .setMaxConnTotal(100) // ルート(基本的にはドメイン)ごとの最大接続数 // !!! デフォルトが「5」で高負荷には耐えられない設定値なので注意 !!! .setMaxConnPerRoute(100) .build(); var httpClient = HttpClientBuilder.create() .setConnectionManager(connectionManager) .build(); var requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient); return RestClient.builder() .baseUrl(baseUrl) .requestFactory(requestFactory) .defaultStatusHandler(new DefaultResponseErrorHandler()) .build(); } }
実際にRestClientを利用しているServiceの実装
簡単に説明すると、NewsApiConfig で生成されたRestClient のインスタンスをDIしてやって、topHeadlinesメソッドで使ってる。※restClient.get()~~~のあたり ※一部抜粋なので、これをコピペしただけでは動かないので注意
public class NewsApiService { @Value("${newsapi.api-key}") private String apiKey; private final RestClient restClient; @Autowired public NewsApiService(RestClient restClient) { this.restClient = restClient; } public NewsApiResponseDto topHeadlines(OptionalRequestParam optionalRequestParam) throws URISyntaxException, IllegalAccessException, IOException { if (Objects.isNull(apiKey)) { throw new GaleWingsSystemException("newsapi.api-key not found. add .env file"); } Map<String, String> param = optionalRequestParam.queryParamMap(); param.put("apiKey", apiKey); String paramStr = generateQueryParamStr(param); String response = restClient.get().uri("/top-headlines?" + paramStr).retrieve().body(String.class); ObjectMapper om = new ObjectMapper(); return om.readValue(response, NewsApiResponseDto.class); } }
今まで、URL組み立ててコネクション確立して、レスポンスのステータスコード見て~みたいなことをしていたけど、よしなにRestClientがやってくれるので、あんまり気にしなくていい。
ボディ部の文字列表現を受け取って、Jacksonでオブジェクトにマッピングしている。
だいぶ楽に書けた。
ユニットテスト
ここで楽するためにやってきた!
package com.galewings.service; import com.galewings.dto.newsapi.request.OptionalRequestParam; import com.galewings.dto.newsapi.response.NewsApiResponseDto; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.http.HttpMethod; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.client.MockRestServiceServer; import org.springframework.web.client.RestClient; import java.io.IOException; import java.net.URISyntaxException; import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; class NewsApiServiceTest { @Test void testTopHeadlines2() throws URISyntaxException, IOException, IllegalAccessException { String apiKey = "test"; var restClientBuilder = RestClient.builder(); MockRestServiceServer mockServer = MockRestServiceServer.bindTo(restClientBuilder).build(); mockServer.expect(requestTo("/top-headlines?country=jp&apiKey=" + apiKey + "&category=test")) .andExpect(method(HttpMethod.GET)) .andRespond(withSuccess().body("{\"status\":\"ok\",\"totalResults\":4145,\"articles\":[{\"source\":{\"id\":null,\"name\":\"ETFDailyNews\"},\"author\":\"MarketBeatNews\",\"title\":\"test\",\"description\":\"test description\",\"url\":\"https://localhost\",\"urlToImage\": null,\"publishedAt\":\"2024-09-17T08:44:33Z\",\"content\":\"test\"}]}")); var restClient = restClientBuilder.build(); newsApiService = new NewsApiService(restClient); ReflectionTestUtils.setField(newsApiService, "apiKey", apiKey); OptionalRequestParam param = new OptionalRequestParam(); param.country = "jp"; param.category = "test"; NewsApiResponseDto result = newsApiService.topHeadlines(param); Assertions.assertNotNull(result); Assertions.assertEquals("ok", result.getStatus()); Assertions.assertEquals(4145L, result.getTotalResults()); } }
注目するところは、MockRestServiceServer のあたり。
コイツが、特定リクエストのレスポンスを返してくれるので、実際にサーバーにアクセスしたりすることなく、実装箇所のテストができるってわけ。
これ使う前は、URLのコネクション確率をモック化したりしてたけど、トラップが大量にあるせいで、前髪がかなり後退した。
実際のサーバーにアクセスせず、モックが簡単に書けるのが、かなりいい。
使わない前の状態が、かなり面倒くさかったから、かなり便利になった。
雑記・感想など
調査する段階で、RestTemplateとか出てきたけど、一番新しいのがRestClientらしく、それが推奨らしい。情報がRestTemplateばかり出てくるので悩んだが、どうせならということでRestClientにした。
Springのバージョン違いでRestClientがライブラリになくてかなり迷った。。。
サンプルコード載せるときは、バージョン情報は必須で書くべきだと思った。
あと、import文も記載が欲しい。どのライブラリのやつ使ってるのかわからないから、それが判断できるものがないと、たくさんライブラリ利用しているプロジェクトだと、どれをimportすればいいのか悩む。
自分も書くときは気をつけないといかんなと思った。
関連リンク
News API – Search News and Blog Articles on the Web
REST Clients :: Spring Framework
Spring Framework 6.1 から追加された RestClient を試してみる #Java - Qiita