【Spring MVC】WebClientの導入

概要

RESTなAPIにアクセスするのはRestTemplateではなくWebClientが今後推奨となるため、WebClientの概要についてまとめた。
WebClientを使用するための準備と、どんなメソッドがあるのかを紹介している。

 

前提

今後動作確認を行う際には、以前の記事で作成したREST APIアプリにリクエストを行ってレスポンスを取得する。
※今回のMavenプロジェクトの作成についても、以下をベースとしている

あわせて読みたい

概要 これから数回にかけてREST APIについて学んだことを載せていく。 今回はREST APIの仕組みとプロジェクト作成方法について紹介する。 尚、RESTとは何かということについては取り扱わない。   仕組み[…]

 

WebClient

WebClientは、Spring Framework5以降で導入された非同期・リアクティブなHTTPクライアントとなる。

REST APIや外部サービスとHTTP経由で通信する際に使用し、GET/POST/PUT/DELETE などのHTTPメソッドを柔軟に扱える。
SpringMVCでは一律同期的に処理を行うため、以降の記事では非同期の処理については扱わない

 

基本的な使い方

WebClientでは、以下のようにメソッドチェーンを使用して柔軟にAPI通信を行う。(※GET通信の例)

webClient.get()
.uri(URI情報)
.retrieve()
.bodyToMono(取得したいレスポンスの型)
.block();

 

webClient.get()

GET通信を行うメソッド。(「webClient」は、WebClientのインスタンス)
この段階ではリクエストはまだ実施せず、内部型のWebClient.RequestHeadersUriSpecオブジェクトを返却する。

 

WebClient.RequestHeadersUriSpec.uri(URI情報)

リクエストの URI を設定する。
WebClient.RequestHeadersSpec<?> を返し、ヘッダー設定や送信メソッドが利用可能になる。

 

WebClient.RequestHeadersSpec.retrieve()

リクエストを送信し、レスポンスを扱うWebClient.ResponseSpecを返却する。

 

WebClient.ResponseSpec.bodyToMono(取得したいレスポンスの型)

レスポンスボディを非同期で扱う Mono<取得したい型> を返す。
※上記のメソッドは、レスポンスボディのみ(ステータスやヘッダーは含まない)を扱う

 

Mono.block()

非同期処理が完了するまで待機し、同期的な戻り値として取得する。
Spring MVCでは、基本的にこのメソッドで同期処理に変換する。

 

基本的なメソッド

WebClientは内部型オブジェクトとMonoオブジェクトを使用してAPI通信を行う。
以下にWebClientにてメソッドチェーンで使用する基本的なメソッドを紹介する。

 

メソッド 説明
WebClient.get() GET通信を行う
WebClient.post() POST通信を行う
WebClient.put() PUT通信を行う
WebClient.delete() DELETE通信を行う
UriSpec.uri(URI情報) URI情報を設定する
RequestBodySpec.bodyValue(リクエストボディ) POSTやPUT通信に使用するリクエストボディを設定する
RequestBodySpec.contentType(コンテンツタイプ) Content-Typeヘッダーを設定する
RequestHeadersSpec.headers(ヘッダー情報) リクエストのヘッダー情報を設定
RequestHeadersSpec.retrieve() リクエスト送信を行い、ResponseSpecを取得する
RequestHeadersSpec.exchangeToMono() リクエスト送信を行い、ClientResponseを変換してMono<T>を取得する
※ClientResponseはヘッダー/ステータス/ボディにアクセス可能なオブジェクト
ResponseSpec.bodyToMono(レスポンス型) リクエスト送信を行い、Mono<T>を取得する
Mono<T>.block() 同期的にT型のレスポンスを取得する

 

事前準備

WebClientを使用するための事前準備を行う。

 

必要ライブラリ

WebClientまわりの機能を利用するため、必要な資材をpom.xmlに追加する。

 

pom.xml


<dependencies>
	<!-- Lombok -->
	<dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.18.30</version>
		<scope>provided</scope>
	</dependency>

	<!-- Spring Web MVC -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-webmvc</artifactId>
		<version>5.3.30</version>
	</dependency>

	<!-- Spring WebFlux -->
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-webflux</artifactId>
		<version>5.3.30</version>
	</dependency>

	<!-- Project Reactor Core -->
	<dependency>
		<groupId>io.projectreactor</groupId>
		<artifactId>reactor-core</artifactId>
		<version>3.4.34</version>
	</dependency>

	<!-- Project Reactor Netty -->
	<dependency>
		<groupId>io.projectreactor.netty</groupId>
		<artifactId>reactor-netty</artifactId>
		<version>1.0.39</version>
	</dependency>

	<!-- SLF4J API -->
	<dependency>
		<groupId>org.slf4j</groupId>
		<artifactId>slf4j-api</artifactId>
		<version>1.7.36</version>
	</dependency>

	<!-- Logback Classic -->
	<dependency>
		<groupId>ch.qos.logback</groupId>
		<artifactId>logback-classic</artifactId>
		<version>1.2.11</version>
	</dependency>

	<!-- Jackson Datatype JSR310 for Java Time API -->
	<dependency>
		<groupId>com.fasterxml.jackson.datatype</groupId>
		<artifactId>jackson-datatype-jsr310</artifactId>
		<version>2.15.2</version>
	</dependency>
</dependencies>

 

WebClientを使用するためには以下の資材が必要となる。

・Spring WebFlux: WebClient本体の機能
・Project Reactor Core: Monoオブジェクトなどの機能
・Project Reactor Netty: 非同期HTTP通信の機能 

 

Bean定義

WebClientをDIコンテナから取得するため、Bean定義を行う。
簡潔に記述できるため、Javaベースで作成する。

 

WebClientConfig.java


package com.example.webclient_prototype.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;

import reactor.netty.http.client.HttpClient;

/**
 * WebClientのBean定義
 */
@Configuration
public class WebClientConfig {
	
	@Bean
	public HttpClient httpClient() {
		return HttpClient.create()
	            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // 接続タイムアウト(5秒)
	            .responseTimeout(Duration.ofSeconds(10)); // レスポンス全体のタイムアウト(10秒)
	}

	@Bean
	public ReactorClientHttpConnector reactorClientHttpConnector(HttpClient httpClient) {
		return new ReactorClientHttpConnector(httpClient);
	}

	@Bean
	public WebClient webClient(ReactorClientHttpConnector reactorClientHttpConnector) {
		return WebClient.builder()
				.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
				.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE + ", " + MediaType.APPLICATION_PROBLEM_JSON_VALUE)
				.clientConnector(reactorClientHttpConnector)
				.build();
	}
}

 

.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE + “, ” + MediaType.APPLICATION_PROBLEM_JSON_VALUE)

WebClientを使用して行うすべてのHTTPリクエストに、共通のHTTPヘッダーを設定している。

 

applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="
         http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans.xsd
         		http://www.springframework.org/schema/context
		http://www.springframework.org/schema/context/spring-context-3.1.xsd">

	<!-- コンポーネントスキャン -->
	<context:component-scan base-package="com.example.webclient_prototype.biz" />
	<!-- WebClientのBean定義 -->
	<bean class="com.example.webclient_prototype.config.WebClientConfig"/>
</beans>

 

<!– WebClientのBean定義 –>
<bean class=”com.example.webclient_prototype.config.WebClientConfig”/>

定義したJavaベースのBean定義ファイルをDIコンテナに登録する。
※コンポーネントスキャン対象のパッケージ配下に格納していれば不要

 

APIアクセスクラス

WebClientのDIを行い、APIにアクセスするメソッドを今後追加していく。

 

WebApiClient.java


package com.example.webclient_prototype.biz;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

/**
 * WebClientを使用したAPI疎通クラス
 */
@Component
public class WebApiClient {

	@Autowired
	private WebClient webClient;
	
}

 

リソースクラス

REST APIアプリから返却されるレスポンスを、リソースクラスにマッピングするため用意する。

 

Resource.java


package com.example.webclient_prototype.resource;


import java.time.LocalDate;

import com.fasterxml.jackson.annotation.JsonFormat;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
 * リソースクラス
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Resource {
	/** ID */
	private String id;
	/** 名前 */
	private String name;
	/** とある日付 */
	@JsonFormat(pattern = "yyyy-MM-dd")
	private LocalDate hogeDate;
}

 

APIエラー情報クラス

例外発生時(4xx系や5xx系)に返却されるレスポンスは、以下のクラスにマッピングする。

 

ApiErrorInfo.java


package com.example.webclient_prototype.resource.error;

import java.util.List;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;

/**
 * APIエラー情報クラス
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class ApiErrorInfo {
	private Integer status;
	private String errorTitle;
	private String errorMsg;
	private String errorCode;
	private List<Item> errors;

	@Data
	private static class Item {
		private String target;
		private String msg;
	}
}

 

 

 

動作確認用クラス

WebClientの動作を簡易的に確認するため、エントリーポイントとなるクラスを用意する。

 

Main.java


package com.example.webclient_prototype.executor;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.example.webclient_prototype.biz.WebApiClient;

public class Main {

	public static void main(String[] args) {
		try (var context = new ClassPathXmlApplicationContext("/META-INF/spring/applicationContext.xml")) {
			// DIコンテナから取得
			var client = context.getBean(WebApiClient.class);
			
		}
	}

}

 

自作例外クラス

WebClientはRestTemplateのようにHTTPステータスごとに細かい例外クラスが用意されておらず、4xx系や5xx系のエラーはすべてWebClientResponseExceptionとして処理される。

このため、例外発生時にはステータスコードを元に条件分岐を行う必要がある。
ステータスごとの処理を明確にして@ExceptionHandler側の実装をシンプルに保つために、WebClientResponseExceptionを自作の例外クラスに変換してスローする方針としている。

今後は、例外ハンドリングを行う際にはこの自作例外クラスを適切にスローしていく。
※実際に@ExceptionHandlerを用いたハンドリングについては既に別の記事で扱っているため、WebClientまわりの記事では割愛する

 

ClientErrorException.java


package com.example.webclient_prototype.exception;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

// 4xx系エラー
@Getter
@Setter
@ToString
public class ClientErrorException extends RuntimeException {
	
	private Integer status;
	private String title;
	private String detailMessage; // toStringで表示するためだけに追加
	
    public ClientErrorException(String message) {
    	this(null, message, null);
    }
    
    public ClientErrorException(String title, String message) {
    	this(title, message, null);
    }
    
    public ClientErrorException(String title, String message, Integer status) {
    	super(message);
    	this.title = title;
    	this.status = status;
    	this.detailMessage = message;
    }
}

 

ServerErrorException.java


package com.example.webclient_prototype.exception;

// 5xx系エラー
public class ServerErrorException extends RuntimeException {
    public ServerErrorException(String message) {
        super(message);
    }

}

 

UnknownErrorException.java


package com.example.webclient_prototype.exception;

// 想定外エラー
public class UnknownErrorException extends RuntimeException {
    public UnknownErrorException(String message) {
        super(message);
    }
}

 

 

 

 

まとめ

 

☑ RestTemplateは今後機能追加がないため、WebClientの使用が推奨される

☑ WebClientはメソッドチェーンで柔軟なAPI通信を実装できる

☑ SpringMVC環境では、 WebClientを同期的に使用する

スポンサーリンク