WebClientを使ったアプリケーションを本番運用するために意識しておきたい設定とその設定方法をまとめてみました。
自分の現場で意識していることが中心になりますが、コピペでそのまま本番アプリケーションに使うようなコードを残しておきます!
依存ライブラリ
implementation 'org.springframework.boot:spring-boot-starter-webflux:2.5.0'
共通の設定
ある程度共通で実装するであろう設定をまとめておきます
@Bean
public WebClient webClient(MetricsWebClientCustomizer metricsWebClientCustomizer) {
var connectionProvider = ConnectionProvider.builder("sample")
.maxConnections(100) // コネクションプール数
.maxIdleTime(Duration.ofSeconds(59)) // keep aliveタイムアウト
.metrics(true) // コネクション数のメトリクスを有効化
.build();
var httpClient = HttpClient.create(connectionProvider)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 1_000) // connection timeout
.responseTimeout(Duration.ofMillis(500)); // read timeout
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.baseUrl("http://localhost:8080")
.apply(metricsWebClientCustomizer::customize) // http clientのメトリクスを収集
.build();
}
URLへの変数の埋め込み
可変な部分はクエリパラメータも含めてuriの第二引数以降で指定しないとダメ。
可変なURLをそのまま指定するとメトリクスがURLごと保持され、OOMEの原因にもなり得ます。
webClient.get()
.uri("/sample/{variable}", "hoge") // /sample/hoge
.retrieve()
.bodyToMono(String.class);
keepaliveの無効化
keep-aliveを無効化するにはHttpClient生成時にkeepAlive
にfalse
を設定します。
※デフォルトは有効
var httpClient = HttpClient.create(connectionProvider)
...
.keepAlive(false)
....
Proxyヘッダ
以下、認証があるproxyに対してWebClientを使って接続するときの実装方法です。
@Bean
public WebClient proxyWebClient() {
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(HttpClient.create()
.proxy(typeSpec -> {
typeSpec.type(ProxyProvider.Proxy.HTTP)
.host("localhost")
.port(8080)
// basic認証は対応している
//.username("username")
//.password(s -> "password")
.httpHeaders(headers -> {
// ここで認証用のヘッダを乗せる
headers.add("uuid", UUID.randomUUID().toString());
});
})
))
.build();
}
単体テスト
WebClientの単体テストにはOkHttpを使うとよいと思います。
以下のようなWebClientに依存するコンポーネントをサンプルにテストコードを書いてみます。
@RequiredArgsConstructor
@Component
public class SampleApiClient {
private final WebClient webClient;
public Mono<String> sample() {
return webClient.get()
.uri("/sample")
.retrieve()
.bodyToMono(SampleResponse.class)
.map(res -> res.getMessage());
}
@Data
public static class SampleResponse {
private String message;
}
}
OkHttpを使用するために、依存を追加します。
dependencies {
...
// 依存を追加
testImplementation 'com.squareup.okhttp3:okhttp'
testImplementation 'com.squareup.okhttp3:mockwebserver'
}
Mock用のHTTPサーバを起動してテストします。
public class SampleApiClientTest {
private static MockWebServer mockServer;
private ObjectMapper objectMapper = new ObjectMapper();
private SampleApiClient sampleApiClient;
@BeforeEach
void beforeEach() {
var webClient = WebClient.builder()
.baseUrl("http://localhost:" + mockServer.getPort())
.build();
sampleApiClient = new SampleApiClient(webClient);
}
@BeforeAll
static void startMock() throws IOException {
mockServer = new MockWebServer();
mockServer.start();
}
@AfterAll
static void shutdownMock() throws IOException {
mockServer.shutdown();
}
@Test
@SneakyThrows
void test() {
// Mockサーバのレスポンスを設定する
var responseBody = new SampleApiClient.SampleResponse();
responseBody.setMessage("Hello, world.");
var mockedResponse = new MockResponse()
.setBody(objectMapper.writeValueAsString(responseBody))
.addHeader("Content-Type", "application/json");
mockServer.enqueue(mockedResponse);
// リクエスト送信
var actual = sampleApiClient.sample();
StepVerifier.create(actual)
.expectNext("Hello, world.")
.verifyComplete();
}
}